mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2024-12-17 17:20:21 +01:00
Compare commits
279 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d438cec674 | |||
| e4c5dbdb08 | |||
| 5d1a393131 | |||
| 8e3a713a49 | |||
| 7cec79e79b | |||
| e9db7e8e58 | |||
| f53882049f | |||
| 8f4c9a673a | |||
| 02f980e968 | |||
| 412f75219a | |||
| c8c81a840b | |||
| 925cb39397 | |||
| f6ceb89c4b | |||
| a35d392f76 | |||
| 1306c84938 | |||
| 6484e36b82 | |||
| 918be31f2c | |||
| 648884044f | |||
| 505c6cb062 | |||
| 065c8f8861 | |||
| 6c509027f7 | |||
| 4eafdc3723 | |||
| 77f5c678d5 | |||
| 5e71d00ace | |||
| c2169f7813 | |||
| 7ff4b05c90 | |||
| d7f8bff957 | |||
| bda0917787 | |||
| 0eae7f56f3 | |||
| 24e3806b25 | |||
| f606e5742c | |||
| c6af2c285a | |||
| 35b7f2cb1c | |||
| 76a33a3743 | |||
| b6fd5e61b6 | |||
| fe639eed48 | |||
| 638fd3756a | |||
| de81434b36 | |||
| 433b3652fb | |||
| 86fe2212c3 | |||
| 182225a5b1 | |||
| e10c63dc64 | |||
| 29941729d2 | |||
| ee53377070 | |||
| 0deb6a4eb1 | |||
| b84ef0ce35 | |||
| 34dee32def | |||
| 94851f177b | |||
| e560b49d14 | |||
| d8b52ee0d3 | |||
| a9c0f356b4 | |||
| 8a3710482b | |||
| b3a4ea8e57 | |||
| 4be6c69633 | |||
| 005d99ad50 | |||
| 36f86beafd | |||
| 1fa79a2654 | |||
| 72611084d3 | |||
| a5db36469d | |||
| f8e6a78a3b | |||
| 5b0c8e0f76 | |||
| 2968cd0460 | |||
| 8ffb4e500e | |||
| 6133993cfe | |||
| 0d7a32877f | |||
| 18efc28d16 | |||
| 069395c2a0 | |||
| a725d2efac | |||
| 5f9710e4bb | |||
| ea6c5bfb0b | |||
| 14c279d1e0 | |||
| d2de81100d | |||
| 9b28ae6d9e | |||
| ea862a8f34 | |||
| 2bd1a82b7d | |||
| 97a37634ef | |||
| 28fbc1cd84 | |||
| b8ee2af5b7 | |||
| c234809894 | |||
| e1a997877a | |||
| 69218952c3 | |||
| dc2591b0bf | |||
| 2272b77b53 | |||
| d90e2f4436 | |||
| 9b6ca23dcb | |||
| 1410b4bd85 | |||
| 42a35e8c72 | |||
| 86fe28f1ed | |||
| 735359c279 | |||
| 3d20465662 | |||
| b64ae90850 | |||
| 366b58ead5 | |||
| 32bcaad126 | |||
| d65137882b | |||
| 095d613f01 | |||
| f22beefbd4 | |||
| c9bf36291b | |||
| 42ebe8bc60 | |||
| 96fb61ccfc | |||
| 1d698c114f | |||
| 154ce5843d | |||
| a5817d9feb | |||
| 72082af139 | |||
| eb36704f59 | |||
| 84fbff6148 | |||
| dd75d30c08 | |||
| 52803ae976 | |||
| ec73a31cb5 | |||
| 6ad5048b58 | |||
| ca0ce4dfdd | |||
| d27502ec47 | |||
| 655ad4d813 | |||
| e0566599a4 | |||
| 2546ad5351 | |||
| 0706e6e464 | |||
| c2adf40b5a | |||
| b220ea23e2 | |||
| a16e3eebd1 | |||
| 6d54c21774 | |||
| d2e6313ce3 | |||
| c2707d3c2b | |||
| 43381b5cf0 | |||
| 3f1d86623a | |||
| 5fde879fea | |||
| 157c4722ca | |||
| 5bc8bbe16c | |||
| d835f9df3d | |||
| 8f04e13750 | |||
| 39167ab3dd | |||
| e925d87548 | |||
| c2e2408394 | |||
| bb16922634 | |||
| a3310cd62e | |||
| 94a6f300ed | |||
| 1f8eb1d638 | |||
| 5a016378a8 | |||
| bac4e61997 | |||
| f3eeb9dcc2 | |||
| 240ca30f91 | |||
| 835944ab24 | |||
| d53ef24bf1 | |||
| e75be71d9a | |||
| 7fa2f2f72e | |||
| 5fd8e6e4fe | |||
| d3b255b7cb | |||
| 96fba950ff | |||
| 4a404f14d9 | |||
| 4f893d9502 | |||
| 01447716fa | |||
| c9df591e52 | |||
| a56bc6760b | |||
| 80690326cb | |||
| 3f6c74f975 | |||
| 8b8846c7b7 | |||
| 6c78bbc178 | |||
| eaf720a0f2 | |||
| 0e7d81681f | |||
| 56fcaf514e | |||
| 49bd866c4b | |||
| 8660bf420a | |||
| 60a73a539e | |||
| 93d56a25bc | |||
| 7fb3c4abdb | |||
| 191d833947 | |||
| 19ca4f7447 | |||
| b86b5578de | |||
| 435ea9a453 | |||
| 1f93d91af5 | |||
| 405348732b | |||
| 109b6e0cff | |||
| b403201310 | |||
| 9ff11f886e | |||
| bc2b135880 | |||
| ce9b909495 | |||
| f1c22a3308 | |||
| 7b7c3227e3 | |||
| 2c6e4db603 | |||
| 8d72757f62 | |||
| 61a3eb32ee | |||
| 64dc758ffd | |||
| eebf014ff7 | |||
| 2155060ad0 | |||
| db2ccaa450 | |||
| 4b7458b58f | |||
| 05383477d9 | |||
| 0c4848ad9e | |||
| b66a3d0595 | |||
| 8ac7677648 | |||
| c27973d202 | |||
| 51a2e09714 | |||
| ec8ae9febe | |||
| 7213913d51 | |||
| e37654772f | |||
| b7709c59d3 | |||
| 5807a0a2fa | |||
| 395654098c | |||
| 2d643e8d29 | |||
| 4e9703f0ff | |||
| b0b249a56a | |||
| 92c89632d4 | |||
| d54b453b49 | |||
| e6e27db243 | |||
| b629c22ee0 | |||
| f7d7f5461f | |||
| c544a636f6 | |||
| 73de74d7e9 | |||
| 654a7885c3 | |||
| daf67c0456 | |||
| e361bcf140 | |||
| 5de9087207 | |||
| 364b62320b | |||
| d83a6b7133 | |||
| cd21a74b83 | |||
| 6d523d5b4b | |||
| 2a321b3ff8 | |||
| 865a32c608 | |||
| e3df7945d5 | |||
| bb1620d7d2 | |||
| edc5a4c31b | |||
| 4260c20012 | |||
| 1a7efbc333 | |||
| e4984cb38d | |||
| dfa9775d7e | |||
| e39544dd24 | |||
| 71bcfc2848 | |||
| 91f10c056f | |||
| 3d8d84f978 | |||
| fec34ca875 | |||
| 3a0920e950 | |||
| f4ae60756c | |||
| eedc1b2860 | |||
| 24a35698dc | |||
| 7e37155938 | |||
| c8b38183c9 | |||
| 90a866ca56 | |||
| 7e9e71ffbd | |||
| 5da8aef794 | |||
| 09bbaa1c94 | |||
| 3a879b755b | |||
| c6f4d62d6c | |||
| 5ad356172f | |||
| 0a6c464a8d | |||
| 1aa464bdb2 | |||
| bce829ef58 | |||
| 7df300cf36 | |||
| 9b35ff6f11 | |||
| c4cbb49f57 | |||
| 2b3eebb7a2 | |||
| a1328c287c | |||
| f2d498dd79 | |||
| 6b8fc6fdcf | |||
| 0e585d5e5b | |||
| 520d8868ef | |||
| 7192448303 | |||
| 0f0f65533a | |||
| 78a38cb080 | |||
| f102f39147 | |||
| cd349e80ce | |||
| 430dc5bd2e | |||
| 3e0b863b64 | |||
| d6afee11bc | |||
| 6ef3a9e668 | |||
| fbe3353434 | |||
| d6d2e6e1eb | |||
| 14d5078404 | |||
| da1d20c17f | |||
| afc324dc4f | |||
| f81e457e09 | |||
| bd30b80e15 | |||
| da9a7f4642 | |||
| 838a759a76 | |||
| f1ff27218c | |||
| 8738cf332f | |||
| 4f602d4571 | |||
| d0b75e0dd3 | |||
| 1bb98706a6 | |||
| 6b7c13f20a | |||
| 93d2bb98b5 | |||
| 3b614e95e3 |
@@ -270,7 +270,7 @@ jobs:
|
||||
|
||||
- name: Unix test
|
||||
if: matrix.os != 'windows-latest'
|
||||
timeout-minutes: 30
|
||||
timeout-minutes: 40
|
||||
shell: bash
|
||||
run: cabal test --test-show-details=direct
|
||||
|
||||
|
||||
@@ -5,14 +5,20 @@ on:
|
||||
pull_request_target:
|
||||
types: [opened, closed, synchronize]
|
||||
|
||||
permissions:
|
||||
actions: write
|
||||
contents: write
|
||||
pull-requests: write
|
||||
statuses: write
|
||||
|
||||
jobs:
|
||||
CLAssistant:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: "CLA Assistant"
|
||||
if: (github.event.comment.body == 'recheck' || github.event.comment.body == 'I have read the CLA Document and I hereby sign the CLA') || github.event_name == 'pull_request'
|
||||
if: (github.event.comment.body == 'recheck' || github.event.comment.body == 'I have read the CLA Document and I hereby sign the CLA') || github.event_name == 'pull_request_target'
|
||||
# Beta Release
|
||||
uses: cla-assistant/github-action@v2.1.3-beta
|
||||
uses: cla-assistant/github-action@v2.3.0
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
# the below token should have repo scope and must be manually added by you in the repository's secret
|
||||
@@ -33,4 +39,4 @@ jobs:
|
||||
#custom-pr-sign-comment: 'The signature to be committed in order to sign the CLA'
|
||||
#custom-allsigned-prcomment: 'pull request comment when all contributors has signed, defaults to **CLA Assistant Lite bot** All Contributors have signed the CLA.'
|
||||
#lock-pullrequest-aftermerge: false - if you don't want this bot to automatically lock the pull request after merging (default - true)
|
||||
#use-dco-flag: true - If you are using DCO instead of CLA
|
||||
#use-dco-flag: true - If you are using DCO instead of CLA
|
||||
|
||||
+27
-18
@@ -1,32 +1,41 @@
|
||||
FROM ubuntu:focal AS build
|
||||
ARG TAG=22.04
|
||||
|
||||
# Install curl and simplex-chat-related dependencies
|
||||
RUN apt-get update && apt-get install -y curl git build-essential libgmp3-dev zlib1g-dev libssl-dev
|
||||
FROM ubuntu:${TAG} AS build
|
||||
|
||||
### Build stage
|
||||
|
||||
# Install curl and git and simplex-chat dependencies
|
||||
RUN apt-get update && apt-get install -y curl git build-essential libgmp3-dev zlib1g-dev llvm-12 llvm-12-dev libnuma-dev libssl-dev
|
||||
|
||||
# Specify bootstrap Haskell versions
|
||||
ENV BOOTSTRAP_HASKELL_GHC_VERSION=9.6.3
|
||||
ENV BOOTSTRAP_HASKELL_CABAL_VERSION=3.10.1.0
|
||||
|
||||
# Install ghcup
|
||||
RUN a=$(arch); curl https://downloads.haskell.org/~ghcup/$a-linux-ghcup -o /usr/bin/ghcup && \
|
||||
chmod +x /usr/bin/ghcup
|
||||
|
||||
# Install ghc
|
||||
RUN ghcup install ghc 9.6.3
|
||||
# Install cabal
|
||||
RUN ghcup install cabal 3.10.1.0
|
||||
# Set both as default
|
||||
RUN ghcup set ghc 9.6.3 && \
|
||||
ghcup set cabal 3.10.1.0
|
||||
|
||||
COPY . /project
|
||||
WORKDIR /project
|
||||
RUN curl --proto '=https' --tlsv1.2 -sSf https://get-ghcup.haskell.org | BOOTSTRAP_HASKELL_NONINTERACTIVE=1 sh
|
||||
|
||||
# Adjust PATH
|
||||
ENV PATH="/root/.cabal/bin:/root/.ghcup/bin:$PATH"
|
||||
|
||||
# Set both as default
|
||||
RUN ghcup set ghc "${BOOTSTRAP_HASKELL_GHC_VERSION}" && \
|
||||
ghcup set cabal "${BOOTSTRAP_HASKELL_CABAL_VERSION}"
|
||||
|
||||
COPY . /project
|
||||
WORKDIR /project
|
||||
|
||||
# Adjust build
|
||||
RUN cp ./scripts/cabal.project.local.linux ./cabal.project.local
|
||||
|
||||
# Compile simplex-chat
|
||||
RUN cabal update
|
||||
RUN cabal install
|
||||
RUN cabal build exe:simplex-chat
|
||||
|
||||
# Strip the binary from debug symbols to reduce size
|
||||
RUN bin=$(find /project/dist-newstyle -name "simplex-chat" -type f -executable) && \
|
||||
mv "$bin" ./ && \
|
||||
strip ./simplex-chat
|
||||
|
||||
# Copy compiled app from build stage
|
||||
FROM scratch AS export-stage
|
||||
COPY --from=build /root/.cabal/bin/simplex-chat /
|
||||
COPY --from=build /project/simplex-chat /
|
||||
|
||||
+92
-58
@@ -1,134 +1,168 @@
|
||||
# SimpleX Chat Terms & Privacy Policy
|
||||
# SimpleX Chat Privacy Policy and Conditions of Use
|
||||
|
||||
SimpleX Chat is the first communication platform that has no user profile IDs of any kind, not even random numbers. Not only it has no access to your messages (thanks to open-source double-ratchet end-to-end encryption protocol and additional encryption layers), it also has no access to your profile and contacts - we cannot observe your connections graph.
|
||||
SimpleX Chat is the first communication network based on a new protocol stack that builds on the same ideas of complete openness and decentralization as email and web, with the focus on providing security and privacy of communications, and without compromising on usability.
|
||||
|
||||
If you believe that some of the clauses in this document are not aligned with our mission or principles, please raise it with us via [email](chat@simplex.chat) or [chat](https://simplex.chat/contact#/?v=1&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2FK1rslx-m5bpXVIdMZg9NLUZ_8JBm8xTt%23%2F%3Fv%3D1%26dh%3DMCowBQYDK2VuAyEALDeVe-sG8mRY22LsXlPgiwTNs9dbiLrNuA7f3ZMAJ2w%253D%26srv%3Dbylepyau3ty4czmn77q4fglvperknl4bi2eb2fdy2bh4jxtf32kf73yd.onion).
|
||||
SimpleX Chat communication protocol is the first protocol that has no user profile IDs of any kind, not even random numbers, cryptographic keys or hashes that identify the users. SimpleX Chat apps allow their users to send messages and files via relay server infrastructure. Relay server owners and providers do not have any access to your messages, thanks to double-ratchet end-to-end encryption algorithm (also known as Signal algorithm - do not confuse with Signal protocols or platform) and additional encryption layers, and they also have no access to your profile and contacts - as they do not provide any user accounts.
|
||||
|
||||
Double ratchet algorithm has such important properties as [forward secrecy](./docs/GLOSSARY.md#forward-secrecy), sender [repudiation](./docs/GLOSSARY.md#) and break-in recovery (also known as [post-compromise security](./docs/GLOSSARY.md#post-compromise-security)).
|
||||
|
||||
If you believe that any part of this document is not aligned with our mission or values, please raise it with us via [email](chat@simplex.chat) or [chat](https://simplex.chat/contact#/?v=1&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2FK1rslx-m5bpXVIdMZg9NLUZ_8JBm8xTt%23%2F%3Fv%3D1%26dh%3DMCowBQYDK2VuAyEALDeVe-sG8mRY22LsXlPgiwTNs9dbiLrNuA7f3ZMAJ2w%253D%26srv%3Dbylepyau3ty4czmn77q4fglvperknl4bi2eb2fdy2bh4jxtf32kf73yd.onion).
|
||||
|
||||
## Privacy Policy
|
||||
|
||||
SimpleX Chat Ltd. ("SimpleX Chat") uses the best industry practices for security and encryption to provide secure [end-to-end encrypted](./docs/GLOSSARY.md#end-to-end-encryption) messaging via private connections. This encryption cannot be compromised by the servers via [man-in-the-middle attack](./docs/GLOSSARY.md#man-in-the-middle-attack).
|
||||
SimpleX Chat Ltd uses the best industry practices for security and encryption to provide client and server software for secure [end-to-end encrypted](./docs/GLOSSARY.md#end-to-end-encryption) messaging via private connections. This encryption cannot be compromised by the relays servers, even if they are modified or compromised, via [man-in-the-middle attack](./docs/GLOSSARY.md#man-in-the-middle-attack), unlike most other communication platforms, services and networks.
|
||||
|
||||
SimpleX Chat is built on top of SimpleX messaging and application platform that uses a new message routing protocol allowing to establish private connections without having any kind of addresses that identify its users - we don't use emails, phone numbers, usernames, identity keys or any other user identifiers to pass messages between the users.
|
||||
SimpleX Chat software is built on top of SimpleX messaging and application protocols, based on a new message routing protocol allowing to establish private connections without having any kind of addresses or other identifiers assigned to its users - it does not use emails, phone numbers, usernames, identity keys or any other user profile identifiers to pass messages between the user applications.
|
||||
|
||||
SimpleX Chat security assessment was done in October 2022 by [Trail of Bits](https://www.trailofbits.com/about), and most fixes were released in v4.2.0 – see [the announcement](./blog/20221108-simplex-chat-v4.2-security-audit-new-website.md).
|
||||
SimpleX Chat software is similar in its design approach to email clients and browsers - it allows you to have full control of your data and freely choose the relay server providers, in the same way you choose which website or email provider to use, or use your own relay servers, simply by changing the configuration of the client software. The only current restriction to that is Apple push notifications - at the moment they can only be delivered via the preset servers that we operate, as explained below. We are exploring the solutions to deliver push notifications to iOS devices via other providers or users' own servers.
|
||||
|
||||
### Information you provide
|
||||
While SimpleX Chat Ltd is not a communication service provider, and provide public preset relays "as is", as experimental, without any guarantees of availability or data retention, we are committed to maintain a high level of availability, reliability and security of these preset relays. We will be adding alternative preset infrastructure providers to the software in the future, and you will continue to be able to use any other providers or your own servers.
|
||||
|
||||
We see users and data sovereignty, and device and provider portability as critically important properties for any communication system.
|
||||
|
||||
SimpleX Chat security assessment was done in October 2022 by [Trail of Bits](https://www.trailofbits.com/about), and most fixes were released in v4.2 – see [the announcement](./blog/20221108-simplex-chat-v4.2-security-audit-new-website.md).
|
||||
|
||||
### Your information
|
||||
|
||||
#### User profiles
|
||||
|
||||
We do not store user profiles. The profile you create in the app is local to your device.
|
||||
Servers used by SimpleX Chat apps do not create, store or identify user profiles. The profiles you can create in the app are local to your device, and can be removed at any time via the app.
|
||||
|
||||
When you create a user profile, no records are created on our servers, and we have no access to any part of your profile information, and even to the fact that you created a profile - it is a local record stored only on your device. That means that if you delete the app, and have no backup, you will permanently lose all the data and the private connections you create with other users.
|
||||
When you create the local profile, no records are created on any of the relay servers, and infrastructure providers, whether SimpleX Chat Ltd or any other, have no access to any part of your information, and even to the fact that you created a profile - it is a local record stored only on your device. That means that if you delete the app, and have no backup, you will permanently lose all your data and the private connections you created with other software users.
|
||||
|
||||
You can transfer the profile to another device by creating a backup of the app data and restoring it on the new device, but you cannot use more than one device with the copy of the same profile at the same time - it will disrupt any active conversations on either or both devices, as a security property of end-to-end encryption.
|
||||
|
||||
#### Messages and Files
|
||||
|
||||
SimpleX Chat cannot decrypt or otherwise access the content or even the size of your messages and files you send or receive. Each message is padded to a fixed size of 16kb. Each file is sent in chunks of 256kb, 1mb or 8mb via all or some of the configured file servers. Both messages and files are sent end-to-end encrypted, and the servers do not have technical means to compromise this encryption, because part of the [key exchange](./docs/GLOSSARY.md#key-exchange) happens out-of-band.
|
||||
SimpleX relay servers cannot decrypt or otherwise access the content or even the size of your messages and files you send or receive. Each message is padded to a fixed size of 16kb. Each file is sent in chunks of 64kb, 256kb, 1mb or 8mb via all or some of the configured file relay servers. Both messages and files are sent end-to-end encrypted, and the servers do not have technical means to compromise this encryption, because part of the [key exchange](./docs/GLOSSARY.md#key-exchange) happens out-of-band.
|
||||
|
||||
Your message history is stored only on your own device and the devices of your contacts. While the recipients' devices are offline SimpleX Chat temporarily stores end-to-end encrypted messages on the messaging (SMP) servers that are preset in the app or chosen by the users.
|
||||
Your message history is stored only on your own device and the devices of your contacts. While the recipients' devices are offline, messaging relay servers temporarily store end-to-end encrypted messages – you can configure which relay servers are used to receive the messages from the new contacts, and you can manually change them for the existing contacts too.
|
||||
|
||||
The messages are permanently removed from the preset servers as soon as they are delivered. Undelivered messages are deleted after the time that is configured in the messaging servers you use (21 days for preset messaging servers).
|
||||
You do not have control over which servers are used to send messages to your contacts - they are chosen by them. To send messages your client needs to connect to these servers, therefore the servers chosen by your contacts can observe your IP address. You can use VPN or some overlay network (e.g., Tor) to hide your IP address from the servers chosen by your contacts. In the near future we will add the layer in the messaging protocol that will route sent message via the relays chosen by you as well.
|
||||
|
||||
The files are stored on file (XFTP) servers for the time configured in the file servers you use (48 hours for preset file servers).
|
||||
The messages are permanently removed from the used relay servers as soon as they are delivered, as long as these servers used unmodified published code. Undelivered messages are deleted after the time that is configured in the messaging servers you use (21 days for preset messaging servers).
|
||||
|
||||
If a messaging or file servers are restarted, the encrypted message or the record of the file can be stored in a backup file until it is overwritten by the next restart (usually within 1 week).
|
||||
The files are stored on file relay servers for the time configured in the relay servers you use (48 hours for preset file servers).
|
||||
|
||||
If a messaging servers are restarted, the encrypted message can be stored in a backup file until it is overwritten by the next restart (usually within 1 week for preset relay servers).
|
||||
|
||||
As this software is fully open-source and provided under AGPLv3 license, all infrastructure providers and owners, and the developers of the client and server applications who use the SimpleX Chat source code, are required to publish any changes to this software under the same AGPLv3 license - including any modifications to the provided servers.
|
||||
|
||||
In addition to the AGPLv3 license terms, SimpleX Chat Ltd is committed to the software users that the preset relays that we provide via the apps will always be compiled from the [published open-source code](https://github.com/simplex-chat/simplexmq), without any modifications.
|
||||
|
||||
#### Connections with other users
|
||||
|
||||
When you create a connection with another user, two messaging queues (you can think about them as about mailboxes) are created on chosen messaging servers, that can be the preset servers or the servers that you configured in the app, in case it allows such configuration. SimpleX uses separate queues for direct and response messages, that the client applications prefer to create on two different servers, in case you have more than one server configured in the app, which is the default.
|
||||
When you create a connection with another user, two messaging queues (you can think about them as mailboxes) are created on messaging relay servers (chosen by you and your contact each), that can be the preset servers or the servers that you and your contact configured in the app. SimpleX messaging protocol uses separate queues for direct and response messages, and the apps prefer to create these queues on two different relay servers for increased privacy, in case you have more than one relay server configured in the app, which is the default.
|
||||
|
||||
At the time of updating this document all our client applications allow configuring the servers. Our servers do not store information about which queues are linked to your profile on the device, and they do not collect any information that would allow us to establish that these queues are related to your device or your profile - the access to each queue is authorized by two anonymous unique cryptographic keys, different for each queue, and separate for sender and recipient of the messages.
|
||||
SimpleX relay servers do not store information about which queues are linked to your profile on the device, and they do not collect any information that would allow infrastructure owners and providers to establish that these queues are related to your device or your profile - the access to each queue is authorized by two anonymous unique cryptographic keys, different for each queue, and separate for sender and recipient of the messages.
|
||||
|
||||
#### Connection links privacy
|
||||
|
||||
When you create a connection with another user, the app generates a link/QR code that can be shared with the user to establish the connection via any channel (email, any other messenger, or a video call). This link is safe to share via insecure channels, as long as you can identify the recipient and also trust that this channel did not replace this link (to mitigate the latter risk you can validate the security code via the app).
|
||||
|
||||
While the connection "links" contain SimpleX Chat Ltd domain name `simplex.chat`, this site is never accessed by the app, and is only used for these purposes:
|
||||
- to direct the new users to the app download instructions,
|
||||
- to show connection QR code that can be scanned via the app,
|
||||
- to "namespace" these links,
|
||||
- to open links directly in the installed app when it is clicked outside of the app.
|
||||
|
||||
You can always safely replace the initial part of the link `https://simplex.chat/` either with `simplex:/` (which is a URI scheme provisionally registered with IANA) or with any other domain name where you can self-host the app download instructions and show the connection QR code (but in case it is your domain, it will not open in the app). Also, while the page renders QR code, all the information needed to render it is only available to the browser, as the part of the "link" after `#` symbol is not sent to the website server.
|
||||
|
||||
#### iOS Push Notifications
|
||||
|
||||
When you choose to use instant push notifications in SimpleX iOS app, because the design of push notifications requires storing the device token on notification server, the notifications server can observe how many messaging queues your device has notifications enabled for, and approximately how many messages are sent to each queue.
|
||||
|
||||
Notification server cannot observe the actual addresses of these queues, as a separate address is used to subscribe to the notifications. It also cannot observe who, or even how many contacts, send messages to you, as notifications are delivered to your device end-to-end encrypted by the messaging servers.
|
||||
Preset notification server cannot observe the actual addresses of these queues, as a separate address is used to subscribe to the notifications. It also cannot observe who sends messages to you. Apple push notifications servers can only observe how many notifications are sent to you, but not from how many contacts, or from which messaging relays, as notifications are delivered to your device end-to-end encrypted by one of the preset notification servers - these notifications only contain end-to-end encrypted metadata, not even encrypted message content, and they look completely random to Apple push notification servers.
|
||||
|
||||
It also does not allow to see message content or sizes, as the actual messages are not sent via the notification server, only the fact that the message is available and where it can be received from (the latter information is encrypted, so that the notification server cannot observe it). You can read more about the design of iOS push notifications [here](https://simplex.chat/blog/20220404-simplex-chat-instant-notifications.html#our-ios-approach-has-one-trade-off).
|
||||
You can read more about the design of iOS push notifications [here](https://simplex.chat/blog/20220404-simplex-chat-instant-notifications.html#our-ios-approach-has-one-trade-off).
|
||||
|
||||
#### Another information stored on the servers
|
||||
|
||||
Additional technical information can be stored on our servers, including randomly generated authentication tokens, keys, push tokens, and other material that is necessary to transmit messages. SimpleX Chat limits this additional technical information to the minimum required to operate the Services.
|
||||
Additional technical information can be stored on our servers, including randomly generated authentication tokens, keys, push tokens, and other material that is necessary to transmit messages. SimpleX Chat design limits this additional technical information to the minimum required to operate the software and servers. To prevent server overloading or attacks, the servers can temporarily store data that can link to particular users or devices, including IP addresses, geographic location, or information related to the transport sessions. This information is not stored for the absolute majority of the app users, even for those who use the servers very actively.
|
||||
|
||||
#### SimpleX Directory Service
|
||||
#### SimpleX Directory
|
||||
|
||||
[SimpleX directory service](./docs/DIRECTORY.md) stores: your search requests, the messages and the members profiles in the group. You can connect to SimpleX Directory Service via [this address](https://simplex.chat/contact#/?v=1-4&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FeXSPwqTkKyDO3px4fLf1wx3MvPdjdLW3%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAaiv6MkMH44L2TcYrt_CsX3ZvM11WgbMEUn0hkIKTOho%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion).
|
||||
[SimpleX Directory](./docs/DIRECTORY.md) stores: your search requests, the messages and the members profiles in the registered groups. You can connect to SimpleX Directory via [this address](https://simplex.chat/contact#/?v=1-4&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FeXSPwqTkKyDO3px4fLf1wx3MvPdjdLW3%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAaiv6MkMH44L2TcYrt_CsX3ZvM11WgbMEUn0hkIKTOho%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion).
|
||||
|
||||
#### User Support.
|
||||
#### User Support
|
||||
|
||||
If you contact SimpleX Chat any personal data you may share with us is kept only for the purposes of researching the issue and contacting you about your case. We recommend contacting support [via chat](https://simplex.chat/contact#/?v=1&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2FK1rslx-m5bpXVIdMZg9NLUZ_8JBm8xTt%23%2F%3Fv%3D1%26dh%3DMCowBQYDK2VuAyEALDeVe-sG8mRY22LsXlPgiwTNs9dbiLrNuA7f3ZMAJ2w%253D%26srv%3Dbylepyau3ty4czmn77q4fglvperknl4bi2eb2fdy2bh4jxtf32kf73yd.onion), when it is possible.
|
||||
If you contact SimpleX Chat Ltd, any personal data you share with us is kept only for the purposes of researching the issue and contacting you about your case. We recommend contacting support [via chat](https://simplex.chat/contact#/?v=1&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2FK1rslx-m5bpXVIdMZg9NLUZ_8JBm8xTt%23%2F%3Fv%3D1%26dh%3DMCowBQYDK2VuAyEALDeVe-sG8mRY22LsXlPgiwTNs9dbiLrNuA7f3ZMAJ2w%253D%26srv%3Dbylepyau3ty4czmn77q4fglvperknl4bi2eb2fdy2bh4jxtf32kf73yd.onion) when it is possible, and avoid sharing any personal information.
|
||||
|
||||
### Information we may share
|
||||
|
||||
We operate our Services using third parties. While we do not share any user data, these third party may access the encrypted user data as it is stored or transmitted via our servers.
|
||||
SimpleX Chat Ltd operates preset relay servers using third parties. While we do not have access and cannot share any user data, these third parties may access the encrypted user messages (but NOT the actual unencrypted message content or size) as it is stored or transmitted via our servers. Hosting providers can also store IP addresses and other transport information as part of their logs.
|
||||
|
||||
We use a third party for email services - if you ask for support via email, your and SimpleX Chat email providers may access these emails according to their privacy policies and terms of service.
|
||||
We use a third party for email services - if you ask for support via email, your and SimpleX Chat Ltd email providers may access these emails according to their privacy policies and terms. When the request is sensitive, we recommend contacting us via SimpleX Chat or using encrypted email using PGP key published at [openpgp.org](https://keys.openpgp.org/search?q=chat%40simplex.chat).
|
||||
|
||||
The cases when SimpleX Chat may need to share the data we temporarily store on the servers:
|
||||
The cases when SimpleX Chat Ltd may share the data temporarily stored on the servers:
|
||||
|
||||
- To meet any applicable law, regulation, legal process or enforceable governmental request.
|
||||
- To enforce applicable Terms, including investigation of potential violations.
|
||||
- To meet any applicable law, or enforceable governmental request or court order.
|
||||
- To enforce applicable terms, including investigation of potential violations.
|
||||
- To detect, prevent, or otherwise address fraud, security, or technical issues.
|
||||
- To protect against harm to the rights, property, or safety of SimpleX Chat, our users, or the public as required or permitted by law.
|
||||
- To protect against harm to the rights, property, or safety of software users, SimpleX Chat Ltd, or the public as required or permitted by law.
|
||||
|
||||
At the time of updating this document, we have never provided or have been requested the access to our servers or any information from our servers by any third parties. If we are ever requested to provide such access or information, we will follow the due legal process.
|
||||
At the time of updating this document, we have never provided or have been requested the access to the preset relay servers or any information from the servers by any third parties. If we are ever requested to provide such access or information, we will follow the due legal process to limit any information shared with the third parties to the minimally required by law.
|
||||
|
||||
### Updates
|
||||
|
||||
We will update this Privacy Policy as needed so that it is current, accurate, and as clear as possible. Your continued use of our Services confirms your acceptance of our updated Privacy Policy.
|
||||
We will update this Privacy Policy as needed so that it is current, accurate, and as clear as possible. Your continued use of our software applications and preset relays infrastructure confirms your acceptance of our updated Privacy Policy.
|
||||
|
||||
Please also read our Terms of Service below.
|
||||
Please also read our Conditions of Use of Software and Infrastructure below.
|
||||
|
||||
If you have questions about our Privacy Policy please contact us via [email](chat@simplex.chat) or [chat](https://simplex.chat/contact#/?v=1&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2FK1rslx-m5bpXVIdMZg9NLUZ_8JBm8xTt%23%2F%3Fv%3D1%26dh%3DMCowBQYDK2VuAyEALDeVe-sG8mRY22LsXlPgiwTNs9dbiLrNuA7f3ZMAJ2w%253D%26srv%3Dbylepyau3ty4czmn77q4fglvperknl4bi2eb2fdy2bh4jxtf32kf73yd.onion).
|
||||
|
||||
## Terms of Service
|
||||
## Conditions of Use of Software and Infrastructure
|
||||
|
||||
You accept our Terms of Service ("Terms") by installing or using any of our apps or services ("Services").
|
||||
You accept the Conditions of Use of Software and Infrastructure ("Conditions") by installing or using any of our software or using any of our server infrastructure (collectively referred to as "Applications"), whether preset in the software or not.
|
||||
|
||||
**Minimal age**. You must be at least 13 years old to use our Services. The minimum age to use our Services without parental approval may be higher in your country.
|
||||
**Minimal age**. You must be at least 13 years old to use our Applications. The minimum age to use our Applications without parental approval may be higher in your country.
|
||||
|
||||
**Accessing the servers**. For the efficiency of the network access, the apps access all queues you create on any server via the same network (TCP/IP) connection. Our servers do not collect information about which queues were accessed via the same connection, so we cannot establish which queues belong to the same users. Whoever might observe your network traffic would know which servers you use, and how much data you send, but not to whom it is sent - the data that leaves the servers is always different from the data they receive - there are no identifiers or ciphertext in common. Please refer to our [technical design document](https://github.com/simplex-chat/simplexmq/blob/master/protocol/overview-tjr.md) for more information about our privacy model and known security and privacy risks.
|
||||
**Infrastructure**. Our Infrastructure includes preset messaging and file relay servers, and iOS push notification servers provided by SimpleX Chat Ltd for public use. Our infrastructure does not have any modifications from the [published open-source code](https://github.com/simplex-chat/simplexmq) available under AGPLv3 license. Any infrastructure provider, whether commercial or not, is required by the Affero clause (named after Affero Inc. company that pioneered the community-based Q&A sites in early 2000s) to publish any modifications under the same license. The statements in relation to Infrastructure and relay servers anywhere in this document assume no modifications to the published code, even in the cases when it is not explicitly stated.
|
||||
|
||||
**Privacy of user data**. We do not retain any data we transmit for any longer than necessary to provide the Services. We only collect aggregate statistics across all users, not per user - we do not have information about how many people use SimpleX Chat (we only know an approximate number of app installations and the aggregate traffic through our servers). In any case, we do not and will not sell or in any way monetize user data.
|
||||
**Client applications**. Our client application Software (referred to as "app" or "apps") also has no modifications compared with published open-source code, and any developers of the alternative client apps based on our code are required to publish any modifications under the same AGPLv3 license. Client applications should not include any tracking or analytics code, and do not share any information with SimpleX Chat Ltd or any other third parties. If you ever discover any tracking or analytics code, please report it to us, so we can remove it.
|
||||
|
||||
**Operating our services**. For the purpose of operating our Services, you agree that your end-to-end encrypted messages are transferred via our servers in the United Kingdom, the United States and other countries where we have or use facilities and service providers or partners.
|
||||
**Accessing the infrastructure**. For the efficiency of the network access, the client Software by default accesses all queues your app creates on any relay server within one user profile via the same network (TCP/IP) connection. At the cost of additional traffic this configuration can be changed to use different transport session for each connection. Relay servers do not collect information about which queues were created or accessed via the same connection, so the relay servers cannot establish which queues belong to the same user profile. Whoever might observe your network traffic would know which relay servers you use, and how much data you send, but not to whom it is sent - the data that leaves the servers is always different from the data they receive - there are no identifiers or ciphertext in common, even inside TLS encryption layer. Please refer to our [technical design document](https://github.com/simplex-chat/simplexmq/blob/master/protocol/overview-tjr.md) for more information about our privacy model and known security and privacy risks.
|
||||
|
||||
**Software**. You agree to downloading and installing updates to our Services when they are available; they would only be automatic if you configure your devices in this way.
|
||||
**Privacy of user data**. Servers do not retain any data we transmit for any longer than necessary to deliver the messages between apps. SimpleX Chat Ltd collects aggregate statistics across all its servers, as supported by published code and can be enabled by any infrastructure provider, but not any statistics per-user, or per geographic location, or per IP address, or per transport session. We do not have information about how many people use SimpleX Chat applications, we only know an approximate number of app installations and the aggregate traffic through the preset servers. In any case, we do not and will not sell or in any way monetize user data. Our future business model assumes charging for some optional Software features instead, in a transparent and fair way.
|
||||
|
||||
**Traffic and device costs**. You are solely responsible for the traffic and device costs on which you use our Services, and any associated taxes.
|
||||
**Operating our Infrastructure**. For the purpose of using our Software, if you continue using preset servers, you agree that your end-to-end encrypted messages are transferred via the preset servers in any countries where we have or use facilities and service providers or partners. The information about geographic location of the servers will be made available in the apps in the near future.
|
||||
|
||||
**Legal and acceptable usage**. You agree to use our Services only for legal and acceptable purposes. You will not use (or assist others in using) our Services in ways that: 1) violate or infringe the rights of SimpleX Chat, our users, or others, including privacy, publicity, intellectual property, or other proprietary rights; 2) involve sending illegal or impermissible communications, e.g. spam.
|
||||
**Software**. You agree to downloading and installing updates to our Applications when they are available; they would only be automatic if you configure your devices in this way.
|
||||
|
||||
**Damage to SimpleX Chat**. You must not (or assist others to) access, use, modify, distribute, transfer, or exploit our Services in unauthorized manners, or in ways that harm SimpleX Chat, our Services, or systems. For example, you must not 1) access our Services or systems without authorization, other than by using the apps; 2) disrupt the integrity or performance of our Services; 3) collect information about our users in any manner; or 4) sell, rent, or charge for our Services.
|
||||
**Traffic and device costs**. You are solely responsible for the traffic and device costs that you incur while using our Applications, and any associated taxes.
|
||||
|
||||
**Keeping your data secure**. SimpleX Chat is the first messaging platform that is 100% private by design - we neither have ability to access your messages, nor we have information about who you communicate with. That means that you are solely responsible for keeping your device and your user profile safe and secure. If you lose your phone or remove the app, you will not be able to recover the lost data, unless you made a back up.
|
||||
**Legal and acceptable usage**. You agree to use our Applications only for legal and acceptable purposes. You will not use (or assist others in using) our Applications in ways that: 1) violate or infringe the rights of Software users, SimpleX Chat Ltd, or others, including privacy, publicity, intellectual property, or other proprietary rights; 2) involve sending illegal or impermissible communications, e.g. spam. While we cannot access content or identify messages or groups, in some cases the links to the illegal or impermissible communications available via our Applications can be shared publicly on social media or websites. We reserve the right to remove such links from the preset servers and disrupt the conversations that send illegal content via our servers, whether they were reported by the users or discovered by our team.
|
||||
|
||||
**Storing the messages on the device**. The messages are stored in the encrypted database on your device. Whether and how database passphrase is stored is determined by the configuration of the application you use. Legacy databases created prior to 2023 or in CLI (terminal) app may remain unencrypted, and it will be indicated in the app. In this case, if you make a backup of the app data and store it unencrypted, the backup provider may be able to access the messages. Please note, that the beta version of desktop app currently stores the database passphrase in the configuration file in plaintext, so you may need to remove passphrase from the device via the app configuration.
|
||||
**Damage to SimpleX Chat Ltd**. You must not (or assist others to) access, use, modify, distribute, transfer, or exploit our Applications in unauthorized manners, or in ways that harm Software users, SimpleX Chat Ltd, our Infrastructure, or any other systems. For example, you must not 1) access our Infrastructure or systems without authorization, in any way other than by using the Software; 2) disrupt the integrity or performance of our Infrastructure; 3) collect information about our users in any manner; or 4) sell, rent, or charge for our Infrastructure. This does not prohibit you from providing your own Infrastructure to others, whether free or for a fee, as long as you do not violate these Conditions and AGPLv3 license, including the requirement to publish any modifications of the relay server software.
|
||||
|
||||
**Storing the files on the device**. The files are stored on your device unencrypted. If you make a backup of the app data and store it unencrypted, the backup provider will be able to access the files.
|
||||
**Keeping your data secure**. SimpleX Chat is the first communication software that aims to be 100% private by design - server software neither has the ability to access your messages, nor it has information about who you communicate with. That means that you are solely responsible for keeping your device, your user profile and any data safe and secure. If you lose your phone or remove the Software from the device, you will not be able to recover the lost data, unless you made a back up. To protect the data you need to make regular backups, as using old backups may disrupt your communication with some of the contacts.
|
||||
|
||||
**No Access to Emergency Services**. Our Services do not provide access to emergency service providers like the police, fire department, hospitals, or other public safety organizations. Make sure you can contact emergency service providers through a mobile, fixed-line telephone, or other service.
|
||||
**Storing the messages on the device**. The messages are stored in the encrypted database on your device. Whether and how database passphrase is stored is determined by the configuration of the Software you use. The databases created prior to 2023 or in CLI (terminal) app may remain unencrypted, and it will be indicated in the app interface. In this case, if you make a backup of the data and store it unencrypted, the backup provider may be able to access the messages. Please note, that the desktop apps can be configured to store the database passphrase in the configuration file in plaintext, and unless you set the passphrase when first running the app, a random passphrase will be used and stored on the device. You can remove it from the device via the app settings.
|
||||
|
||||
**Third-party services**. Our Services may allow you to access, use, or interact with third-party websites, apps, content, and other products and services. When you use third-party services, their terms and privacy policies govern your use of those services.
|
||||
**Storing the files on the device**. The files currently sent and received in the apps by default (except CLI app) are stored on your device encrypted using unique keys, different for each file, that are stored in the database. Once the message that the file was attached to is removed, even if the copy of the encrypted file is retained, it should be impossible to recover the key allowing to decrypt the file. This local file encryption may affect app performance, and it can be disabled via the app settings. This change will only affect the new files. If you later re-enable the encryption, it will also affect only the new files. If you make a backup of the app data and store it unencrypted, the backup provider will be able to access any unencrypted files. In any case, irrespective of the storage setting, the files are always sent by all apps end-to-end encrypted.
|
||||
|
||||
**Your Rights**. You own the messages and the information you transmit through our Services. Your recipients are able to retain the messages you receive from you; there is no technical ability to delete data from their devices. While there are various app features that allow deleting messages from the recipients' devices, such as _disappearing messages_ and _full message deletion_, their functioning on your recipients' devices cannot be guaranteed or enforced, as the device may be offline or have a modified version of the app.
|
||||
**No Access to Emergency Services**. Our Applications do not provide access to emergency service providers like the police, fire department, hospitals, or other public safety organizations. Make sure you can contact emergency service providers through a mobile, fixed-line telephone, or other service.
|
||||
|
||||
**License**. SimpleX Chat grants you a limited, revocable, non-exclusive, and non-transferable license to use our Services in accordance with these Terms. The source-code of services is available and can be used under [AGPL v3 license](https://github.com/simplex-chat/simplex-chat/blob/stable/LICENSE)
|
||||
**Third-party services**. Our Applications may allow you to access, use, or interact with our or third-party websites, apps, content, and other products and services. When you use third-party services, their terms and privacy policies govern your use of those services.
|
||||
|
||||
**SimpleX Chat Rights**. We own all copyrights, trademarks, domains, logos, trade secrets, and other intellectual property rights associated with our Services. You may not use our copyrights, trademarks, domains, logos, and other intellectual property rights unless you have our written permission, and unless under an open-source license distributed together with the source code. To report copyright, trademark, or other intellectual property infringement, please contact chat@simplex.chat.
|
||||
**Your Rights**. You own the messages and the information you transmit through our Applications. Your recipients are able to retain the messages they receive from you; there is no technical ability to delete data from their devices. While there are various app features that allow deleting messages from the recipients' devices, such as _disappearing messages_ and _full message deletion_, their functioning on your recipients' devices cannot be guaranteed or enforced, as the device may be offline or have a modified version of the Software. At the same time, repudiation property of the end-to-end encryption algorithm allows you to plausibly deny having sent the message, like you can deny what you said in a private face-to-face conversation, as the recipient cannot provide any proof to the third parties, by design.
|
||||
|
||||
**Disclaimers**. YOU USE OUR SERVICES AT YOUR OWN RISK AND SUBJECT TO THE FOLLOWING DISCLAIMERS. WE PROVIDE OUR SERVICES ON AN “AS IS” BASIS WITHOUT ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, TITLE, NON-INFRINGEMENT, AND FREEDOM FROM COMPUTER VIRUS OR OTHER HARMFUL CODE. SIMPLEX DOES NOT WARRANT THAT ANY INFORMATION PROVIDED BY US IS ACCURATE, COMPLETE, OR USEFUL, THAT OUR SERVICES WILL BE OPERATIONAL, ERROR-FREE, SECURE, OR SAFE, OR THAT OUR SERVICES WILL FUNCTION WITHOUT DISRUPTIONS, DELAYS, OR IMPERFECTIONS. WE DO NOT CONTROL, AND ARE NOT RESPONSIBLE FOR, CONTROLLING HOW OR WHEN OUR USERS USE OUR SERVICES. WE ARE NOT RESPONSIBLE FOR THE ACTIONS OR INFORMATION (INCLUDING CONTENT) OF OUR USERS OR OTHER THIRD PARTIES. YOU RELEASE US, AFFILIATES, DIRECTORS, OFFICERS, EMPLOYEES, PARTNERS, AND AGENTS ("SIMPLEX PARTIES") FROM ANY CLAIM, COMPLAINT, CAUSE OF ACTION, CONTROVERSY, OR DISPUTE (TOGETHER, "CLAIM") AND DAMAGES, KNOWN AND UNKNOWN, RELATING TO, ARISING OUT OF, OR IN ANY WAY CONNECTED WITH ANY SUCH CLAIM YOU HAVE AGAINST ANY THIRD PARTIES.
|
||||
**License**. SimpleX Chat Ltd grants you a limited, revocable, non-exclusive, and non-transferable license to use our Applications in accordance with these Conditions. The source-code of Applications is available and can be used under [AGPL v3 license](https://github.com/simplex-chat/simplex-chat/blob/stable/LICENSE).
|
||||
|
||||
**Limitation of liability**. THE SIMPLEX PARTIES WILL NOT BE LIABLE TO YOU FOR ANY LOST PROFITS OR CONSEQUENTIAL, SPECIAL, PUNITIVE, INDIRECT, OR INCIDENTAL DAMAGES RELATING TO, ARISING OUT OF, OR IN ANY WAY IN CONNECTION WITH OUR TERMS, US, OR OUR SERVICES, EVEN IF THE SIMPLEX PARTIES HAVE BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. OUR AGGREGATE LIABILITY RELATING TO, ARISING OUT OF, OR IN ANY WAY IN CONNECTION WITH OUR TERMS, US, OR OUR SERVICES WILL NOT EXCEED ONE DOLLAR ($1). THE FOREGOING DISCLAIMER OF CERTAIN DAMAGES AND LIMITATION OF LIABILITY WILL APPLY TO THE MAXIMUM EXTENT PERMITTED BY APPLICABLE LAW. THE LAWS OF SOME JURISDICTIONS MAY NOT ALLOW THE EXCLUSION OR LIMITATION OF CERTAIN DAMAGES, SO SOME OR ALL OF THE EXCLUSIONS AND LIMITATIONS SET FORTH ABOVE MAY NOT APPLY TO YOU. NOTWITHSTANDING ANYTHING TO THE CONTRARY IN OUR TERMS, IN SUCH CASES, THE LIABILITY OF THE SIMPLEX PARTIES WILL BE LIMITED TO THE EXTENT PERMITTED BY APPLICABLE LAW.
|
||||
**SimpleX Chat Ltd Rights**. We own all copyrights, trademarks, domains, logos, trade secrets, and other intellectual property rights associated with our Applications. You may not use our copyrights, trademarks, domains, logos, and other intellectual property rights unless you have our written permission, and unless under an open-source license distributed together with the source code. To report copyright, trademark, or other intellectual property infringement, please contact chat@simplex.chat.
|
||||
|
||||
**Availability**. Our Services may be interrupted, including for maintenance, upgrades, or network or equipment failures. We may discontinue some or all of our Services, including certain features and the support for certain devices and platforms, at any time.
|
||||
**Disclaimers**. YOU USE OUR APPLICATIONS AT YOUR OWN RISK AND SUBJECT TO THE FOLLOWING DISCLAIMERS. WE PROVIDE OUR APPLICATIONS ON AN “AS IS” BASIS WITHOUT ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, TITLE, NON-INFRINGEMENT, AND FREEDOM FROM COMPUTER VIRUS OR OTHER HARMFUL CODE. SIMPLEX CHAT LTD DOES NOT WARRANT THAT ANY INFORMATION PROVIDED BY US IS ACCURATE, COMPLETE, OR USEFUL, THAT OUR APPLICATIONS WILL BE OPERATIONAL, ERROR-FREE, SECURE, OR SAFE, OR THAT OUR APPLICATIONS WILL FUNCTION WITHOUT DISRUPTIONS, DELAYS, OR IMPERFECTIONS. WE DO NOT CONTROL, AND ARE NOT RESPONSIBLE FOR, CONTROLLING HOW OR WHEN OUR USERS USE OUR APPLICATIONS. WE ARE NOT RESPONSIBLE FOR THE ACTIONS OR INFORMATION (INCLUDING CONTENT) OF OUR USERS OR OTHER THIRD PARTIES. YOU RELEASE US, AFFILIATES, DIRECTORS, OFFICERS, EMPLOYEES, PARTNERS, AND AGENTS ("SIMPLEX PARTIES") FROM ANY CLAIM, COMPLAINT, CAUSE OF ACTION, CONTROVERSY, OR DISPUTE (TOGETHER, "CLAIM") AND DAMAGES, KNOWN AND UNKNOWN, RELATING TO, ARISING OUT OF, OR IN ANY WAY CONNECTED WITH ANY SUCH CLAIM YOU HAVE AGAINST ANY THIRD PARTIES.
|
||||
|
||||
**Resolving disputes**. You agree to resolve any Claim you have with us relating to or arising from our Terms, us, or our Services in the courts of England and Wales. You also agree to submit to the personal jurisdiction of such courts for the purpose of resolving all such disputes. The laws of England govern our Terms, as well as any disputes, whether in court or arbitration, which might arise between SimpleX Chat and you, without regard to conflict of law provisions.
|
||||
**Limitation of liability**. THE SIMPLEX PARTIES WILL NOT BE LIABLE TO YOU FOR ANY LOST PROFITS OR CONSEQUENTIAL, SPECIAL, PUNITIVE, INDIRECT, OR INCIDENTAL DAMAGES RELATING TO, ARISING OUT OF, OR IN ANY WAY IN CONNECTION WITH OUR CONDITIONS, US, OR OUR APPLICATIONS, EVEN IF THE SIMPLEX PARTIES HAVE BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. OUR AGGREGATE LIABILITY RELATING TO, ARISING OUT OF, OR IN ANY WAY IN CONNECTION WITH OUR CONDITIONS, US, OR OUR APPLICATIONS WILL NOT EXCEED ONE DOLLAR ($1). THE FOREGOING DISCLAIMER OF CERTAIN DAMAGES AND LIMITATION OF LIABILITY WILL APPLY TO THE MAXIMUM EXTENT PERMITTED BY APPLICABLE LAW. THE LAWS OF SOME JURISDICTIONS MAY NOT ALLOW THE EXCLUSION OR LIMITATION OF CERTAIN DAMAGES, SO SOME OR ALL OF THE EXCLUSIONS AND LIMITATIONS SET FORTH ABOVE MAY NOT APPLY TO YOU. NOTWITHSTANDING ANYTHING TO THE CONTRARY IN OUR CONDITIONS, IN SUCH CASES, THE LIABILITY OF THE SIMPLEX PARTIES WILL BE LIMITED TO THE EXTENT PERMITTED BY APPLICABLE LAW.
|
||||
|
||||
**Changes to the terms**. SimpleX Chat may update the Terms from time to time. Your continued use of our Services confirms your acceptance of our updated Terms and supersedes any prior Terms. You will comply with all applicable export control and trade sanctions laws. Our Terms cover the entire agreement between you and SimpleX Chat regarding our Services. If you do not agree with our Terms, you should stop using our Services.
|
||||
**Availability**. Our Applications may be disrupted, including for maintenance, upgrades, or network or equipment failures. We may discontinue some or all of our Applications, including certain features and the support for certain devices and platforms, at any time.
|
||||
|
||||
**Enforcing the terms**. If we fail to enforce any of our Terms, that does not mean we waive the right to enforce them. If any provision of the Terms is deemed unlawful, void, or unenforceable, that provision shall be deemed severable from our Terms and shall not affect the enforceability of the remaining provisions. Our Services are not intended for distribution to or use in any country where such distribution or use would violate local law or would subject us to any regulations in another country. We reserve the right to limit our Services in any country. If you have specific questions about these Terms, please contact us at chat@simplex.chat.
|
||||
**Resolving disputes**. You agree to resolve any Claim you have with us relating to or arising from our Conditions, us, or our Applications in the courts of England and Wales. You also agree to submit to the personal jurisdiction of such courts for the purpose of resolving all such disputes. The laws of England govern our Conditions, as well as any disputes, whether in court or arbitration, which might arise between SimpleX Chat Ltd and you, without regard to conflict of law provisions.
|
||||
|
||||
**Ending these Terms**. You may end these Terms with SimpleX Chat at any time by deleting SimpleX Chat app(s) from your device and discontinuing use of our Services. The provisions related to Licenses, Disclaimers, Limitation of Liability, Resolving dispute, Availability, Changes to the terms, Enforcing the terms, and Ending these Terms will survive termination of your relationship with SimpleX Chat.
|
||||
**Changes to the conditions**. SimpleX Chat Ltd may update the Conditions from time to time. Your continued use of our Applications confirms your acceptance of our updated Conditions and supersedes any prior Conditions. You will comply with all applicable export control and trade sanctions laws. Our Conditions cover the entire agreement between you and SimpleX Chat Ltd regarding our Applications. If you do not agree with our Conditions, you should stop using our Applications.
|
||||
|
||||
Updated August 17, 2023
|
||||
**Enforcing the conditions**. If we fail to enforce any of our Conditions, that does not mean we waive the right to enforce them. If any provision of the Conditions is deemed unlawful, void, or unenforceable, that provision shall be deemed severable from our Conditions and shall not affect the enforceability of the remaining provisions. Our Applications are not intended for distribution to or use in any country where such distribution or use would violate local law or would subject us to any regulations in another country. We reserve the right to limit our Applications in any country. If you have specific questions about these Conditions, please contact us at chat@simplex.chat.
|
||||
|
||||
**Ending these conditions**. You may end these Conditions with SimpleX Chat Ltd at any time by deleting our Applications from your devices and discontinuing use of our Infrastructure. The provisions related to Licenses, Disclaimers, Limitation of Liability, Resolving dispute, Availability, Changes to the conditions, Enforcing the conditions, and Ending these conditions will survive termination of your relationship with SimpleX Chat Ltd.
|
||||
|
||||
Updated February 24, 2024
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
[](https://www.reddit.com/r/SimpleXChat)
|
||||
<a rel="me" href="https://mastodon.social/@simplex"></a>
|
||||
|
||||
| 30/03/2023 | EN, [FR](/docs/lang/fr/README.md), [CZ](/docs/lang/cs/README.md) |
|
||||
| 30/03/2023 | EN, [FR](/docs/lang/fr/README.md), [CZ](/docs/lang/cs/README.md), [PL](/docs/lang/pl/README.md) |
|
||||
|
||||
<img src="images/simplex-chat-logo.svg" alt="SimpleX logo" width="100%">
|
||||
|
||||
@@ -234,6 +234,12 @@ You can use SimpleX with your own servers and still communicate with people usin
|
||||
|
||||
Recent and important updates:
|
||||
|
||||
[Mar 23, 2024. SimpleX network: real privacy and stable profits, non-profits for protocols, v5.6 released with quantum resistant e2e encryption and simple profile migration.](./blog/20240323-simplex-network-privacy-non-profit-v5-6-quantum-resistant-e2e-encryption-simple-migration.md)
|
||||
|
||||
[Mar 14, 2024. SimpleX Chat v5.6 beta: adding quantum resistance to Signal double ratchet algorithm.](./blog/20240314-simplex-chat-v5-6-quantum-resistance-signal-double-ratchet-algorithm.md)
|
||||
|
||||
[Jan 24, 2024. SimpleX Chat: free infrastructure from Linode, v5.5 released with private notes, group history and a simpler UX to connect.](./blog/20240124-simplex-chat-infrastructure-costs-v5-5-simplex-ux-private-notes-group-history.md)
|
||||
|
||||
[Nov 25, 2023. SimpleX Chat v5.4 released: link mobile and desktop apps via quantum resistant protocol, and much better groups](./blog/20231125-simplex-chat-v5-4-link-mobile-desktop-quantum-resistant-better-groups.md).
|
||||
|
||||
[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).
|
||||
@@ -299,7 +305,7 @@ What is already implemented:
|
||||
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).
|
||||
14. Local files encryption.
|
||||
|
||||
We plan to add:
|
||||
|
||||
@@ -371,12 +377,14 @@ Please also join [#simplex-devs](https://simplex.chat/contact#/?v=1-2&smp=smp%3A
|
||||
- ✅ Desktop client.
|
||||
- ✅ Encryption of local files stored in the app.
|
||||
- ✅ Using mobile profiles from the desktop app.
|
||||
- ✅ Private notes.
|
||||
- ✅ Improve sending videos (including encryption of locally stored videos).
|
||||
- ✅ Post-quantum resistant key exchange in double ratchet protocol.
|
||||
- 🏗 Improve stability and reduce battery usage.
|
||||
- 🏗 Improve experience for the new users.
|
||||
- 🏗 Post-quantum resistant key exchange in double ratchet protocol.
|
||||
- 🏗 Large groups, communities and public channels.
|
||||
- Message delivery relay for senders (to conceal IP address from the recipients' servers and to reduce the traffic).
|
||||
- 🏗 Message delivery relay for senders (to conceal IP address from the recipients' servers and to reduce the traffic).
|
||||
- Privacy & security slider - a simple way to set all settings at once.
|
||||
- Improve sending videos (including encryption of locally stored videos).
|
||||
- SMP queue redundancy and rotation (manual is supported).
|
||||
- Include optional message into connection request sent via contact address.
|
||||
- Improved navigation and search in the conversation (expand and scroll to quoted message, scroll to search results, etc.).
|
||||
|
||||
@@ -16,6 +16,7 @@ class AppDelegate: NSObject, UIApplicationDelegate {
|
||||
application.registerForRemoteNotifications()
|
||||
if #available(iOS 17.0, *) { trackKeyboard() }
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(pasteboardChanged), name: UIPasteboard.changedNotification, object: nil)
|
||||
removePasscodesIfReinstalled()
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -127,6 +128,19 @@ class AppDelegate: NSObject, UIApplicationDelegate {
|
||||
BGManager.shared.receiveMessages(complete)
|
||||
}
|
||||
|
||||
private func removePasscodesIfReinstalled() {
|
||||
// Check for the database existence, because app and self destruct passcodes
|
||||
// will be saved and restored by iOS when a user deletes and re-installs the app.
|
||||
// In this case the database and settings will be deleted, but the passcodes won't be.
|
||||
// Deleting passcodes ensures that the user will not get stuck on "Opening app..." screen.
|
||||
if (kcAppPassword.get() != nil || kcSelfDestructPassword.get() != nil) &&
|
||||
!UserDefaults.standard.bool(forKey: DEFAULT_PERFORM_LA) && !hasDatabase() {
|
||||
_ = kcAppPassword.remove()
|
||||
_ = kcSelfDestructPassword.remove()
|
||||
_ = kcDatabasePassword.remove()
|
||||
}
|
||||
}
|
||||
|
||||
static func keepScreenOn(_ on: Bool) {
|
||||
UIApplication.shared.isIdleTimerDisabled = on
|
||||
}
|
||||
|
||||
@@ -34,6 +34,8 @@ struct ContentView: View {
|
||||
@State private var waitingForOrPassedAuth = true
|
||||
@State private var chatListActionSheet: ChatListActionSheet? = nil
|
||||
|
||||
private let callTopPadding: CGFloat = 50
|
||||
|
||||
private enum ChatListActionSheet: Identifiable {
|
||||
case planAndConnectSheet(sheet: PlanAndConnectActionSheet)
|
||||
|
||||
@@ -50,16 +52,28 @@ struct ContentView: View {
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
let showCallArea = chatModel.activeCall != nil && chatModel.activeCall?.callState != .waitCapabilities && chatModel.activeCall?.callState != .invitationAccepted
|
||||
// contentView() has to be in a single branch, so that enabling authentication doesn't trigger re-rendering and close settings.
|
||||
// i.e. with separate branches like this settings are closed: `if prefPerformLA { ... contentView() ... } else { contentView() }
|
||||
if !prefPerformLA || accessAuthenticated {
|
||||
contentView()
|
||||
.padding(.top, showCallArea ? callTopPadding : 0)
|
||||
} else {
|
||||
lockButton()
|
||||
.padding(.top, showCallArea ? callTopPadding : 0)
|
||||
}
|
||||
|
||||
if showCallArea, let call = chatModel.activeCall {
|
||||
VStack {
|
||||
activeCallInteractiveArea(call)
|
||||
Spacer()
|
||||
}
|
||||
}
|
||||
|
||||
if chatModel.showCallView, let call = chatModel.activeCall {
|
||||
callView(call)
|
||||
}
|
||||
|
||||
if !showSettings, let la = chatModel.laRequest {
|
||||
LocalAuthView(authRequest: la)
|
||||
.onDisappear {
|
||||
@@ -135,11 +149,11 @@ struct ContentView: View {
|
||||
if case .onboardingComplete = step,
|
||||
chatModel.currentUser != nil {
|
||||
mainView()
|
||||
.actionSheet(item: $chatListActionSheet) { sheet in
|
||||
switch sheet {
|
||||
case let .planAndConnectSheet(sheet): return planAndConnectActionSheet(sheet, dismiss: false)
|
||||
.actionSheet(item: $chatListActionSheet) { sheet in
|
||||
switch sheet {
|
||||
case let .planAndConnectSheet(sheet): return planAndConnectActionSheet(sheet, dismiss: false)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
OnboardingView(onboarding: step)
|
||||
}
|
||||
@@ -163,6 +177,40 @@ struct ContentView: View {
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder private func activeCallInteractiveArea(_ call: Call) -> some View {
|
||||
HStack {
|
||||
Text(call.contact.displayName).font(.body).foregroundColor(.white)
|
||||
Spacer()
|
||||
CallDuration(call: call)
|
||||
}
|
||||
.padding(.horizontal)
|
||||
.frame(height: callTopPadding - 10)
|
||||
.background(Color(uiColor: UIColor(red: 47/255, green: 208/255, blue: 88/255, alpha: 1)))
|
||||
.onTapGesture {
|
||||
chatModel.activeCallViewIsCollapsed = false
|
||||
}
|
||||
}
|
||||
|
||||
struct CallDuration: View {
|
||||
let call: Call
|
||||
@State var text: String = ""
|
||||
@State var timer: Timer? = nil
|
||||
|
||||
var body: some View {
|
||||
Text(text).frame(minWidth: text.count <= 5 ? 52 : 77, alignment: .leading).offset(x: 4).font(.body).foregroundColor(.white)
|
||||
.onAppear {
|
||||
timer = Timer.scheduledTimer(withTimeInterval: 0.3, repeats: true) { timer in
|
||||
if let connectedAt = call.connectedAt {
|
||||
text = durationText(Int(Date.now.timeIntervalSince1970 - connectedAt.timeIntervalSince1970))
|
||||
}
|
||||
}
|
||||
}
|
||||
.onDisappear {
|
||||
_ = timer?.invalidate()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func lockButton() -> some View {
|
||||
Button(action: authenticateContentViewAccess) { Label("Unlock", systemImage: "lock") }
|
||||
}
|
||||
|
||||
@@ -80,6 +80,7 @@ final class ChatModel: ObservableObject {
|
||||
@Published var tokenRegistered = false
|
||||
@Published var tokenStatus: NtfTknStatus?
|
||||
@Published var notificationMode = NotificationsMode.off
|
||||
@Published var notificationServer: String?
|
||||
@Published var notificationPreview: NotificationPreviewMode = ntfPreviewModeGroupDefault.get()
|
||||
// pending notification actions
|
||||
@Published var ntfContactRequest: NTFContactRequest?
|
||||
@@ -89,10 +90,12 @@ final class ChatModel: ObservableObject {
|
||||
@Published var activeCall: Call?
|
||||
let callCommand: WebRTCCommandProcessor = WebRTCCommandProcessor()
|
||||
@Published var showCallView = false
|
||||
@Published var activeCallViewIsCollapsed = false
|
||||
// remote desktop
|
||||
@Published var remoteCtrlSession: RemoteCtrlSession?
|
||||
// currently showing invitation
|
||||
@Published var showingInvitation: ShowingInvitation?
|
||||
@Published var migrationState: MigrationToState? = MigrationToDeviceState.makeMigrationState()
|
||||
// audio recording and playback
|
||||
@Published var stopPreviousRecPlay: URL? = nil // coordinates currently playing source
|
||||
@Published var draft: ComposeState?
|
||||
@@ -100,6 +103,7 @@ final class ChatModel: ObservableObject {
|
||||
// tracks keyboard height via subscription in AppDelegate
|
||||
@Published var keyboardHeight: CGFloat = 0
|
||||
@Published var pasteboardHasStrings: Bool = UIPasteboard.general.hasStrings
|
||||
@Published var networkInfo = UserNetworkInfo(networkType: .other, online: true)
|
||||
|
||||
var messageDelivery: Dictionary<Int64, () -> Void> = [:]
|
||||
|
||||
@@ -775,6 +779,24 @@ final class Chat: ObservableObject, Identifiable {
|
||||
|
||||
var viewId: String { get { "\(chatInfo.id) \(created.timeIntervalSince1970)" } }
|
||||
|
||||
func groupFeatureEnabled(_ feature: GroupFeature) -> Bool {
|
||||
if case let .group(groupInfo) = self.chatInfo {
|
||||
let p = groupInfo.fullGroupPreferences
|
||||
return switch feature {
|
||||
case .timedMessages: p.timedMessages.on
|
||||
case .directMessages: p.directMessages.on(for: groupInfo.membership)
|
||||
case .fullDelete: p.fullDelete.on
|
||||
case .reactions: p.reactions.on
|
||||
case .voice: p.voice.on(for: groupInfo.membership)
|
||||
case .files: p.files.on(for: groupInfo.membership)
|
||||
case .simplexLinks: p.simplexLinks.on(for: groupInfo.membership)
|
||||
case .history: p.history.on
|
||||
}
|
||||
} else {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
public static var sampleData: Chat = Chat(chatInfo: ChatInfo.sampleData.direct, chatItems: [])
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
//
|
||||
// NetworkObserver.swift
|
||||
// SimpleX (iOS)
|
||||
//
|
||||
// Created by Avently on 05.04.2024.
|
||||
// Copyright © 2024 SimpleX Chat. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Network
|
||||
import SimpleXChat
|
||||
|
||||
class NetworkObserver {
|
||||
static let shared = NetworkObserver()
|
||||
private let queue: DispatchQueue = DispatchQueue(label: "chat.simplex.app.NetworkObserver")
|
||||
private var prevInfo: UserNetworkInfo? = nil
|
||||
private var monitor: NWPathMonitor?
|
||||
private let monitorLock: DispatchQueue = DispatchQueue(label: "chat.simplex.app.monitorLock")
|
||||
|
||||
func restartMonitor() {
|
||||
monitorLock.sync {
|
||||
monitor?.cancel()
|
||||
let mon = NWPathMonitor()
|
||||
mon.pathUpdateHandler = { [weak self] path in
|
||||
self?.networkPathChanged(path: path)
|
||||
}
|
||||
mon.start(queue: queue)
|
||||
monitor = mon
|
||||
}
|
||||
}
|
||||
|
||||
private func networkPathChanged(path: NWPath) {
|
||||
let info = UserNetworkInfo(
|
||||
networkType: networkTypeFromPath(path),
|
||||
online: path.status == .satisfied
|
||||
)
|
||||
if (prevInfo != info) {
|
||||
prevInfo = info
|
||||
setNetworkInfo(info)
|
||||
}
|
||||
}
|
||||
|
||||
private func networkTypeFromPath(_ path: NWPath) -> UserNetworkType {
|
||||
if path.usesInterfaceType(.wiredEthernet) {
|
||||
.ethernet
|
||||
} else if path.usesInterfaceType(.wifi) {
|
||||
.wifi
|
||||
} else if path.usesInterfaceType(.cellular) {
|
||||
.cellular
|
||||
} else if path.usesInterfaceType(.other) {
|
||||
.other
|
||||
} else {
|
||||
.none
|
||||
}
|
||||
}
|
||||
|
||||
private static var networkObserver: NetworkObserver? = nil
|
||||
|
||||
private func setNetworkInfo(_ info: UserNetworkInfo) {
|
||||
logger.debug("setNetworkInfo Network changed: \(String(describing: info))")
|
||||
DispatchQueue.main.sync {
|
||||
ChatModel.shared.networkInfo = info
|
||||
}
|
||||
if !hasChatCtrl() { return }
|
||||
self.monitorLock.sync {
|
||||
do {
|
||||
try apiSetNetworkInfo(info)
|
||||
} catch let err {
|
||||
logger.error("setNetworkInfo error: \(responseError(err))")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -21,6 +21,8 @@ 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)
|
||||
|
||||
private let networkStatusesLock = DispatchQueue(label: "chat.simplex.app.network-statuses.lock")
|
||||
|
||||
enum TerminalItem: Identifiable {
|
||||
case cmd(Date, ChatCommand)
|
||||
case resp(Date, ChatResponse)
|
||||
@@ -90,12 +92,12 @@ private func withBGTask<T>(bgDelay: Double? = nil, f: @escaping () -> T) -> T {
|
||||
return r
|
||||
}
|
||||
|
||||
func chatSendCmdSync(_ cmd: ChatCommand, bgTask: Bool = true, bgDelay: Double? = nil) -> ChatResponse {
|
||||
func chatSendCmdSync(_ cmd: ChatCommand, bgTask: Bool = true, bgDelay: Double? = nil, _ ctrl: chat_ctrl? = nil) -> ChatResponse {
|
||||
logger.debug("chatSendCmd \(cmd.cmdType)")
|
||||
let start = Date.now
|
||||
let resp = bgTask
|
||||
? withBGTask(bgDelay: bgDelay) { sendSimpleXCmd(cmd) }
|
||||
: sendSimpleXCmd(cmd)
|
||||
? withBGTask(bgDelay: bgDelay) { sendSimpleXCmd(cmd, ctrl) }
|
||||
: sendSimpleXCmd(cmd, ctrl)
|
||||
logger.debug("chatSendCmd \(cmd.cmdType): \(resp.responseType)")
|
||||
if case let .response(_, json) = resp {
|
||||
logger.debug("chatSendCmd \(cmd.cmdType) response: \(json)")
|
||||
@@ -106,24 +108,24 @@ func chatSendCmdSync(_ cmd: ChatCommand, bgTask: Bool = true, bgDelay: Double? =
|
||||
return resp
|
||||
}
|
||||
|
||||
func chatSendCmd(_ cmd: ChatCommand, bgTask: Bool = true, bgDelay: Double? = nil) async -> ChatResponse {
|
||||
func chatSendCmd(_ cmd: ChatCommand, bgTask: Bool = true, bgDelay: Double? = nil, _ ctrl: chat_ctrl? = nil) async -> ChatResponse {
|
||||
await withCheckedContinuation { cont in
|
||||
cont.resume(returning: chatSendCmdSync(cmd, bgTask: bgTask, bgDelay: bgDelay))
|
||||
cont.resume(returning: chatSendCmdSync(cmd, bgTask: bgTask, bgDelay: bgDelay, ctrl))
|
||||
}
|
||||
}
|
||||
|
||||
func chatRecvMsg() async -> ChatResponse? {
|
||||
func chatRecvMsg(_ ctrl: chat_ctrl? = nil) async -> ChatResponse? {
|
||||
await withCheckedContinuation { cont in
|
||||
_ = withBGTask(bgDelay: msgDelay) { () -> ChatResponse? in
|
||||
let resp = recvSimpleXMsg()
|
||||
let resp = recvSimpleXMsg(ctrl)
|
||||
cont.resume(returning: resp)
|
||||
return resp
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func apiGetActiveUser() throws -> User? {
|
||||
let r = chatSendCmdSync(.showActiveUser)
|
||||
func apiGetActiveUser(ctrl: chat_ctrl? = nil) throws -> User? {
|
||||
let r = chatSendCmdSync(.showActiveUser, ctrl)
|
||||
switch r {
|
||||
case let .activeUser(user): return user
|
||||
case .chatCmdError(_, .error(.noActiveUser)): return nil
|
||||
@@ -131,8 +133,8 @@ func apiGetActiveUser() throws -> User? {
|
||||
}
|
||||
}
|
||||
|
||||
func apiCreateActiveUser(_ p: Profile?, sameServers: Bool = false, pastTimestamp: Bool = false) throws -> User {
|
||||
let r = chatSendCmdSync(.createActiveUser(profile: p, sameServers: sameServers, pastTimestamp: pastTimestamp))
|
||||
func apiCreateActiveUser(_ p: Profile?, sameServers: Bool = false, pastTimestamp: Bool = false, ctrl: chat_ctrl? = nil) throws -> User {
|
||||
let r = chatSendCmdSync(.createActiveUser(profile: p, sameServers: sameServers, pastTimestamp: pastTimestamp), ctrl)
|
||||
if case let .activeUser(user) = r { return user }
|
||||
throw r
|
||||
}
|
||||
@@ -210,8 +212,8 @@ func apiDeleteUser(_ userId: Int64, _ delSMPQueues: Bool, viewPwd: String?) asyn
|
||||
throw r
|
||||
}
|
||||
|
||||
func apiStartChat() throws -> Bool {
|
||||
let r = chatSendCmdSync(.startChat(mainApp: true))
|
||||
func apiStartChat(ctrl: chat_ctrl? = nil) throws -> Bool {
|
||||
let r = chatSendCmdSync(.startChat(mainApp: true), ctrl)
|
||||
switch r {
|
||||
case .chatStarted: return true
|
||||
case .chatRunning: return false
|
||||
@@ -240,20 +242,14 @@ func apiSuspendChat(timeoutMicroseconds: Int) {
|
||||
logger.error("apiSuspendChat error: \(String(describing: r))")
|
||||
}
|
||||
|
||||
func apiSetTempFolder(tempFolder: String) throws {
|
||||
let r = chatSendCmdSync(.setTempFolder(tempFolder: tempFolder))
|
||||
func apiSetTempFolder(tempFolder: String, ctrl: chat_ctrl? = nil) throws {
|
||||
let r = chatSendCmdSync(.setTempFolder(tempFolder: tempFolder), ctrl)
|
||||
if case .cmdOk = r { return }
|
||||
throw r
|
||||
}
|
||||
|
||||
func apiSetFilesFolder(filesFolder: String) throws {
|
||||
let r = chatSendCmdSync(.setFilesFolder(filesFolder: filesFolder))
|
||||
if case .cmdOk = r { return }
|
||||
throw r
|
||||
}
|
||||
|
||||
func setXFTPConfig(_ cfg: XFTPFileConfig?) throws {
|
||||
let r = chatSendCmdSync(.apiSetXFTPConfig(config: cfg))
|
||||
func apiSetFilesFolder(filesFolder: String, ctrl: chat_ctrl? = nil) throws {
|
||||
let r = chatSendCmdSync(.setFilesFolder(filesFolder: filesFolder), ctrl)
|
||||
if case .cmdOk = r { return }
|
||||
throw r
|
||||
}
|
||||
@@ -264,6 +260,30 @@ func apiSetEncryptLocalFiles(_ enable: Bool) throws {
|
||||
throw r
|
||||
}
|
||||
|
||||
func apiSaveAppSettings(settings: AppSettings) throws {
|
||||
let r = chatSendCmdSync(.apiSaveSettings(settings: settings))
|
||||
if case .cmdOk = r { return }
|
||||
throw r
|
||||
}
|
||||
|
||||
func apiGetAppSettings(settings: AppSettings) throws -> AppSettings {
|
||||
let r = chatSendCmdSync(.apiGetSettings(settings: settings))
|
||||
if case let .appSettings(settings) = r { return settings }
|
||||
throw r
|
||||
}
|
||||
|
||||
func apiSetPQEncryption(_ enable: Bool) throws {
|
||||
let r = chatSendCmdSync(.apiSetPQEncryption(enable: enable))
|
||||
if case .cmdOk = r { return }
|
||||
throw r
|
||||
}
|
||||
|
||||
func apiSetContactPQ(_ contactId: Int64, _ enable: Bool) async throws -> Contact {
|
||||
let r = await chatSendCmd(.apiSetContactPQ(contactId: contactId, enable: enable))
|
||||
if case let .contactPQAllowed(_, contact, _) = r { return contact }
|
||||
throw r
|
||||
}
|
||||
|
||||
func apiExportArchive(config: ArchiveConfig) async throws {
|
||||
try await sendCommandOkResp(.apiExportArchive(config: config))
|
||||
}
|
||||
@@ -282,6 +302,10 @@ func apiStorageEncryption(currentKey: String = "", newKey: String = "") async th
|
||||
try await sendCommandOkResp(.apiStorageEncryption(config: DBEncryptionConfig(currentKey: currentKey, newKey: newKey)))
|
||||
}
|
||||
|
||||
func testStorageEncryption(key: String, _ ctrl: chat_ctrl? = nil) async throws {
|
||||
try await sendCommandOkResp(.testStorageEncryption(key: key), ctrl)
|
||||
}
|
||||
|
||||
func apiGetChats() throws -> [ChatData] {
|
||||
let userId = try currentUserId("apiGetChats")
|
||||
return try apiChatsResponse(chatSendCmdSync(.apiGetChats(userId: userId)))
|
||||
@@ -329,11 +353,20 @@ func apiGetChatItemInfo(type: ChatType, id: Int64, itemId: Int64) async throws -
|
||||
throw r
|
||||
}
|
||||
|
||||
func apiForwardChatItem(toChatType: ChatType, toChatId: Int64, fromChatType: ChatType, fromChatId: Int64, itemId: Int64) async -> ChatItem? {
|
||||
let cmd: ChatCommand = .apiForwardChatItem(toChatType: toChatType, toChatId: toChatId, fromChatType: fromChatType, fromChatId: fromChatId, itemId: itemId)
|
||||
return await processSendMessageCmd(toChatType: toChatType, cmd: cmd)
|
||||
}
|
||||
|
||||
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(type: type, id: id, file: file, quotedItemId: quotedItemId, msg: msg, live: live, ttl: ttl)
|
||||
return await processSendMessageCmd(toChatType: type, cmd: cmd)
|
||||
}
|
||||
|
||||
private func processSendMessageCmd(toChatType: ChatType, cmd: ChatCommand) async -> ChatItem? {
|
||||
let chatModel = ChatModel.shared
|
||||
let r: ChatResponse
|
||||
if type == .direct {
|
||||
if toChatType == .direct {
|
||||
var cItem: ChatItem? = nil
|
||||
let endTask = beginBGTask({
|
||||
if let cItem = cItem {
|
||||
@@ -373,7 +406,7 @@ func apiCreateChatItem(noteFolderId: Int64, file: CryptoFile?, msg: MsgContent)
|
||||
}
|
||||
|
||||
private func sendMessageErrorAlert(_ r: ChatResponse) {
|
||||
logger.error("apiSendMessage error: \(String(describing: r))")
|
||||
logger.error("send message error: \(String(describing: r))")
|
||||
AlertManager.shared.showAlertMsg(
|
||||
title: "Error sending message",
|
||||
message: "Error: \(String(describing: r))"
|
||||
@@ -412,14 +445,14 @@ func apiDeleteMemberChatItem(groupId: Int64, groupMemberId: Int64, itemId: Int64
|
||||
throw r
|
||||
}
|
||||
|
||||
func apiGetNtfToken() -> (DeviceToken?, NtfTknStatus?, NotificationsMode) {
|
||||
func apiGetNtfToken() -> (DeviceToken?, NtfTknStatus?, NotificationsMode, String?) {
|
||||
let r = chatSendCmdSync(.apiGetNtfToken)
|
||||
switch r {
|
||||
case let .ntfToken(token, status, ntfMode): return (token, status, ntfMode)
|
||||
case .chatCmdError(_, .errorAgent(.CMD(.PROHIBITED))): return (nil, nil, .off)
|
||||
case let .ntfToken(token, status, ntfMode, ntfServer): return (token, status, ntfMode, ntfServer)
|
||||
case .chatCmdError(_, .errorAgent(.CMD(.PROHIBITED))): return (nil, nil, .off, nil)
|
||||
default:
|
||||
logger.debug("apiGetNtfToken response: \(String(describing: r))")
|
||||
return (nil, nil, .off)
|
||||
return (nil, nil, .off, nil)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -504,8 +537,14 @@ func getNetworkConfig() async throws -> NetCfg? {
|
||||
throw r
|
||||
}
|
||||
|
||||
func setNetworkConfig(_ cfg: NetCfg) throws {
|
||||
let r = chatSendCmdSync(.apiSetNetworkConfig(networkConfig: cfg))
|
||||
func setNetworkConfig(_ cfg: NetCfg, ctrl: chat_ctrl? = nil) throws {
|
||||
let r = chatSendCmdSync(.apiSetNetworkConfig(networkConfig: cfg), ctrl)
|
||||
if case .cmdOk = r { return }
|
||||
throw r
|
||||
}
|
||||
|
||||
func apiSetNetworkInfo(_ networkInfo: UserNetworkInfo) throws {
|
||||
let r = chatSendCmdSync(.apiSetNetworkInfo(networkInfo: networkInfo))
|
||||
if case .cmdOk = r { return }
|
||||
throw r
|
||||
}
|
||||
@@ -870,6 +909,36 @@ func apiChatUnread(type: ChatType, id: Int64, unreadChat: Bool) async throws {
|
||||
try await sendCommandOkResp(.apiChatUnread(type: type, id: id, unreadChat: unreadChat))
|
||||
}
|
||||
|
||||
func uploadStandaloneFile(user: any UserLike, file: CryptoFile, ctrl: chat_ctrl? = nil) async -> (FileTransferMeta?, String?) {
|
||||
let r = await chatSendCmd(.apiUploadStandaloneFile(userId: user.userId, file: file), ctrl)
|
||||
if case let .sndStandaloneFileCreated(_, fileTransferMeta) = r {
|
||||
return (fileTransferMeta, nil)
|
||||
} else {
|
||||
logger.error("uploadStandaloneFile error: \(String(describing: r))")
|
||||
return (nil, String(describing: r))
|
||||
}
|
||||
}
|
||||
|
||||
func downloadStandaloneFile(user: any UserLike, url: String, file: CryptoFile, ctrl: chat_ctrl? = nil) async -> (RcvFileTransfer?, String?) {
|
||||
let r = await chatSendCmd(.apiDownloadStandaloneFile(userId: user.userId, url: url, file: file), ctrl)
|
||||
if case let .rcvStandaloneFileCreated(_, rcvFileTransfer) = r {
|
||||
return (rcvFileTransfer, nil)
|
||||
} else {
|
||||
logger.error("downloadStandaloneFile error: \(String(describing: r))")
|
||||
return (nil, String(describing: r))
|
||||
}
|
||||
}
|
||||
|
||||
func standaloneFileInfo(url: String, ctrl: chat_ctrl? = nil) async -> MigrationFileLinkData? {
|
||||
let r = await chatSendCmd(.apiStandaloneFileInfo(url: url), ctrl)
|
||||
if case let .standaloneFileInfo(fileMeta) = r {
|
||||
return fileMeta
|
||||
} else {
|
||||
logger.error("standaloneFileInfo error: \(String(describing: r))")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func receiveFile(user: any UserLike, fileId: Int64, auto: Bool = false) async {
|
||||
if let chatItem = await apiReceiveFile(fileId: fileId, encrypted: privacyEncryptLocalFilesGroupDefault.get(), auto: auto) {
|
||||
await chatItemSimpleUpdate(user, chatItem)
|
||||
@@ -915,8 +984,8 @@ func cancelFile(user: User, fileId: Int64) async {
|
||||
}
|
||||
}
|
||||
|
||||
func apiCancelFile(fileId: Int64) async -> AChatItem? {
|
||||
let r = await chatSendCmd(.cancelFile(fileId: fileId))
|
||||
func apiCancelFile(fileId: Int64, ctrl: chat_ctrl? = nil) async -> AChatItem? {
|
||||
let r = await chatSendCmd(.cancelFile(fileId: fileId), ctrl)
|
||||
switch r {
|
||||
case let .sndFileCancelled(_, chatItem, _, _) : return chatItem
|
||||
case let .rcvFileCancelled(_, chatItem, _) : return chatItem
|
||||
@@ -1088,8 +1157,8 @@ func apiMarkChatItemRead(_ cInfo: ChatInfo, _ cItem: ChatItem) async {
|
||||
}
|
||||
}
|
||||
|
||||
private func sendCommandOkResp(_ cmd: ChatCommand) async throws {
|
||||
let r = await chatSendCmd(cmd)
|
||||
private func sendCommandOkResp(_ cmd: ChatCommand, _ ctrl: chat_ctrl? = nil) async throws {
|
||||
let r = await chatSendCmd(cmd, ctrl)
|
||||
if case .cmdOk = r { return }
|
||||
throw r
|
||||
}
|
||||
@@ -1172,7 +1241,7 @@ 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) }
|
||||
.filter{ c in c.ready && c.active && !memberContactIds.contains(c.apiId) }
|
||||
.sorted{ $0.displayName.lowercased() < $1.displayName.lowercased() }
|
||||
}
|
||||
|
||||
@@ -1243,14 +1312,15 @@ func initializeChat(start: Bool, confirmStart: Bool = false, dbKey: String? = ni
|
||||
defer { m.ctrlInitInProgress = false }
|
||||
(m.chatDbEncrypted, m.chatDbStatus) = chatMigrateInit(dbKey, confirmMigrations: confirmMigrations)
|
||||
if m.chatDbStatus != .ok { return }
|
||||
NetworkObserver.shared.restartMonitor()
|
||||
// If we migrated successfully means previous re-encryption process on database level finished successfully too
|
||||
if encryptionStartedDefault.get() {
|
||||
encryptionStartedDefault.set(false)
|
||||
}
|
||||
try apiSetTempFolder(tempFolder: getTempFilesDirectory().path)
|
||||
try apiSetFilesFolder(filesFolder: getAppFilesDirectory().path)
|
||||
try setXFTPConfig(getXFTPCfg())
|
||||
try apiSetEncryptLocalFiles(privacyEncryptLocalFilesGroupDefault.get())
|
||||
try apiSetPQEncryption(pqExperimentalEnabledDefault.get())
|
||||
m.chatInitialized = true
|
||||
m.currentUser = try apiGetActiveUser()
|
||||
if m.currentUser == nil {
|
||||
@@ -1309,7 +1379,7 @@ func startChat(refreshInvitations: Bool = true) throws {
|
||||
if (refreshInvitations) {
|
||||
try refreshCallInvitations()
|
||||
}
|
||||
(m.savedToken, m.tokenStatus, m.notificationMode) = apiGetNtfToken()
|
||||
(m.savedToken, m.tokenStatus, m.notificationMode, m.notificationServer) = apiGetNtfToken()
|
||||
// deviceToken is set when AppDelegate.application(didRegisterForRemoteNotificationsWithDeviceToken:) is called,
|
||||
// when it is called before startChat
|
||||
if let token = m.deviceToken {
|
||||
@@ -1330,6 +1400,16 @@ func startChat(refreshInvitations: Bool = true) throws {
|
||||
chatLastStartGroupDefault.set(Date.now)
|
||||
}
|
||||
|
||||
func startChatWithTemporaryDatabase(ctrl: chat_ctrl) throws -> User? {
|
||||
logger.debug("startChatWithTemporaryDatabase")
|
||||
let migrationActiveUser = try? apiGetActiveUser(ctrl: ctrl) ?? apiCreateActiveUser(Profile(displayName: "Temp", fullName: ""), ctrl: ctrl)
|
||||
try setNetworkConfig(getNetCfg(), ctrl: ctrl)
|
||||
try apiSetTempFolder(tempFolder: getMigrationTempFilesDirectory().path, ctrl: ctrl)
|
||||
try apiSetFilesFolder(filesFolder: getMigrationTempFilesDirectory().path, ctrl: ctrl)
|
||||
_ = try apiStartChat(ctrl: ctrl)
|
||||
return migrationActiveUser
|
||||
}
|
||||
|
||||
func changeActiveUser(_ userId: Int64, viewPwd: String?) {
|
||||
do {
|
||||
try changeActiveUser_(userId, viewPwd: viewPwd)
|
||||
@@ -1398,6 +1478,8 @@ class ChatReceiver {
|
||||
private var receiveMessages = true
|
||||
private var _lastMsgTime = Date.now
|
||||
|
||||
var messagesChannel: ((ChatResponse) -> Void)? = nil
|
||||
|
||||
static let shared = ChatReceiver()
|
||||
|
||||
var lastMsgTime: Date { get { _lastMsgTime } }
|
||||
@@ -1411,14 +1493,15 @@ class ChatReceiver {
|
||||
}
|
||||
|
||||
func receiveMsgLoop() async {
|
||||
// TODO use function that has timeout
|
||||
if let msg = await chatRecvMsg() {
|
||||
self._lastMsgTime = .now
|
||||
await processReceivedMsg(msg)
|
||||
}
|
||||
if self.receiveMessages {
|
||||
while self.receiveMessages {
|
||||
if let msg = await chatRecvMsg() {
|
||||
self._lastMsgTime = .now
|
||||
await processReceivedMsg(msg)
|
||||
if let messagesChannel {
|
||||
messagesChannel(msg)
|
||||
}
|
||||
}
|
||||
_ = try? await Task.sleep(nanoseconds: 7_500_000)
|
||||
await receiveMsgLoop()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1506,34 +1589,30 @@ func processReceivedMsg(_ res: ChatResponse) async {
|
||||
m.removeChat(mergedContact.id)
|
||||
}
|
||||
}
|
||||
case let .contactsSubscribed(_, contactRefs):
|
||||
await updateContactsStatus(contactRefs, status: .connected)
|
||||
case let .contactsDisconnected(_, contactRefs):
|
||||
await updateContactsStatus(contactRefs, status: .disconnected)
|
||||
case let .contactSubSummary(_, contactSubscriptions):
|
||||
await MainActor.run {
|
||||
for sub in contactSubscriptions {
|
||||
// no need to update contact here, and it is slow
|
||||
// if active(user) {
|
||||
// m.updateContact(sub.contact)
|
||||
// }
|
||||
if let err = sub.contactError {
|
||||
processContactSubError(sub.contact, err)
|
||||
} else {
|
||||
m.setContactNetworkStatus(sub.contact, .connected)
|
||||
}
|
||||
}
|
||||
}
|
||||
case let .networkStatus(status, connections):
|
||||
await MainActor.run {
|
||||
// dispatch queue to synchronize access
|
||||
networkStatusesLock.sync {
|
||||
var ns = m.networkStatuses
|
||||
// slow loop is on the background thread
|
||||
for cId in connections {
|
||||
m.networkStatuses[cId] = status
|
||||
ns[cId] = status
|
||||
}
|
||||
// fast model update is on the main thread
|
||||
DispatchQueue.main.sync {
|
||||
m.networkStatuses = ns
|
||||
}
|
||||
}
|
||||
case let .networkStatuses(_, statuses): ()
|
||||
await MainActor.run {
|
||||
// dispatch queue to synchronize access
|
||||
networkStatusesLock.sync {
|
||||
var ns = m.networkStatuses
|
||||
// slow loop is on the background thread
|
||||
for s in statuses {
|
||||
m.networkStatuses[s.agentConnId] = s.networkStatus
|
||||
ns[s.agentConnId] = s.networkStatus
|
||||
}
|
||||
// fast model update is on the main thread
|
||||
DispatchQueue.main.sync {
|
||||
m.networkStatuses = ns
|
||||
}
|
||||
}
|
||||
case let .newChatItem(user, aChatItem):
|
||||
@@ -1708,27 +1787,36 @@ func processReceivedMsg(_ res: ChatResponse) async {
|
||||
case let .rcvFileSndCancelled(user, aChatItem, _):
|
||||
await chatItemSimpleUpdate(user, aChatItem)
|
||||
Task { cleanupFile(aChatItem) }
|
||||
case let .rcvFileProgressXFTP(user, aChatItem, _, _):
|
||||
await chatItemSimpleUpdate(user, aChatItem)
|
||||
case let .rcvFileError(user, aChatItem):
|
||||
await chatItemSimpleUpdate(user, aChatItem)
|
||||
Task { cleanupFile(aChatItem) }
|
||||
case let .rcvFileProgressXFTP(user, aChatItem, _, _, _):
|
||||
if let aChatItem = aChatItem {
|
||||
await chatItemSimpleUpdate(user, aChatItem)
|
||||
}
|
||||
case let .rcvFileError(user, aChatItem, _):
|
||||
if let aChatItem = aChatItem {
|
||||
await chatItemSimpleUpdate(user, aChatItem)
|
||||
Task { cleanupFile(aChatItem) }
|
||||
}
|
||||
case let .sndFileStart(user, aChatItem, _):
|
||||
await chatItemSimpleUpdate(user, aChatItem)
|
||||
case let .sndFileComplete(user, aChatItem, _):
|
||||
await chatItemSimpleUpdate(user, aChatItem)
|
||||
Task { cleanupDirectFile(aChatItem) }
|
||||
case let .sndFileRcvCancelled(user, aChatItem, _):
|
||||
await chatItemSimpleUpdate(user, aChatItem)
|
||||
Task { cleanupDirectFile(aChatItem) }
|
||||
if let aChatItem = aChatItem {
|
||||
await chatItemSimpleUpdate(user, aChatItem)
|
||||
Task { cleanupDirectFile(aChatItem) }
|
||||
}
|
||||
case let .sndFileProgressXFTP(user, aChatItem, _, _, _):
|
||||
await chatItemSimpleUpdate(user, aChatItem)
|
||||
if let aChatItem = aChatItem {
|
||||
await chatItemSimpleUpdate(user, aChatItem)
|
||||
}
|
||||
case let .sndFileCompleteXFTP(user, aChatItem, _):
|
||||
await chatItemSimpleUpdate(user, aChatItem)
|
||||
Task { cleanupFile(aChatItem) }
|
||||
case let .sndFileError(user, aChatItem):
|
||||
await chatItemSimpleUpdate(user, aChatItem)
|
||||
Task { cleanupFile(aChatItem) }
|
||||
case let .sndFileError(user, aChatItem, _):
|
||||
if let aChatItem = aChatItem {
|
||||
await chatItemSimpleUpdate(user, aChatItem)
|
||||
Task { cleanupFile(aChatItem) }
|
||||
}
|
||||
case let .callInvitation(invitation):
|
||||
await MainActor.run {
|
||||
m.callInvitations[invitation.contact.id] = invitation
|
||||
@@ -1825,6 +1913,12 @@ func processReceivedMsg(_ res: ChatResponse) async {
|
||||
}
|
||||
}
|
||||
}
|
||||
case let .contactPQEnabled(user, contact, _):
|
||||
if active(user) {
|
||||
await MainActor.run {
|
||||
m.updateContact(contact)
|
||||
}
|
||||
}
|
||||
default:
|
||||
logger.debug("unsupported event: \(res.responseType)")
|
||||
}
|
||||
@@ -1861,31 +1955,13 @@ func chatItemSimpleUpdate(_ user: any UserLike, _ aChatItem: AChatItem) async {
|
||||
let cItem = aChatItem.chatItem
|
||||
if active(user) {
|
||||
if await MainActor.run(body: { m.upsertChatItem(cInfo, cItem) }) {
|
||||
NtfManager.shared.notifyMessageReceived(user, cInfo, cItem)
|
||||
if cItem.showNotification {
|
||||
NtfManager.shared.notifyMessageReceived(user, cInfo, cItem)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func updateContactsStatus(_ contactRefs: [ContactRef], status: NetworkStatus) async {
|
||||
let m = ChatModel.shared
|
||||
await MainActor.run {
|
||||
for c in contactRefs {
|
||||
m.networkStatuses[c.agentConnId] = status
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func processContactSubError(_ contact: Contact, _ chatError: ChatError) {
|
||||
let m = ChatModel.shared
|
||||
var err: String
|
||||
switch chatError {
|
||||
case .errorAgent(agentError: .BROKER(_, .NETWORK)): err = "network"
|
||||
case .errorAgent(agentError: .SMP(smpErr: .AUTH)): err = "contact deleted"
|
||||
default: err = String(describing: chatError)
|
||||
}
|
||||
m.setContactNetworkStatus(contact, .error(connectionError: err))
|
||||
}
|
||||
|
||||
func refreshCallInvitations() throws {
|
||||
let m = ChatModel.shared
|
||||
let callInvitations = try justRefreshCallInvitations()
|
||||
|
||||
@@ -44,7 +44,12 @@ struct SimpleXApp: App {
|
||||
chatModel.appOpenUrl = url
|
||||
}
|
||||
.onAppear() {
|
||||
if kcAppPassword.get() == nil || kcSelfDestructPassword.get() == nil {
|
||||
// Present screen for continue migration if it wasn't finished yet
|
||||
if chatModel.migrationState != nil {
|
||||
// It's important, otherwise, user may be locked in undefined state
|
||||
onboardingStageDefault.set(.step1_SimpleXInfo)
|
||||
chatModel.onboardingStage = onboardingStageDefault.get()
|
||||
} else if kcAppPassword.get() == nil || kcSelfDestructPassword.get() == nil {
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.15) {
|
||||
initChatAndMigrate()
|
||||
}
|
||||
|
||||
@@ -9,52 +9,77 @@
|
||||
import SwiftUI
|
||||
import WebKit
|
||||
import SimpleXChat
|
||||
import AVFoundation
|
||||
|
||||
struct ActiveCallView: View {
|
||||
@EnvironmentObject var m: ChatModel
|
||||
@Environment(\.colorScheme) var colorScheme
|
||||
@ObservedObject var call: Call
|
||||
@Environment(\.scenePhase) var scenePhase
|
||||
@State private var client: WebRTCClient? = nil
|
||||
@State private var activeCall: WebRTCClient.Call? = nil
|
||||
@State private var localRendererAspectRatio: CGFloat? = nil
|
||||
@Binding var canConnectCall: Bool
|
||||
@State var prevColorScheme: ColorScheme = .dark
|
||||
@State var pipShown = false
|
||||
@State var wasConnected = false
|
||||
|
||||
var body: some View {
|
||||
ZStack(alignment: .bottom) {
|
||||
if let client = client, [call.peerMedia, call.localMedia].contains(.video), activeCall != nil {
|
||||
GeometryReader { g in
|
||||
let width = g.size.width * 0.3
|
||||
ZStack(alignment: .topTrailing) {
|
||||
CallViewRemote(client: client, activeCall: $activeCall)
|
||||
CallViewLocal(client: client, activeCall: $activeCall, localRendererAspectRatio: $localRendererAspectRatio)
|
||||
.cornerRadius(10)
|
||||
.frame(width: width, height: width / (localRendererAspectRatio ?? 1))
|
||||
.padding([.top, .trailing], 17)
|
||||
ZStack(alignment: .topLeading) {
|
||||
ZStack(alignment: .bottom) {
|
||||
if let client = client, [call.peerMedia, call.localMedia].contains(.video), activeCall != nil {
|
||||
GeometryReader { g in
|
||||
let width = g.size.width * 0.3
|
||||
ZStack(alignment: .topTrailing) {
|
||||
CallViewRemote(client: client, activeCall: $activeCall, activeCallViewIsCollapsed: $m.activeCallViewIsCollapsed, pipShown: $pipShown)
|
||||
CallViewLocal(client: client, activeCall: $activeCall, localRendererAspectRatio: $localRendererAspectRatio, pipShown: $pipShown)
|
||||
.cornerRadius(10)
|
||||
.frame(width: width, height: width / (localRendererAspectRatio ?? 1))
|
||||
.padding([.top, .trailing], 17)
|
||||
ZStack(alignment: .center) {
|
||||
// For some reason, when the view in GeometryReader and ZStack is visible, it steals clicks on a back button, so showing something on top like this with background color helps (.clear color doesn't work)
|
||||
}
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
.background(Color.primary.opacity(0.000001))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if let call = m.activeCall, let client = client {
|
||||
ActiveCallOverlay(call: call, client: client)
|
||||
if let call = m.activeCall, let client = client, (!pipShown || !call.supportsVideo) {
|
||||
ActiveCallOverlay(call: call, client: client)
|
||||
}
|
||||
}
|
||||
}
|
||||
.allowsHitTesting(!m.activeCallViewIsCollapsed)
|
||||
.opacity(m.activeCallViewIsCollapsed ? 0 : 1)
|
||||
.onAppear {
|
||||
logger.debug("ActiveCallView: appear client is nil \(client == nil), scenePhase \(String(describing: scenePhase)), canConnectCall \(canConnectCall)")
|
||||
AppDelegate.keepScreenOn(true)
|
||||
createWebRTCClient()
|
||||
dismissAllSheets()
|
||||
hideKeyboard()
|
||||
prevColorScheme = colorScheme
|
||||
}
|
||||
.onChange(of: canConnectCall) { _ in
|
||||
logger.debug("ActiveCallView: canConnectCall changed to \(canConnectCall)")
|
||||
createWebRTCClient()
|
||||
}
|
||||
.onChange(of: m.activeCallViewIsCollapsed) { _ in
|
||||
hideKeyboard()
|
||||
}
|
||||
.onDisappear {
|
||||
logger.debug("ActiveCallView: disappear")
|
||||
Task { await m.callCommand.setClient(nil) }
|
||||
AppDelegate.keepScreenOn(false)
|
||||
client?.endCall()
|
||||
CallSoundsPlayer.shared.stop()
|
||||
try? AVAudioSession.sharedInstance().setCategory(.soloAmbient)
|
||||
if (wasConnected) {
|
||||
CallSoundsPlayer.shared.vibrate(long: true)
|
||||
}
|
||||
}
|
||||
.background(.black)
|
||||
.preferredColorScheme(.dark)
|
||||
.background(m.activeCallViewIsCollapsed ? .clear : .black)
|
||||
// Quite a big delay when opening/closing the view when a scheme changes (globally) this way. It's not needed when CallKit is used since status bar is green with white text on it
|
||||
.preferredColorScheme(m.activeCallViewIsCollapsed || CallController.useCallKit() ? prevColorScheme : .dark)
|
||||
}
|
||||
|
||||
private func createWebRTCClient() {
|
||||
@@ -69,8 +94,8 @@ struct ActiveCallView: View {
|
||||
@MainActor
|
||||
private func processRtcMessage(msg: WVAPIMessage) {
|
||||
if call == m.activeCall,
|
||||
let call = m.activeCall,
|
||||
let client = client {
|
||||
let call = m.activeCall,
|
||||
let client = client {
|
||||
logger.debug("ActiveCallView: response \(msg.resp.respType)")
|
||||
switch msg.resp {
|
||||
case let .capabilities(capabilities):
|
||||
@@ -85,12 +110,17 @@ struct ActiveCallView: View {
|
||||
call.callState = .invitationSent
|
||||
call.localCapabilities = capabilities
|
||||
}
|
||||
if call.supportsVideo {
|
||||
try? AVAudioSession.sharedInstance().setCategory(.playback, options: .defaultToSpeaker)
|
||||
}
|
||||
CallSoundsPlayer.shared.startConnectingCallSound()
|
||||
activeCallWaitDeliveryReceipt()
|
||||
}
|
||||
case let .offer(offer, iceCandidates, capabilities):
|
||||
Task {
|
||||
do {
|
||||
try await apiSendCallOffer(call.contact, offer, iceCandidates,
|
||||
media: call.localMedia, capabilities: capabilities)
|
||||
media: call.localMedia, capabilities: capabilities)
|
||||
} catch {
|
||||
logger.error("apiSendCallOffer \(responseError(error))")
|
||||
}
|
||||
@@ -108,6 +138,8 @@ struct ActiveCallView: View {
|
||||
}
|
||||
await MainActor.run {
|
||||
call.callState = .negotiated
|
||||
CallSoundsPlayer.shared.stop()
|
||||
try? AVAudioSession.sharedInstance().setCategory(.soloAmbient)
|
||||
}
|
||||
}
|
||||
case let .ice(iceCandidates):
|
||||
@@ -122,13 +154,19 @@ struct ActiveCallView: View {
|
||||
if let callStatus = WebRTCCallStatus.init(rawValue: state.connectionState),
|
||||
case .connected = callStatus {
|
||||
call.direction == .outgoing
|
||||
? CallController.shared.reportOutgoingCall(call: call, connectedAt: nil)
|
||||
: CallController.shared.reportIncomingCall(call: call, connectedAt: nil)
|
||||
? CallController.shared.reportOutgoingCall(call: call, connectedAt: nil)
|
||||
: CallController.shared.reportIncomingCall(call: call, connectedAt: nil)
|
||||
call.callState = .connected
|
||||
call.connectedAt = .now
|
||||
if !wasConnected {
|
||||
CallSoundsPlayer.shared.vibrate(long: false)
|
||||
wasConnected = true
|
||||
}
|
||||
}
|
||||
if state.connectionState == "closed" {
|
||||
closeCallView(client)
|
||||
m.activeCall = nil
|
||||
m.activeCallViewIsCollapsed = false
|
||||
}
|
||||
Task {
|
||||
do {
|
||||
@@ -140,6 +178,11 @@ struct ActiveCallView: View {
|
||||
case let .connected(connectionInfo):
|
||||
call.callState = .connected
|
||||
call.connectionInfo = connectionInfo
|
||||
call.connectedAt = .now
|
||||
if !wasConnected {
|
||||
CallSoundsPlayer.shared.vibrate(long: false)
|
||||
wasConnected = true
|
||||
}
|
||||
case .ended:
|
||||
closeCallView(client)
|
||||
call.callState = .ended
|
||||
@@ -153,6 +196,7 @@ struct ActiveCallView: View {
|
||||
case .end:
|
||||
closeCallView(client)
|
||||
m.activeCall = nil
|
||||
m.activeCallViewIsCollapsed = false
|
||||
default: ()
|
||||
}
|
||||
case let .error(message):
|
||||
@@ -165,6 +209,22 @@ struct ActiveCallView: View {
|
||||
}
|
||||
}
|
||||
|
||||
private func activeCallWaitDeliveryReceipt() {
|
||||
ChatReceiver.shared.messagesChannel = { msg in
|
||||
guard let call = ChatModel.shared.activeCall, call.callState == .invitationSent else {
|
||||
ChatReceiver.shared.messagesChannel = nil
|
||||
return
|
||||
}
|
||||
if case let .chatItemStatusUpdated(_, msg) = msg,
|
||||
msg.chatInfo.id == call.contact.id,
|
||||
case .sndCall = msg.chatItem.content,
|
||||
case .sndRcvd = msg.chatItem.meta.itemStatus {
|
||||
CallSoundsPlayer.shared.startInCallSound()
|
||||
ChatReceiver.shared.messagesChannel = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func closeCallView(_ client: WebRTCClient) {
|
||||
if m.activeCall != nil {
|
||||
m.showCallView = false
|
||||
@@ -181,7 +241,7 @@ struct ActiveCallOverlay: View {
|
||||
VStack {
|
||||
switch call.localMedia {
|
||||
case .video:
|
||||
callInfoView(call, .leading)
|
||||
videoCallInfoView(call)
|
||||
.foregroundColor(.white)
|
||||
.opacity(0.8)
|
||||
.padding()
|
||||
@@ -208,16 +268,25 @@ struct ActiveCallOverlay: View {
|
||||
.frame(maxWidth: .infinity, alignment: .center)
|
||||
|
||||
case .audio:
|
||||
VStack {
|
||||
ProfileImage(imageStr: call.contact.profile.image)
|
||||
.scaledToFit()
|
||||
.frame(width: 192, height: 192)
|
||||
callInfoView(call, .center)
|
||||
ZStack(alignment: .topLeading) {
|
||||
Button {
|
||||
chatModel.activeCallViewIsCollapsed = true
|
||||
} label: {
|
||||
Label("Back", systemImage: "chevron.left")
|
||||
.padding()
|
||||
.foregroundColor(.white.opacity(0.8))
|
||||
}
|
||||
VStack {
|
||||
ProfileImage(imageStr: call.contact.profile.image)
|
||||
.scaledToFit()
|
||||
.frame(width: 192, height: 192)
|
||||
audioCallInfoView(call)
|
||||
}
|
||||
.foregroundColor(.white)
|
||||
.opacity(0.8)
|
||||
.padding()
|
||||
.frame(maxHeight: .infinity)
|
||||
}
|
||||
.foregroundColor(.white)
|
||||
.opacity(0.8)
|
||||
.padding()
|
||||
.frame(maxHeight: .infinity)
|
||||
|
||||
Spacer()
|
||||
|
||||
@@ -235,12 +304,12 @@ struct ActiveCallOverlay: View {
|
||||
.frame(maxWidth: .infinity)
|
||||
}
|
||||
|
||||
private func callInfoView(_ call: Call, _ alignment: Alignment) -> some View {
|
||||
private func audioCallInfoView(_ call: Call) -> some View {
|
||||
VStack {
|
||||
Text(call.contact.chatViewName)
|
||||
.lineLimit(1)
|
||||
.font(.title)
|
||||
.frame(maxWidth: .infinity, alignment: alignment)
|
||||
.frame(maxWidth: .infinity, alignment: .center)
|
||||
Group {
|
||||
Text(call.callState.text)
|
||||
HStack {
|
||||
@@ -251,7 +320,36 @@ struct ActiveCallOverlay: View {
|
||||
}
|
||||
}
|
||||
.font(.subheadline)
|
||||
.frame(maxWidth: .infinity, alignment: alignment)
|
||||
.frame(maxWidth: .infinity, alignment: .center)
|
||||
}
|
||||
}
|
||||
|
||||
private func videoCallInfoView(_ call: Call) -> some View {
|
||||
VStack {
|
||||
Button {
|
||||
chatModel.activeCallViewIsCollapsed = true
|
||||
} label: {
|
||||
HStack(alignment: .center, spacing: 16) {
|
||||
Image(systemName: "chevron.left")
|
||||
.resizable()
|
||||
.frame(width: 10, height: 18)
|
||||
Text(call.contact.chatViewName)
|
||||
.lineLimit(1)
|
||||
.font(.title)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
}
|
||||
}
|
||||
Group {
|
||||
Text(call.callState.text)
|
||||
HStack {
|
||||
Text(call.encryptionStatus)
|
||||
if let connInfo = call.connectionInfo {
|
||||
Text("(") + Text(connInfo.text) + Text(")")
|
||||
}
|
||||
}
|
||||
}
|
||||
.font(.subheadline)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -92,6 +92,7 @@ class CallManager {
|
||||
if case .ended = call.callState {
|
||||
logger.debug("CallManager.endCall: call ended")
|
||||
m.activeCall = nil
|
||||
m.activeCallViewIsCollapsed = false
|
||||
m.showCallView = false
|
||||
completed()
|
||||
} else {
|
||||
@@ -100,6 +101,7 @@ class CallManager {
|
||||
await m.callCommand.processCommand(.end)
|
||||
await MainActor.run {
|
||||
m.activeCall = nil
|
||||
m.activeCallViewIsCollapsed = false
|
||||
m.showCallView = false
|
||||
completed()
|
||||
}
|
||||
|
||||
@@ -6,14 +6,20 @@
|
||||
import SwiftUI
|
||||
import WebRTC
|
||||
import SimpleXChat
|
||||
import AVKit
|
||||
|
||||
struct CallViewRemote: UIViewRepresentable {
|
||||
var client: WebRTCClient
|
||||
var activeCall: Binding<WebRTCClient.Call?>
|
||||
@State var enablePip: (Bool) -> Void = {_ in }
|
||||
@Binding var activeCallViewIsCollapsed: Bool
|
||||
@Binding var pipShown: Bool
|
||||
|
||||
init(client: WebRTCClient, activeCall: Binding<WebRTCClient.Call?>) {
|
||||
init(client: WebRTCClient, activeCall: Binding<WebRTCClient.Call?>, activeCallViewIsCollapsed: Binding<Bool>, pipShown: Binding<Bool>) {
|
||||
self.client = client
|
||||
self.activeCall = activeCall
|
||||
self._activeCallViewIsCollapsed = activeCallViewIsCollapsed
|
||||
self._pipShown = pipShown
|
||||
}
|
||||
|
||||
func makeUIView(context: Context) -> UIView {
|
||||
@@ -23,12 +29,120 @@ struct CallViewRemote: UIViewRepresentable {
|
||||
remoteRenderer.videoContentMode = .scaleAspectFill
|
||||
client.addRemoteRenderer(call, remoteRenderer)
|
||||
addSubviewAndResize(remoteRenderer, into: view)
|
||||
|
||||
if AVPictureInPictureController.isPictureInPictureSupported() {
|
||||
makeViewWithRTCRenderer(call, remoteRenderer, view, context)
|
||||
}
|
||||
}
|
||||
return view
|
||||
}
|
||||
|
||||
func makeViewWithRTCRenderer(_ call: WebRTCClient.Call, _ remoteRenderer: RTCMTLVideoView, _ view: UIView, _ context: Context) {
|
||||
let pipRemoteRenderer = RTCMTLVideoView(frame: view.frame)
|
||||
pipRemoteRenderer.videoContentMode = .scaleAspectFill
|
||||
|
||||
let pipVideoCallViewController = AVPictureInPictureVideoCallViewController()
|
||||
pipVideoCallViewController.preferredContentSize = CGSize(width: 1080, height: 1920)
|
||||
addSubviewAndResize(pipRemoteRenderer, into: pipVideoCallViewController.view)
|
||||
let pipContentSource = AVPictureInPictureController.ContentSource(
|
||||
activeVideoCallSourceView: view,
|
||||
contentViewController: pipVideoCallViewController
|
||||
)
|
||||
|
||||
let pipController = AVPictureInPictureController(contentSource: pipContentSource)
|
||||
pipController.canStartPictureInPictureAutomaticallyFromInline = true
|
||||
pipController.delegate = context.coordinator
|
||||
context.coordinator.pipController = pipController
|
||||
context.coordinator.willShowHide = { show in
|
||||
if show {
|
||||
client.addRemoteRenderer(call, pipRemoteRenderer)
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
|
||||
activeCallViewIsCollapsed = true
|
||||
}
|
||||
} else {
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.05) {
|
||||
activeCallViewIsCollapsed = false
|
||||
}
|
||||
}
|
||||
}
|
||||
context.coordinator.didShowHide = { show in
|
||||
if show {
|
||||
remoteRenderer.isHidden = true
|
||||
} else {
|
||||
client.removeRemoteRenderer(call, pipRemoteRenderer)
|
||||
remoteRenderer.isHidden = false
|
||||
}
|
||||
pipShown = show
|
||||
}
|
||||
DispatchQueue.main.async {
|
||||
enablePip = { enable in
|
||||
if enable != pipShown /* pipController.isPictureInPictureActive */ {
|
||||
if enable {
|
||||
pipController.startPictureInPicture()
|
||||
} else {
|
||||
pipController.stopPictureInPicture()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func makeCoordinator() -> Coordinator {
|
||||
Coordinator()
|
||||
}
|
||||
|
||||
func updateUIView(_ view: UIView, context: Context) {
|
||||
logger.debug("CallView.updateUIView remote")
|
||||
DispatchQueue.main.async {
|
||||
if activeCallViewIsCollapsed != pipShown {
|
||||
enablePip(activeCallViewIsCollapsed)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Coordinator
|
||||
class Coordinator: NSObject, AVPictureInPictureControllerDelegate {
|
||||
var pipController: AVPictureInPictureController? = nil
|
||||
var willShowHide: (Bool) -> Void = { _ in }
|
||||
var didShowHide: (Bool) -> Void = { _ in }
|
||||
|
||||
func pictureInPictureControllerWillStartPictureInPicture(_ pictureInPictureController: AVPictureInPictureController) {
|
||||
willShowHide(true)
|
||||
}
|
||||
|
||||
func pictureInPictureControllerDidStartPictureInPicture(_ pictureInPictureController: AVPictureInPictureController) {
|
||||
didShowHide(true)
|
||||
}
|
||||
|
||||
func pictureInPictureController(_ pictureInPictureController: AVPictureInPictureController, failedToStartPictureInPictureWithError error: Error) {
|
||||
logger.error("PiP failed to start: \(error.localizedDescription)")
|
||||
}
|
||||
|
||||
func pictureInPictureControllerWillStopPictureInPicture(_ pictureInPictureController: AVPictureInPictureController) {
|
||||
willShowHide(false)
|
||||
}
|
||||
|
||||
func pictureInPictureControllerDidStopPictureInPicture(_ pictureInPictureController: AVPictureInPictureController) {
|
||||
didShowHide(false)
|
||||
}
|
||||
|
||||
deinit {
|
||||
pipController?.stopPictureInPicture()
|
||||
pipController?.canStartPictureInPictureAutomaticallyFromInline = false
|
||||
pipController?.contentSource = nil
|
||||
pipController?.delegate = nil
|
||||
pipController = nil
|
||||
}
|
||||
}
|
||||
|
||||
class SampleBufferVideoCallView: UIView {
|
||||
override class var layerClass: AnyClass {
|
||||
get { return AVSampleBufferDisplayLayer.self }
|
||||
}
|
||||
|
||||
var sampleBufferDisplayLayer: AVSampleBufferDisplayLayer {
|
||||
return layer as! AVSampleBufferDisplayLayer
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,11 +150,14 @@ struct CallViewLocal: UIViewRepresentable {
|
||||
var client: WebRTCClient
|
||||
var activeCall: Binding<WebRTCClient.Call?>
|
||||
var localRendererAspectRatio: Binding<CGFloat?>
|
||||
@State var pipStateChanged: (Bool) -> Void = {_ in }
|
||||
@Binding var pipShown: Bool
|
||||
|
||||
init(client: WebRTCClient, activeCall: Binding<WebRTCClient.Call?>, localRendererAspectRatio: Binding<CGFloat?>) {
|
||||
init(client: WebRTCClient, activeCall: Binding<WebRTCClient.Call?>, localRendererAspectRatio: Binding<CGFloat?>, pipShown: Binding<Bool>) {
|
||||
self.client = client
|
||||
self.activeCall = activeCall
|
||||
self.localRendererAspectRatio = localRendererAspectRatio
|
||||
self._pipShown = pipShown
|
||||
}
|
||||
|
||||
func makeUIView(context: Context) -> UIView {
|
||||
@@ -50,12 +167,18 @@ struct CallViewLocal: UIViewRepresentable {
|
||||
client.addLocalRenderer(call, localRenderer)
|
||||
client.startCaptureLocalVideo(call)
|
||||
addSubviewAndResize(localRenderer, into: view)
|
||||
DispatchQueue.main.async {
|
||||
pipStateChanged = { shown in
|
||||
localRenderer.isHidden = shown
|
||||
}
|
||||
}
|
||||
}
|
||||
return view
|
||||
}
|
||||
|
||||
func updateUIView(_ view: UIView, context: Context) {
|
||||
logger.debug("CallView.updateUIView local")
|
||||
pipStateChanged(pipShown)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
|
||||
import Foundation
|
||||
import AVFoundation
|
||||
import UIKit
|
||||
|
||||
class SoundPlayer {
|
||||
static let shared = SoundPlayer()
|
||||
@@ -43,3 +44,63 @@ class SoundPlayer {
|
||||
audioPlayer = nil
|
||||
}
|
||||
}
|
||||
|
||||
class CallSoundsPlayer {
|
||||
static let shared = CallSoundsPlayer()
|
||||
private var audioPlayer: AVAudioPlayer?
|
||||
private var playerTask: Task = Task {}
|
||||
|
||||
private func start(_ soundName: String, delayMs: Double) {
|
||||
audioPlayer?.stop()
|
||||
playerTask.cancel()
|
||||
logger.debug("start \(soundName)")
|
||||
guard let path = Bundle.main.path(forResource: soundName, ofType: "mp3", inDirectory: "sounds") else {
|
||||
logger.debug("start: file not found")
|
||||
return
|
||||
}
|
||||
do {
|
||||
let player = try AVAudioPlayer(contentsOf: URL(fileURLWithPath: path))
|
||||
if player.prepareToPlay() {
|
||||
audioPlayer = player
|
||||
}
|
||||
} catch {
|
||||
logger.debug("start: AVAudioPlayer error \(error.localizedDescription)")
|
||||
}
|
||||
|
||||
playerTask = Task {
|
||||
while let player = audioPlayer {
|
||||
player.play()
|
||||
do {
|
||||
try await Task.sleep(nanoseconds: UInt64((player.duration * 1_000_000_000) + delayMs * 1_000_000))
|
||||
} catch {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func startConnectingCallSound() {
|
||||
start("connecting_call", delayMs: 0)
|
||||
}
|
||||
|
||||
func startInCallSound() {
|
||||
// Taken from https://github.com/TelegramOrg/Telegram-Android
|
||||
// https://github.com/TelegramOrg/Telegram-Android/blob/master/LICENSE
|
||||
start("in_call", delayMs: 1000)
|
||||
}
|
||||
|
||||
func stop() {
|
||||
playerTask.cancel()
|
||||
audioPlayer?.stop()
|
||||
audioPlayer = nil
|
||||
}
|
||||
|
||||
func vibrate(long: Bool) {
|
||||
// iOS just don't want to vibrate more than once after a short period of time, and all 'styles' feel the same
|
||||
if long {
|
||||
AudioServicesPlayAlertSound(kSystemSoundID_Vibrate)
|
||||
} else {
|
||||
UIImpactFeedbackGenerator(style: .heavy).impactOccurred()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,6 +28,7 @@ class Call: ObservableObject, Equatable {
|
||||
@Published var speakerEnabled = false
|
||||
@Published var videoEnabled: Bool
|
||||
@Published var connectionInfo: ConnectionInfo?
|
||||
@Published var connectedAt: Date? = nil
|
||||
|
||||
init(
|
||||
direction: CallDirection,
|
||||
@@ -59,6 +60,7 @@ class Call: ObservableObject, Equatable {
|
||||
}
|
||||
}
|
||||
var hasMedia: Bool { get { callState == .offerSent || callState == .negotiated || callState == .connected } }
|
||||
var supportsVideo: Bool { get { peerMedia == .video || localMedia == .video } }
|
||||
}
|
||||
|
||||
enum CallDirection {
|
||||
@@ -429,17 +431,18 @@ struct RTCIceServer: Codable, Equatable {
|
||||
}
|
||||
|
||||
// the servers are expected in this format:
|
||||
// stun:stun.simplex.im:443?transport=tcp
|
||||
// turn:private:yleob6AVkiNI87hpR94Z@turn.simplex.im:443?transport=tcp
|
||||
// stuns:stun.simplex.im:443?transport=tcp
|
||||
// turns:private2:Hxuq2QxUjnhj96Zq2r4HjqHRj@turn.simplex.im:443?transport=tcp
|
||||
func parseRTCIceServer(_ str: String) -> RTCIceServer? {
|
||||
var s = replaceScheme(str, "stun:")
|
||||
s = replaceScheme(s, "stuns:")
|
||||
s = replaceScheme(s, "turn:")
|
||||
s = replaceScheme(s, "turns:")
|
||||
if let u: URL = URL(string: s),
|
||||
let scheme = u.scheme,
|
||||
let host = u.host,
|
||||
let port = u.port,
|
||||
u.path == "" && (scheme == "stun" || scheme == "turn" || scheme == "turns") {
|
||||
u.path == "" && (scheme == "stun" || scheme == "stuns" || scheme == "turn" || scheme == "turns") {
|
||||
let query = u.query == nil || u.query == "" ? "" : "?" + (u.query ?? "")
|
||||
return RTCIceServer(
|
||||
urls: ["\(scheme):\(host):\(port)\(query)"],
|
||||
|
||||
@@ -49,7 +49,7 @@ final class WebRTCClient: NSObject, RTCVideoViewDelegate, RTCFrameEncryptorDeleg
|
||||
}
|
||||
|
||||
private let rtcAudioSession = RTCAudioSession.sharedInstance()
|
||||
private let audioQueue = DispatchQueue(label: "audio")
|
||||
private let audioQueue = DispatchQueue(label: "chat.simplex.app.audio")
|
||||
private var sendCallResponse: (WVAPIMessage) async -> Void
|
||||
var activeCall: Binding<Call?>
|
||||
private var localRendererAspectRatio: Binding<CGFloat?>
|
||||
@@ -65,14 +65,14 @@ final class WebRTCClient: NSObject, RTCVideoViewDelegate, RTCFrameEncryptorDeleg
|
||||
self.localRendererAspectRatio = localRendererAspectRatio
|
||||
rtcAudioSession.useManualAudio = CallController.useCallKit()
|
||||
rtcAudioSession.isAudioEnabled = !CallController.useCallKit()
|
||||
logger.debug("WebRTCClient: rtcAudioSession has manual audio \(self.rtcAudioSession.useManualAudio) and audio enabled \(self.rtcAudioSession.isAudioEnabled)}")
|
||||
logger.debug("WebRTCClient: rtcAudioSession has manual audio \(self.rtcAudioSession.useManualAudio) and audio enabled \(self.rtcAudioSession.isAudioEnabled)")
|
||||
super.init()
|
||||
}
|
||||
|
||||
let defaultIceServers: [WebRTC.RTCIceServer] = [
|
||||
WebRTC.RTCIceServer(urlStrings: ["stun:stun.simplex.im:443"]),
|
||||
WebRTC.RTCIceServer(urlStrings: ["turn:turn.simplex.im:443?transport=udp"], username: "private", credential: "yleob6AVkiNI87hpR94Z"),
|
||||
WebRTC.RTCIceServer(urlStrings: ["turn:turn.simplex.im:443?transport=tcp"], username: "private", credential: "yleob6AVkiNI87hpR94Z"),
|
||||
WebRTC.RTCIceServer(urlStrings: ["stuns:stun.simplex.im:443"]),
|
||||
//WebRTC.RTCIceServer(urlStrings: ["turns:turn.simplex.im:443?transport=udp"], username: "private2", credential: "Hxuq2QxUjnhj96Zq2r4HjqHRj"),
|
||||
WebRTC.RTCIceServer(urlStrings: ["turns:turn.simplex.im:443?transport=tcp"], username: "private2", credential: "Hxuq2QxUjnhj96Zq2r4HjqHRj"),
|
||||
]
|
||||
|
||||
func initializeCall(_ iceServers: [WebRTC.RTCIceServer]?, _ mediaType: CallMediaType, _ aesKey: String?, _ relay: Bool?) -> Call {
|
||||
@@ -331,6 +331,10 @@ final class WebRTCClient: NSObject, RTCVideoViewDelegate, RTCFrameEncryptorDeleg
|
||||
activeCall.remoteStream?.add(renderer)
|
||||
}
|
||||
|
||||
func removeRemoteRenderer(_ activeCall: Call, _ renderer: RTCVideoRenderer) {
|
||||
activeCall.remoteStream?.remove(renderer)
|
||||
}
|
||||
|
||||
func startCaptureLocalVideo(_ activeCall: Call) {
|
||||
#if targetEnvironment(simulator)
|
||||
guard
|
||||
@@ -410,6 +414,7 @@ final class WebRTCClient: NSObject, RTCVideoViewDelegate, RTCFrameEncryptorDeleg
|
||||
guard let call = activeCall.wrappedValue else { return }
|
||||
logger.debug("WebRTCClient: ending the call")
|
||||
activeCall.wrappedValue = nil
|
||||
(call.localCamera as? RTCCameraVideoCapturer)?.stopCapture()
|
||||
call.connection.close()
|
||||
call.connection.delegate = nil
|
||||
call.frameEncryptor?.delegate = nil
|
||||
|
||||
@@ -103,6 +103,7 @@ struct ChatInfoView: View {
|
||||
@State private var sendReceipts = SendReceipts.userDefault(true)
|
||||
@State private var sendReceiptsUserDefault = true
|
||||
@AppStorage(DEFAULT_DEVELOPER_TOOLS) private var developerTools = false
|
||||
@AppStorage(GROUP_DEFAULT_PQ_EXPERIMENTAL_ENABLED, store: groupDefaults) private var pqExperimentalEnabled = false
|
||||
|
||||
enum ChatInfoViewAlert: Identifiable {
|
||||
case clearChatAlert
|
||||
@@ -110,6 +111,7 @@ struct ChatInfoView: View {
|
||||
case switchAddressAlert
|
||||
case abortSwitchAddressAlert
|
||||
case syncConnectionForceAlert
|
||||
case allowContactPQEncryptionAlert
|
||||
case error(title: LocalizedStringKey, error: LocalizedStringKey = "")
|
||||
|
||||
var id: String {
|
||||
@@ -119,6 +121,7 @@ struct ChatInfoView: View {
|
||||
case .switchAddressAlert: return "switchAddressAlert"
|
||||
case .abortSwitchAddressAlert: return "abortSwitchAddressAlert"
|
||||
case .syncConnectionForceAlert: return "syncConnectionForceAlert"
|
||||
case .allowContactPQEncryptionAlert: return "allowContactPQEncryptionAlert"
|
||||
case let .error(title, _): return "error \(title)"
|
||||
}
|
||||
}
|
||||
@@ -165,6 +168,22 @@ struct ChatInfoView: View {
|
||||
}
|
||||
.disabled(!contact.ready || !contact.active)
|
||||
|
||||
if pqExperimentalEnabled,
|
||||
let conn = contact.activeConn {
|
||||
Section {
|
||||
infoRow(Text(String("E2E encryption")), conn.connPQEnabled ? "Quantum resistant" : "Standard")
|
||||
if !conn.pqEncryption {
|
||||
allowPQButton()
|
||||
}
|
||||
} header: {
|
||||
Text(String("Quantum resistant E2E encryption"))
|
||||
} footer: {
|
||||
if !conn.pqEncryption {
|
||||
Text(String("After allowing quantum resistant encryption, it will be enabled after several messages if your contact also allows it."))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let contactLink = contact.contactLink {
|
||||
Section {
|
||||
SimpleXLinkQRCode(uri: contactLink)
|
||||
@@ -237,6 +256,7 @@ struct ChatInfoView: View {
|
||||
case .switchAddressAlert: return switchAddressAlert(switchContactAddress)
|
||||
case .abortSwitchAddressAlert: return abortSwitchAddressAlert(abortSwitchContactAddress)
|
||||
case .syncConnectionForceAlert: return syncConnectionForceAlert({ syncContactConnection(force: true) })
|
||||
case .allowContactPQEncryptionAlert: return allowContactPQEncryptionAlert()
|
||||
case let .error(title, error): return mkAlert(title: title, message: error)
|
||||
}
|
||||
}
|
||||
@@ -410,6 +430,15 @@ struct ChatInfoView: View {
|
||||
}
|
||||
}
|
||||
|
||||
private func allowPQButton() -> some View {
|
||||
Button {
|
||||
alert = .allowContactPQEncryptionAlert
|
||||
} label: {
|
||||
Label(String("Allow PQ encryption"), systemImage: "exclamationmark.triangle")
|
||||
.foregroundColor(.orange)
|
||||
}
|
||||
}
|
||||
|
||||
private func networkStatusRow() -> some View {
|
||||
HStack {
|
||||
Text("Network status")
|
||||
@@ -543,6 +572,34 @@ struct ChatInfoView: View {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func allowContactPQEncryption() {
|
||||
Task {
|
||||
do {
|
||||
let ct = try await apiSetContactPQ(contact.apiId, true)
|
||||
contact = ct
|
||||
await MainActor.run {
|
||||
chatModel.updateContact(ct)
|
||||
dismiss()
|
||||
}
|
||||
} catch let error {
|
||||
logger.error("allowContactPQEncryption apiSetContactPQ error: \(responseError(error))")
|
||||
let a = getErrorAlert(error, "Error allowing contact PQ encryption")
|
||||
await MainActor.run {
|
||||
alert = .error(title: a.title, error: a.message)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func allowContactPQEncryptionAlert() -> Alert {
|
||||
Alert(
|
||||
title: Text(String("Allow quantum resistant encryption?")),
|
||||
message: Text(String("This is an experimental feature, it is not recommended to enable it for important chats.")),
|
||||
primaryButton: .destructive(Text(String("Allow")), action: allowContactPQEncryption),
|
||||
secondaryButton: .cancel()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func switchAddressAlert(_ switchAddress: @escaping () -> Void) -> Alert {
|
||||
|
||||
@@ -11,6 +11,7 @@ import SimpleXChat
|
||||
|
||||
struct CIChatFeatureView: View {
|
||||
@EnvironmentObject var m: ChatModel
|
||||
@ObservedObject var chat: Chat
|
||||
var chatItem: ChatItem
|
||||
@Binding var revealed: Bool
|
||||
var feature: Feature
|
||||
@@ -18,7 +19,7 @@ struct CIChatFeatureView: View {
|
||||
var iconColor: Color
|
||||
|
||||
var body: some View {
|
||||
if !revealed, let fs = mergedFeautures() {
|
||||
if !revealed, let fs = mergedFeatures() {
|
||||
HStack {
|
||||
ForEach(fs, content: featureIconView)
|
||||
}
|
||||
@@ -47,7 +48,7 @@ struct CIChatFeatureView: View {
|
||||
}
|
||||
}
|
||||
|
||||
private func mergedFeautures() -> [FeatureInfo]? {
|
||||
private func mergedFeatures() -> [FeatureInfo]? {
|
||||
var fs: [FeatureInfo] = []
|
||||
var icons: Set<String> = []
|
||||
if var i = m.getChatItemIndex(chatItem) {
|
||||
@@ -67,8 +68,8 @@ struct CIChatFeatureView: View {
|
||||
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)
|
||||
case let .rcvGroupFeature(feature, preference, param, role): FeatureInfo(feature, preference.enabled(role, for: chat.chatInfo.groupInfo?.membership).iconColor, param)
|
||||
case let .sndGroupFeature(feature, preference, param, role): FeatureInfo(feature, preference.enabled(role, for: chat.chatInfo.groupInfo?.membership).iconColor, param)
|
||||
default: nil
|
||||
}
|
||||
}
|
||||
@@ -103,6 +104,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), revealed: Binding.constant(true), feature: ChatFeature.fullDelete, iconColor: enabled.iconColor)
|
||||
CIChatFeatureView(chat: Chat.sampleData, chatItem: ChatItem.getChatFeatureSample(.fullDelete, enabled), revealed: Binding.constant(true), feature: ChatFeature.fullDelete, iconColor: enabled.iconColor)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ struct CIEventView: View {
|
||||
.padding(.horizontal, 6)
|
||||
.padding(.vertical, 4)
|
||||
.textSelection(.disabled)
|
||||
.lineLimit(3)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -69,19 +69,12 @@ struct CIFileView: View {
|
||||
return false
|
||||
}
|
||||
|
||||
private func fileSizeValid() -> Bool {
|
||||
if let file = file {
|
||||
return file.fileSize <= getMaxFileSize(file.fileProtocol)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
private func fileAction() {
|
||||
logger.debug("CIFileView fileAction")
|
||||
if let file = file {
|
||||
switch (file.fileStatus) {
|
||||
case .rcvInvitation:
|
||||
if fileSizeValid() {
|
||||
if fileSizeValid(file) {
|
||||
Task {
|
||||
logger.debug("CIFileView fileAction - in .rcvInvitation, in Task")
|
||||
if let user = m.currentUser {
|
||||
@@ -143,7 +136,7 @@ struct CIFileView: View {
|
||||
case .sndCancelled: fileIcon("doc.fill", innerIcon: "xmark", innerIconSize: 10)
|
||||
case .sndError: fileIcon("doc.fill", innerIcon: "xmark", innerIconSize: 10)
|
||||
case .rcvInvitation:
|
||||
if fileSizeValid() {
|
||||
if fileSizeValid(file) {
|
||||
fileIcon("arrow.down.doc.fill", color: .accentColor)
|
||||
} else {
|
||||
fileIcon("doc.fill", color: .orange, innerIcon: "exclamationmark", innerIconSize: 12)
|
||||
@@ -201,6 +194,13 @@ struct CIFileView: View {
|
||||
}
|
||||
}
|
||||
|
||||
func fileSizeValid(_ file: CIFile?) -> Bool {
|
||||
if let file = file {
|
||||
return file.fileSize <= getMaxFileSize(file.fileProtocol)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func saveCryptoFile(_ fileSource: CryptoFile) {
|
||||
if let cfArgs = fileSource.cryptoArgs {
|
||||
let url = getAppFilePath(fileSource.filePath)
|
||||
|
||||
@@ -29,6 +29,9 @@ struct CIImageView: View {
|
||||
FullScreenMediaView(chatItem: chatItem, image: uiImage, showView: $showFullScreenImage, scrollProxy: scrollProxy)
|
||||
}
|
||||
.onTapGesture { showFullScreenImage = true }
|
||||
.onChange(of: m.activeCallViewIsCollapsed) { _ in
|
||||
showFullScreenImage = false
|
||||
}
|
||||
} else if let data = Data(base64Encoded: dropImagePrefix(image)),
|
||||
let uiImage = UIImage(data: data) {
|
||||
imageView(uiImage)
|
||||
@@ -67,14 +70,14 @@ struct CIImageView: View {
|
||||
}
|
||||
|
||||
private func imageView(_ img: UIImage) -> some View {
|
||||
let w = img.size.width <= img.size.height ? maxWidth * 0.75 : img.imageData == nil ? .infinity : maxWidth
|
||||
let w = img.size.width <= img.size.height ? maxWidth * 0.75 : maxWidth
|
||||
DispatchQueue.main.async { imgWidth = w }
|
||||
return ZStack(alignment: .topTrailing) {
|
||||
if img.imageData == nil {
|
||||
Image(uiImage: img)
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
.frame(maxWidth: w)
|
||||
.frame(width: w)
|
||||
} else {
|
||||
SwiftyGif(image: img)
|
||||
.frame(width: w, height: w * img.size.height / img.size.width)
|
||||
|
||||
@@ -120,10 +120,13 @@ struct CIVideoView: View {
|
||||
showFullScreenPlayer = urlDecrypted != nil
|
||||
}
|
||||
}
|
||||
.onChange(of: m.activeCallViewIsCollapsed) { _ in
|
||||
showFullScreenPlayer = false
|
||||
}
|
||||
if !decryptionInProgress {
|
||||
Button {
|
||||
decrypt(file: file) {
|
||||
if let decrypted = urlDecrypted {
|
||||
if urlDecrypted != nil {
|
||||
videoPlaying = true
|
||||
player?.play()
|
||||
}
|
||||
@@ -168,6 +171,9 @@ struct CIVideoView: View {
|
||||
default: ()
|
||||
}
|
||||
}
|
||||
.onChange(of: m.activeCallViewIsCollapsed) { _ in
|
||||
showFullScreenPlayer = false
|
||||
}
|
||||
if !videoPlaying {
|
||||
Button {
|
||||
m.stopPreviousRecPlay = url
|
||||
@@ -237,13 +243,13 @@ struct CIVideoView: View {
|
||||
}
|
||||
|
||||
private func imageView(_ img: UIImage) -> some View {
|
||||
let w = img.size.width <= img.size.height ? maxWidth * 0.75 : .infinity
|
||||
let w = img.size.width <= img.size.height ? maxWidth * 0.75 : maxWidth
|
||||
DispatchQueue.main.async { videoWidth = w }
|
||||
return ZStack(alignment: .topTrailing) {
|
||||
Image(uiImage: img)
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
.frame(maxWidth: w)
|
||||
.frame(width: w)
|
||||
loadingIndicator()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -65,6 +65,8 @@ struct FramedItemView: View {
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if let itemForwarded = chatItem.meta.itemForwarded {
|
||||
framedItemHeader(icon: "arrowshape.turn.up.forward", caption: Text(itemForwarded.text(chat.chatInfo.chatType)).italic(), pad: true)
|
||||
}
|
||||
|
||||
ChatItemContentView(chat: chat, chatItem: chatItem, revealed: $revealed, msgContentView: framedMsgContentView)
|
||||
@@ -163,7 +165,7 @@ struct FramedItemView: View {
|
||||
)
|
||||
}
|
||||
|
||||
@ViewBuilder func framedItemHeader(icon: String? = nil, caption: Text) -> some View {
|
||||
@ViewBuilder func framedItemHeader(icon: String? = nil, caption: Text, pad: Bool = false) -> some View {
|
||||
let v = HStack(spacing: 6) {
|
||||
if let icon = icon {
|
||||
Image(systemName: icon)
|
||||
@@ -178,7 +180,7 @@ struct FramedItemView: View {
|
||||
.foregroundColor(.secondary)
|
||||
.padding(.horizontal, 12)
|
||||
.padding(.top, 6)
|
||||
.padding(.bottom, chatItem.quotedItem == nil ? 6 : 0) // TODO think how to regroup
|
||||
.padding(.bottom, pad || (chatItem.quotedItem == nil && chatItem.meta.itemForwarded == nil) ? 6 : 0)
|
||||
.overlay(DetermineWidth())
|
||||
.frame(minWidth: msgWidth, alignment: .leading)
|
||||
.background(chatItemFrameContextColor(chatItem, colorScheme))
|
||||
@@ -253,6 +255,7 @@ struct FramedItemView: View {
|
||||
ciQuotedMsgTextView(qi, lines: 3)
|
||||
}
|
||||
}
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
.padding(.top, 6)
|
||||
.padding(.horizontal, 12)
|
||||
}
|
||||
@@ -352,9 +355,9 @@ private struct MetaColorPreferenceKey: PreferenceKey {
|
||||
|
||||
func onlyImageOrVideo(_ ci: ChatItem) -> Bool {
|
||||
if case let .image(text, _) = ci.content.msgContent {
|
||||
return ci.meta.itemDeleted == nil && !ci.meta.isLive && ci.quotedItem == nil && text == ""
|
||||
return ci.meta.itemDeleted == nil && !ci.meta.isLive && ci.quotedItem == nil && ci.meta.itemForwarded == nil && text == ""
|
||||
} else if case let .video(text, _, _) = ci.content.msgContent {
|
||||
return ci.meta.itemDeleted == nil && !ci.meta.isLive && ci.quotedItem == nil && text == ""
|
||||
return ci.meta.itemDeleted == nil && !ci.meta.isLive && ci.quotedItem == nil && ci.meta.itemForwarded == nil && text == ""
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -65,6 +65,8 @@ struct MarkedDeletedItemView: View {
|
||||
}
|
||||
}
|
||||
|
||||
// same texts are in markedDeletedText in ChatPreviewView, but it returns String;
|
||||
// can be refactored into a single function if functions calling these are changed to return same type
|
||||
var markedDeletedText: LocalizedStringKey {
|
||||
switch chatItem.meta.itemDeleted {
|
||||
case let .moderated(_, byGroupMember): "moderated by \(byGroupMember.displayName)"
|
||||
|
||||
@@ -0,0 +1,152 @@
|
||||
//
|
||||
// ChatItemForwardingView.swift
|
||||
// SimpleX (iOS)
|
||||
//
|
||||
// Created by spaced4ndy on 12.04.2024.
|
||||
// Copyright © 2024 SimpleX Chat. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import SimpleXChat
|
||||
|
||||
struct ChatItemForwardingView: View {
|
||||
@EnvironmentObject var chatModel: ChatModel
|
||||
@Environment(\.dismiss) var dismiss
|
||||
|
||||
var ci: ChatItem
|
||||
var fromChatInfo: ChatInfo
|
||||
@Binding var composeState: ComposeState
|
||||
|
||||
@State private var searchText: String = ""
|
||||
@FocusState private var searchFocused
|
||||
|
||||
var body: some View {
|
||||
NavigationView {
|
||||
forwardListView()
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .navigationBarLeading) {
|
||||
Button("Cancel") {
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
ToolbarItem(placement: .principal) {
|
||||
Text("Forward")
|
||||
.bold()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder private func forwardListView() -> some View {
|
||||
VStack(alignment: .leading) {
|
||||
let chatsToForwardTo = filterChatsToForwardTo()
|
||||
if !chatsToForwardTo.isEmpty {
|
||||
ScrollView {
|
||||
LazyVStack(alignment: .leading, spacing: 8) {
|
||||
searchFieldView(text: $searchText, focussed: $searchFocused)
|
||||
.padding(.leading, 2)
|
||||
let s = searchText.trimmingCharacters(in: .whitespaces).localizedLowercase
|
||||
let chats = s == "" ? chatsToForwardTo : chatsToForwardTo.filter { filterChatSearched($0, s) }
|
||||
ForEach(chats) { chat in
|
||||
Divider()
|
||||
forwardListNavLinkView(chat)
|
||||
.disabled(chatModel.deletedChats.contains(chat.chatInfo.id))
|
||||
}
|
||||
}
|
||||
.padding(.horizontal)
|
||||
.padding(.vertical, 8)
|
||||
.background(Color(uiColor: .systemBackground))
|
||||
.cornerRadius(12)
|
||||
.padding(.horizontal)
|
||||
}
|
||||
.background(Color(.systemGroupedBackground))
|
||||
} else {
|
||||
emptyList()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func filterChatsToForwardTo() -> [Chat] {
|
||||
var filteredChats = chatModel.chats.filter({ canForwardToChat($0) })
|
||||
if let index = filteredChats.firstIndex(where: { $0.chatInfo.chatType == .local }) {
|
||||
let privateNotes = filteredChats.remove(at: index)
|
||||
filteredChats.insert(privateNotes, at: 0)
|
||||
}
|
||||
return filteredChats
|
||||
}
|
||||
|
||||
private func filterChatSearched(_ chat: Chat, _ searchStr: String) -> Bool {
|
||||
let cInfo = chat.chatInfo
|
||||
return switch cInfo {
|
||||
case let .direct(contact):
|
||||
viewNameContains(cInfo, searchStr) ||
|
||||
contact.profile.displayName.localizedLowercase.contains(searchStr) ||
|
||||
contact.fullName.localizedLowercase.contains(searchStr)
|
||||
default:
|
||||
viewNameContains(cInfo, searchStr)
|
||||
}
|
||||
|
||||
func viewNameContains(_ cInfo: ChatInfo, _ s: String) -> Bool {
|
||||
cInfo.chatViewName.localizedLowercase.contains(s)
|
||||
}
|
||||
}
|
||||
|
||||
private func canForwardToChat(_ chat: Chat) -> Bool {
|
||||
switch chat.chatInfo {
|
||||
case let .direct(contact): contact.sendMsgEnabled && !contact.nextSendGrpInv
|
||||
case let .group(groupInfo): groupInfo.sendMsgEnabled
|
||||
case let .local(noteFolder): noteFolder.sendMsgEnabled
|
||||
case .contactRequest: false
|
||||
case .contactConnection: false
|
||||
case .invalidJSON: false
|
||||
}
|
||||
}
|
||||
|
||||
private func emptyList() -> some View {
|
||||
Text("No filtered chats")
|
||||
.foregroundColor(.secondary)
|
||||
.frame(maxWidth: .infinity)
|
||||
}
|
||||
|
||||
@ViewBuilder private func forwardListNavLinkView(_ chat: Chat) -> some View {
|
||||
Button {
|
||||
dismiss()
|
||||
if chat.id == fromChatInfo.id {
|
||||
composeState = ComposeState(
|
||||
message: composeState.message,
|
||||
preview: composeState.linkPreview != nil ? composeState.preview : .noPreview,
|
||||
contextItem: .forwardingItem(chatItem: ci, fromChatInfo: fromChatInfo)
|
||||
)
|
||||
} else {
|
||||
composeState = ComposeState.init(forwardingItem: ci, fromChatInfo: fromChatInfo)
|
||||
chatModel.chatId = chat.id
|
||||
}
|
||||
} label: {
|
||||
HStack {
|
||||
ChatInfoImage(chat: chat)
|
||||
.frame(width: 30, height: 30)
|
||||
.padding(.trailing, 2)
|
||||
Text(chat.chatInfo.chatViewName)
|
||||
.foregroundColor(.primary)
|
||||
.lineLimit(1)
|
||||
if chat.chatInfo.incognito {
|
||||
Spacer()
|
||||
Image(systemName: "theatermasks")
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
.frame(width: 22, height: 22)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
}
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
ChatItemForwardingView(
|
||||
ci: ChatItem.getSample(1, .directSnd, .now, "hello"),
|
||||
fromChatInfo: .direct(contact: Contact.sampleData),
|
||||
composeState: Binding.constant(ComposeState(message: "hello"))
|
||||
)
|
||||
}
|
||||
@@ -11,6 +11,7 @@ import SimpleXChat
|
||||
|
||||
struct ChatItemInfoView: View {
|
||||
@EnvironmentObject var chatModel: ChatModel
|
||||
@Environment(\.dismiss) var dismiss
|
||||
@Environment(\.colorScheme) var colorScheme
|
||||
var ci: ChatItem
|
||||
@Binding var chatItemInfo: ChatItemInfo?
|
||||
@@ -21,6 +22,7 @@ struct ChatItemInfoView: View {
|
||||
enum CIInfoTab {
|
||||
case history
|
||||
case quote
|
||||
case forwarded
|
||||
case delivery
|
||||
}
|
||||
|
||||
@@ -68,9 +70,20 @@ struct ChatItemInfoView: View {
|
||||
if ci.quotedItem != nil {
|
||||
numTabs += 1
|
||||
}
|
||||
if chatItemInfo?.forwardedFromChatItem != nil {
|
||||
numTabs += 1
|
||||
}
|
||||
return numTabs
|
||||
}
|
||||
|
||||
private var local: Bool {
|
||||
switch ci.chatDir {
|
||||
case .localSnd: true
|
||||
case .localRcv: true
|
||||
default: false
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder private func itemInfoView() -> some View {
|
||||
if numTabs > 1 {
|
||||
TabView(selection: $selection) {
|
||||
@@ -93,6 +106,13 @@ struct ChatItemInfoView: View {
|
||||
}
|
||||
.tag(CIInfoTab.quote)
|
||||
}
|
||||
if let forwardedFromItem = chatItemInfo?.forwardedFromChatItem {
|
||||
forwardedFromTab(forwardedFromItem)
|
||||
.tabItem {
|
||||
Label(local ? "Saved" : "Forwarded", systemImage: "arrowshape.turn.up.forward")
|
||||
}
|
||||
.tag(CIInfoTab.forwarded)
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
if chatItemInfo?.memberDeliveryStatuses != nil {
|
||||
@@ -275,6 +295,75 @@ struct ChatItemInfoView: View {
|
||||
: Color(uiColor: .tertiarySystemGroupedBackground)
|
||||
}
|
||||
|
||||
@ViewBuilder private func forwardedFromTab(_ forwardedFromItem: AChatItem) -> some View {
|
||||
ScrollView {
|
||||
VStack(alignment: .leading, spacing: 16) {
|
||||
details()
|
||||
Divider().padding(.vertical)
|
||||
Text(local ? "Saved from" : "Forwarded from")
|
||||
.font(.title2)
|
||||
.padding(.bottom, 4)
|
||||
forwardedFromView(forwardedFromItem)
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
.frame(maxHeight: .infinity, alignment: .top)
|
||||
}
|
||||
|
||||
private func forwardedFromView(_ forwardedFromItem: AChatItem) -> some View {
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
Button {
|
||||
Task {
|
||||
await MainActor.run {
|
||||
chatModel.chatId = forwardedFromItem.chatInfo.id
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
} label: {
|
||||
forwardedFromSender(forwardedFromItem)
|
||||
}
|
||||
|
||||
if !local {
|
||||
Divider().padding(.top, 32)
|
||||
Text("Recipient(s) can't see who this message is from.")
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder private func forwardedFromSender(_ forwardedFromItem: AChatItem) -> some View {
|
||||
HStack {
|
||||
ChatInfoImage(chat: Chat(chatInfo: forwardedFromItem.chatInfo))
|
||||
.frame(width: 48, height: 48)
|
||||
.padding(.trailing, 6)
|
||||
|
||||
if forwardedFromItem.chatItem.chatDir.sent {
|
||||
VStack(alignment: .leading) {
|
||||
Text("you")
|
||||
.italic()
|
||||
.foregroundColor(.primary)
|
||||
Text(forwardedFromItem.chatInfo.chatViewName)
|
||||
.foregroundColor(.secondary)
|
||||
.lineLimit(1)
|
||||
}
|
||||
} else if case let .groupRcv(groupMember) = forwardedFromItem.chatItem.chatDir {
|
||||
VStack(alignment: .leading) {
|
||||
Text(groupMember.chatViewName)
|
||||
.foregroundColor(.primary)
|
||||
.lineLimit(1)
|
||||
Text(forwardedFromItem.chatInfo.chatViewName)
|
||||
.foregroundColor(.secondary)
|
||||
.lineLimit(1)
|
||||
}
|
||||
} else {
|
||||
Text(forwardedFromItem.chatInfo.chatViewName)
|
||||
.foregroundColor(.primary)
|
||||
.lineLimit(1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder private func deliveryTab(_ memberDeliveryStatuses: [MemberDeliveryStatus]) -> some View {
|
||||
ScrollView {
|
||||
VStack(alignment: .leading, spacing: 16) {
|
||||
|
||||
@@ -46,7 +46,7 @@ struct ChatItemView: View {
|
||||
let ci = 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 {
|
||||
} else if ci.quotedItem == nil && ci.meta.itemForwarded == nil && ci.meta.itemDeleted == nil && !ci.meta.isLive {
|
||||
if let mc = ci.content.msgContent, mc.isText && isShortEmoji(ci.content.text) {
|
||||
EmojiItemView(chat: chat, chatItem: ci)
|
||||
} else if ci.content.text.isEmpty, case let .voice(_, duration) = ci.content.msgContent {
|
||||
@@ -102,14 +102,18 @@ struct ChatItemContentView<Content: View>: View {
|
||||
case let .rcvChatPreference(feature, allowed, param):
|
||||
CIFeaturePreferenceView(chat: chat, chatItem: chatItem, feature: feature, allowed: allowed, param: param)
|
||||
case let .sndChatPreference(feature, _, _):
|
||||
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)
|
||||
CIChatFeatureView(chat: chat, chatItem: chatItem, revealed: $revealed, feature: feature, icon: feature.icon, iconColor: .secondary)
|
||||
case let .rcvGroupFeature(feature, preference, _, role): chatFeatureView(feature, preference.enabled(role, for: chat.chatInfo.groupInfo?.membership).iconColor)
|
||||
case let .sndGroupFeature(feature, preference, _, role): chatFeatureView(feature, preference.enabled(role, for: chat.chatInfo.groupInfo?.membership).iconColor)
|
||||
case let .rcvChatFeatureRejected(feature): chatFeatureView(feature, .red)
|
||||
case let .rcvGroupFeatureRejected(feature): chatFeatureView(feature, .red)
|
||||
case .sndModerated: deletedItemView()
|
||||
case .rcvModerated: deletedItemView()
|
||||
case .rcvBlocked: deletedItemView()
|
||||
case let .sndDirectE2EEInfo(e2eeInfo): CIEventView(eventText: directE2EEInfoText(e2eeInfo))
|
||||
case let .rcvDirectE2EEInfo(e2eeInfo): CIEventView(eventText: directE2EEInfoText(e2eeInfo))
|
||||
case .sndGroupE2EEInfo: CIEventView(eventText: e2eeInfoNoPQText())
|
||||
case .rcvGroupE2EEInfo: CIEventView(eventText: e2eeInfoNoPQText())
|
||||
case let .invalidJSON(json): CIInvalidJSONView(json: json)
|
||||
}
|
||||
}
|
||||
@@ -145,7 +149,7 @@ struct ChatItemContentView<Content: View>: View {
|
||||
}
|
||||
|
||||
private func chatFeatureView(_ feature: Feature, _ iconColor: Color) -> some View {
|
||||
CIChatFeatureView(chatItem: chatItem, revealed: $revealed, feature: feature, iconColor: iconColor)
|
||||
CIChatFeatureView(chat: chat, chatItem: chatItem, revealed: $revealed, feature: feature, iconColor: iconColor)
|
||||
}
|
||||
|
||||
private var mergedGroupEventText: Text? {
|
||||
@@ -170,6 +174,22 @@ struct ChatItemContentView<Content: View>: View {
|
||||
Text(members)
|
||||
}
|
||||
}
|
||||
|
||||
private func directE2EEInfoText(_ info: E2EEInfo) -> Text {
|
||||
info.pqEnabled
|
||||
? Text("Messages, files and calls are protected by **quantum resistant e2e encryption** with perfect forward secrecy, repudiation and break-in recovery.")
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
.fontWeight(.light)
|
||||
: e2eeInfoNoPQText()
|
||||
}
|
||||
|
||||
private func e2eeInfoNoPQText() -> Text {
|
||||
Text("Messages, files and calls are protected by **end-to-end encryption** with perfect forward secrecy, repudiation and break-in recovery.")
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
.fontWeight(.light)
|
||||
}
|
||||
}
|
||||
|
||||
func chatEventText(_ text: Text) -> Text {
|
||||
|
||||
@@ -17,6 +17,7 @@ struct ChatView: View {
|
||||
@Environment(\.colorScheme) var colorScheme
|
||||
@Environment(\.dismiss) var dismiss
|
||||
@Environment(\.presentationMode) var presentationMode
|
||||
@Environment(\.scenePhase) var scenePhase
|
||||
@State @ObservedObject var chat: Chat
|
||||
@State private var showChatInfoSheet: Bool = false
|
||||
@State private var showAddMembersSheet: Bool = false
|
||||
@@ -82,7 +83,11 @@ struct ChatView: View {
|
||||
initChatView()
|
||||
}
|
||||
.onChange(of: chatModel.chatId) { cId in
|
||||
if cId != nil {
|
||||
showChatInfoSheet = false
|
||||
if let cId {
|
||||
if let c = chatModel.getChat(cId) {
|
||||
chat = c
|
||||
}
|
||||
initChatView()
|
||||
} else {
|
||||
dismiss()
|
||||
@@ -159,12 +164,17 @@ struct ChatView: View {
|
||||
switch cInfo {
|
||||
case let .direct(contact):
|
||||
HStack {
|
||||
if contact.allowsFeature(.calls) {
|
||||
callButton(contact, .audio, imageName: "phone")
|
||||
.disabled(!contact.ready || !contact.active)
|
||||
let callsPrefEnabled = contact.mergedPreferences.calls.enabled.forUser
|
||||
if callsPrefEnabled {
|
||||
if chatModel.activeCall == nil {
|
||||
callButton(contact, .audio, imageName: "phone")
|
||||
.disabled(!contact.ready || !contact.active)
|
||||
} else if let call = chatModel.activeCall, call.contact.id == cInfo.id {
|
||||
endCallButton(call)
|
||||
}
|
||||
}
|
||||
Menu {
|
||||
if contact.allowsFeature(.calls) {
|
||||
if callsPrefEnabled && chatModel.activeCall == nil {
|
||||
Button {
|
||||
CallController.shared.startCall(contact, .video)
|
||||
} label: {
|
||||
@@ -173,7 +183,7 @@ struct ChatView: View {
|
||||
.disabled(!contact.ready || !contact.active)
|
||||
}
|
||||
searchButton()
|
||||
toggleNtfsButton(chat)
|
||||
ToggleNtfsButton(chat: chat)
|
||||
.disabled(!contact.ready || !contact.active)
|
||||
} label: {
|
||||
Image(systemName: "ellipsis")
|
||||
@@ -202,7 +212,7 @@ struct ChatView: View {
|
||||
}
|
||||
Menu {
|
||||
searchButton()
|
||||
toggleNtfsButton(chat)
|
||||
ToggleNtfsButton(chat: chat)
|
||||
} label: {
|
||||
Image(systemName: "ellipsis")
|
||||
}
|
||||
@@ -229,7 +239,9 @@ struct ChatView: View {
|
||||
|
||||
private func initChatView() {
|
||||
let cInfo = chat.chatInfo
|
||||
if case let .direct(contact) = cInfo {
|
||||
// This check prevents the call to apiContactInfo after the app is suspended, and the database is closed.
|
||||
if case .active = scenePhase,
|
||||
case let .direct(contact) = cInfo {
|
||||
Task {
|
||||
do {
|
||||
let (stats, _) = try await apiContactInfo(chat.chatInfo.apiId)
|
||||
@@ -243,7 +255,8 @@ struct ChatView: View {
|
||||
}
|
||||
}
|
||||
}
|
||||
if chatModel.draftChatId == cInfo.id, let draft = chatModel.draft {
|
||||
if chatModel.draftChatId == cInfo.id && !composeState.forwarding,
|
||||
let draft = chatModel.draft {
|
||||
composeState = draft
|
||||
}
|
||||
if chat.chatStats.unreadChat {
|
||||
@@ -288,7 +301,7 @@ struct ChatView: View {
|
||||
}
|
||||
|
||||
private func voiceWithoutFrame(_ ci: ChatItem) -> Bool {
|
||||
ci.content.msgContent?.isVoice == true && ci.content.text.count == 0 && ci.quotedItem == nil
|
||||
ci.content.msgContent?.isVoice == true && ci.content.text.count == 0 && ci.quotedItem == nil && ci.meta.itemForwarded == nil
|
||||
}
|
||||
|
||||
private func chatItemsList() -> some View {
|
||||
@@ -334,8 +347,8 @@ struct ChatView: View {
|
||||
.onChange(of: searchText) { _ in
|
||||
loadChat(chat: chat, search: searchText)
|
||||
}
|
||||
.onChange(of: chatModel.chatId) { _ in
|
||||
if let chatId = chatModel.chatId, let c = chatModel.getChat(chatId) {
|
||||
.onChange(of: chatModel.chatId) { chatId in
|
||||
if let chatId, let c = chatModel.getChat(chatId) {
|
||||
chat = c
|
||||
showChatInfoSheet = false
|
||||
loadChat(chat: c)
|
||||
@@ -421,7 +434,19 @@ struct ChatView: View {
|
||||
Image(systemName: imageName)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private func endCallButton(_ call: Call) -> some View {
|
||||
Button {
|
||||
if let uuid = call.callkitUUID {
|
||||
CallController.shared.endCall(callUUID: uuid)
|
||||
} else {
|
||||
CallController.shared.endCall(call: call) {}
|
||||
}
|
||||
} label: {
|
||||
Image(systemName: "phone.down.fill").tint(.red)
|
||||
}
|
||||
}
|
||||
|
||||
private func searchButton() -> some View {
|
||||
Button {
|
||||
searchMode = true
|
||||
@@ -494,6 +519,7 @@ struct ChatView: View {
|
||||
chat: chat,
|
||||
chatItem: ci,
|
||||
maxWidth: maxWidth,
|
||||
itemWidth: maxWidth,
|
||||
composeState: $composeState,
|
||||
selectedMember: $selectedMember,
|
||||
chatView: self
|
||||
@@ -506,6 +532,7 @@ struct ChatView: View {
|
||||
@ObservedObject var chat: Chat
|
||||
var chatItem: ChatItem
|
||||
var maxWidth: CGFloat
|
||||
@State var itemWidth: CGFloat
|
||||
@Binding var composeState: ComposeState
|
||||
@Binding var selectedMember: GMember?
|
||||
var chatView: ChatView
|
||||
@@ -517,6 +544,7 @@ struct ChatView: View {
|
||||
@State private var revealed = false
|
||||
@State private var showChatItemInfoSheet: Bool = false
|
||||
@State private var chatItemInfo: ChatItemInfo?
|
||||
@State private var showForwardingSheet: Bool = false
|
||||
|
||||
@State private var allowMenu: Bool = true
|
||||
|
||||
@@ -540,7 +568,12 @@ struct ChatView: View {
|
||||
chatItemView(ci, nil, prev)
|
||||
}
|
||||
} else {
|
||||
chatItemView(chatItem, range, prevItem)
|
||||
// Switch branches just to work around context menu problem when 'revealed' changes but size of item isn't
|
||||
if revealed {
|
||||
chatItemView(chatItem, range, prevItem)
|
||||
} else {
|
||||
chatItemView(chatItem, range, prevItem)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -629,7 +662,7 @@ struct ChatView: View {
|
||||
playbackState: $playbackState,
|
||||
playbackTime: $playbackTime
|
||||
)
|
||||
.uiKitContextMenu(menu: uiMenu, allowMenu: $allowMenu)
|
||||
.uiKitContextMenu(hasImageOrVideo: ci.content.msgContent?.isImageOrVideo == true, maxWidth: maxWidth, itemWidth: $itemWidth, menu: uiMenu, allowMenu: $allowMenu)
|
||||
.accessibilityLabel("")
|
||||
if ci.content.msgContent != nil && (ci.meta.itemDeleted == nil || revealed) && ci.reactions.count > 0 {
|
||||
chatItemReactions(ci)
|
||||
@@ -640,7 +673,7 @@ struct ChatView: View {
|
||||
Button("Delete for me", role: .destructive) {
|
||||
deleteMessage(.cidmInternal)
|
||||
}
|
||||
if let di = deletingItem, di.meta.editable && !di.localNote {
|
||||
if let di = deletingItem, di.meta.deletable && !di.localNote {
|
||||
Button(broadcastDeleteButtonText, role: .destructive) {
|
||||
deleteMessage(.cidmBroadcast)
|
||||
}
|
||||
@@ -666,6 +699,14 @@ struct ChatView: View {
|
||||
}) {
|
||||
ChatItemInfoView(ci: ci, chatItemInfo: $chatItemInfo)
|
||||
}
|
||||
.sheet(isPresented: $showForwardingSheet) {
|
||||
if #available(iOS 16.0, *) {
|
||||
ChatItemForwardingView(ci: ci, fromChatInfo: chat.chatInfo, composeState: $composeState)
|
||||
.presentationDetents([.fraction(0.8)])
|
||||
} else {
|
||||
ChatItemForwardingView(ci: ci, fromChatInfo: chat.chatInfo, composeState: $composeState)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func showMemberImage(_ member: GroupMember, _ prevItem: ChatItem?) -> Bool {
|
||||
@@ -744,11 +785,20 @@ struct ChatView: View {
|
||||
} else {
|
||||
menu.append(saveFileAction(fileSource))
|
||||
}
|
||||
} else if let file = ci.file, case .rcvInvitation = file.fileStatus, fileSizeValid(file) {
|
||||
menu.append(downloadFileAction(file))
|
||||
}
|
||||
if ci.meta.editable && !mc.isVoice && !live {
|
||||
menu.append(editAction(ci))
|
||||
}
|
||||
menu.append(viewInfoUIAction(ci))
|
||||
if ci.meta.itemDeleted == nil
|
||||
&& (ci.file == nil || (fileSource != nil && fileExists))
|
||||
&& !ci.isLiveDummy && !live {
|
||||
menu.append(forwardUIAction(ci))
|
||||
}
|
||||
if !ci.isLiveDummy {
|
||||
menu.append(viewInfoUIAction(ci))
|
||||
}
|
||||
if revealed {
|
||||
menu.append(hideUIAction())
|
||||
}
|
||||
@@ -797,6 +847,15 @@ struct ChatView: View {
|
||||
}
|
||||
}
|
||||
|
||||
private func forwardUIAction(_ ci: ChatItem) -> UIAction {
|
||||
UIAction(
|
||||
title: NSLocalizedString("Forward", comment: "chat item action"),
|
||||
image: UIImage(systemName: "arrowshape.turn.up.forward")
|
||||
) { _ in
|
||||
showForwardingSheet = true
|
||||
}
|
||||
}
|
||||
|
||||
private func reactionUIMenuPreiOS16(_ rs: [UIAction]) -> UIMenu {
|
||||
UIMenu(
|
||||
title: NSLocalizedString("React…", comment: "chat item menu"),
|
||||
@@ -894,7 +953,21 @@ struct ChatView: View {
|
||||
saveCryptoFile(fileSource)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private func downloadFileAction(_ file: CIFile) -> UIAction {
|
||||
UIAction(
|
||||
title: NSLocalizedString("Download", comment: "chat item action"),
|
||||
image: UIImage(systemName: "arrow.down.doc")
|
||||
) { _ in
|
||||
Task {
|
||||
logger.debug("ChatView downloadFileAction, in Task")
|
||||
if let user = m.currentUser {
|
||||
await receiveFile(user: user, fileId: file.fileId)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func editAction(_ ci: ChatItem) -> UIAction {
|
||||
UIAction(
|
||||
title: NSLocalizedString("Edit", comment: "chat item action"),
|
||||
@@ -1143,14 +1216,18 @@ struct ChatView: View {
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder func toggleNtfsButton(_ chat: Chat) -> some View {
|
||||
Button {
|
||||
toggleNotifications(chat, enableNtfs: !chat.chatInfo.ntfsEnabled)
|
||||
} label: {
|
||||
if chat.chatInfo.ntfsEnabled {
|
||||
Label("Mute", systemImage: "speaker.slash")
|
||||
} else {
|
||||
Label("Unmute", systemImage: "speaker.wave.2")
|
||||
struct ToggleNtfsButton: View {
|
||||
@ObservedObject var chat: Chat
|
||||
|
||||
var body: some View {
|
||||
Button {
|
||||
toggleNotifications(chat, enableNtfs: !chat.chatInfo.ntfsEnabled)
|
||||
} label: {
|
||||
if chat.chatInfo.ntfsEnabled {
|
||||
Label("Mute", systemImage: "speaker.slash")
|
||||
} else {
|
||||
Label("Unmute", systemImage: "speaker.wave.2")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ enum ComposeContextItem {
|
||||
case noContextItem
|
||||
case quotedItem(chatItem: ChatItem)
|
||||
case editingItem(chatItem: ChatItem)
|
||||
case forwardingItem(chatItem: ChatItem, fromChatInfo: ChatInfo)
|
||||
}
|
||||
|
||||
enum VoiceMessageRecordingState {
|
||||
@@ -72,6 +73,13 @@ struct ComposeState {
|
||||
}
|
||||
}
|
||||
|
||||
init(forwardingItem: ChatItem, fromChatInfo: ChatInfo) {
|
||||
self.message = ""
|
||||
self.preview = .noPreview
|
||||
self.contextItem = .forwardingItem(chatItem: forwardingItem, fromChatInfo: fromChatInfo)
|
||||
self.voiceMessageRecordingState = .noRecording
|
||||
}
|
||||
|
||||
func copy(
|
||||
message: String? = nil,
|
||||
liveMessage: LiveMessage? = nil,
|
||||
@@ -102,12 +110,19 @@ struct ComposeState {
|
||||
}
|
||||
}
|
||||
|
||||
var forwarding: Bool {
|
||||
switch contextItem {
|
||||
case .forwardingItem: return true
|
||||
default: return false
|
||||
}
|
||||
}
|
||||
|
||||
var sendEnabled: Bool {
|
||||
switch preview {
|
||||
case let .mediaPreviews(media): return !media.isEmpty
|
||||
case .voicePreview: return voiceMessageRecordingState == .finished
|
||||
case .filePreview: return true
|
||||
default: return !message.isEmpty || liveMessage != nil
|
||||
default: return !message.isEmpty || forwarding || liveMessage != nil
|
||||
}
|
||||
}
|
||||
|
||||
@@ -153,7 +168,7 @@ struct ComposeState {
|
||||
}
|
||||
|
||||
var attachmentDisabled: Bool {
|
||||
if editing || liveMessage != nil || inProgress { return true }
|
||||
if editing || forwarding || liveMessage != nil || inProgress { return true }
|
||||
switch preview {
|
||||
case .noPreview: return false
|
||||
case .linkPreview: return false
|
||||
@@ -161,6 +176,16 @@ struct ComposeState {
|
||||
}
|
||||
}
|
||||
|
||||
var attachmentPreview: Bool {
|
||||
switch preview {
|
||||
case .noPreview: false
|
||||
case .linkPreview: false
|
||||
case let .mediaPreviews(mediaPreviews): !mediaPreviews.isEmpty
|
||||
case .voicePreview: false
|
||||
case .filePreview: true
|
||||
}
|
||||
}
|
||||
|
||||
var empty: Bool {
|
||||
message == "" && noPreview
|
||||
}
|
||||
@@ -234,6 +259,7 @@ struct ComposeView: View {
|
||||
@Binding var keyboardVisible: Bool
|
||||
|
||||
@State var linkUrl: URL? = nil
|
||||
@State var hasSimplexLink: Bool = false
|
||||
@State var prevLinkUrl: URL? = nil
|
||||
@State var pendingLinkUrl: URL? = nil
|
||||
@State var cancelledLinks: Set<String> = []
|
||||
@@ -260,6 +286,16 @@ struct ComposeView: View {
|
||||
if chat.chatInfo.contact?.nextSendGrpInv ?? false {
|
||||
ContextInvitingContactMemberView()
|
||||
}
|
||||
let simplexLinkProhibited = hasSimplexLink && !chat.groupFeatureEnabled(.simplexLinks)
|
||||
let fileProhibited = composeState.attachmentPreview && !chat.groupFeatureEnabled(.files)
|
||||
let voiceProhibited = composeState.voicePreview && !chat.chatInfo.featureEnabled(.voice)
|
||||
if simplexLinkProhibited {
|
||||
msgNotAllowedView("SimpleX links not allowed", icon: "link")
|
||||
} else if fileProhibited {
|
||||
msgNotAllowedView("Files and media not allowed", icon: "doc")
|
||||
} else if voiceProhibited {
|
||||
msgNotAllowedView("Voice messages not allowed", icon: "mic")
|
||||
}
|
||||
contextItemView()
|
||||
switch (composeState.editing, composeState.preview) {
|
||||
case (true, .filePreview): EmptyView()
|
||||
@@ -278,7 +314,7 @@ struct ComposeView: View {
|
||||
.padding(.bottom, 12)
|
||||
.padding(.leading, 12)
|
||||
if case let .group(g) = chat.chatInfo,
|
||||
!g.fullGroupPreferences.files.on {
|
||||
!g.fullGroupPreferences.files.on(for: g.membership) {
|
||||
b.disabled(true).onTapGesture {
|
||||
AlertManager.shared.showAlertMsg(
|
||||
title: "Files and media prohibited!",
|
||||
@@ -303,6 +339,7 @@ struct ComposeView: View {
|
||||
},
|
||||
nextSendGrpInv: chat.chatInfo.contact?.nextSendGrpInv ?? false,
|
||||
voiceMessageAllowed: chat.chatInfo.featureEnabled(.voice),
|
||||
disableSendButton: simplexLinkProhibited || fileProhibited || voiceProhibited,
|
||||
showEnableVoiceMessagesAlert: chat.chatInfo.showEnableVoiceMessagesAlert,
|
||||
startVoiceMessageRecording: {
|
||||
Task {
|
||||
@@ -337,13 +374,18 @@ struct ComposeView: View {
|
||||
}
|
||||
}
|
||||
}
|
||||
.onChange(of: composeState.message) { _ in
|
||||
.onChange(of: composeState.message) { msg in
|
||||
if composeState.linkPreviewAllowed {
|
||||
if composeState.message.count > 0 {
|
||||
showLinkPreview(composeState.message)
|
||||
if msg.count > 0 {
|
||||
showLinkPreview(msg)
|
||||
} else {
|
||||
resetLinkPreview()
|
||||
hasSimplexLink = false
|
||||
}
|
||||
} else if msg.count > 0 && !chat.groupFeatureEnabled(.simplexLinks) {
|
||||
(_, hasSimplexLink) = parseMessage(msg)
|
||||
} else {
|
||||
hasSimplexLink = false
|
||||
}
|
||||
}
|
||||
.onChange(of: chat.userCanSend) { canSend in
|
||||
@@ -610,6 +652,18 @@ struct ComposeView: View {
|
||||
}
|
||||
}
|
||||
|
||||
private func msgNotAllowedView(_ reason: LocalizedStringKey, icon: String) -> some View {
|
||||
HStack {
|
||||
Image(systemName: icon).foregroundColor(.secondary)
|
||||
Text(reason).italic()
|
||||
}
|
||||
.padding(12)
|
||||
.frame(minHeight: 50)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.background(Color(uiColor: .tertiarySystemGroupedBackground))
|
||||
.padding(.top, 8)
|
||||
}
|
||||
|
||||
@ViewBuilder private func contextItemView() -> some View {
|
||||
switch composeState.contextItem {
|
||||
case .noContextItem:
|
||||
@@ -628,6 +682,14 @@ struct ComposeView: View {
|
||||
contextIcon: "pencil",
|
||||
cancelContextItem: { clearState() }
|
||||
)
|
||||
case let .forwardingItem(chatItem: forwardedItem, _):
|
||||
ContextItemView(
|
||||
chat: chat,
|
||||
contextItem: forwardedItem,
|
||||
contextIcon: "arrowshape.turn.up.forward",
|
||||
cancelContextItem: { composeState = composeState.copy(contextItem: .noContextItem) },
|
||||
showSender: false
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -649,6 +711,11 @@ struct ComposeView: View {
|
||||
}
|
||||
if chat.chatInfo.contact?.nextSendGrpInv ?? false {
|
||||
await sendMemberContactInvitation()
|
||||
} else if case let .forwardingItem(ci, fromChatInfo) = composeState.contextItem {
|
||||
sent = await forwardItem(ci, fromChatInfo)
|
||||
if !composeState.message.isEmpty {
|
||||
sent = await send(checkLinkPreview(), quoted: sent?.id, live: false, ttl: nil)
|
||||
}
|
||||
} else if case let .editingItem(ci) = composeState.contextItem {
|
||||
sent = await updateMessage(ci, live: live)
|
||||
} else if let liveMessage = liveMessage, liveMessage.sentMsg != nil {
|
||||
@@ -694,7 +761,15 @@ struct ComposeView: View {
|
||||
}
|
||||
}
|
||||
}
|
||||
await MainActor.run { clearState(live: live) }
|
||||
await MainActor.run {
|
||||
let wasForwarding = composeState.forwarding
|
||||
clearState(live: live)
|
||||
if wasForwarding,
|
||||
chatModel.draftChatId == chat.chatInfo.id,
|
||||
let draft = chatModel.draft {
|
||||
composeState = draft
|
||||
}
|
||||
}
|
||||
return sent
|
||||
|
||||
func sending() async {
|
||||
@@ -815,10 +890,26 @@ struct ComposeView: View {
|
||||
return nil
|
||||
}
|
||||
|
||||
func forwardItem(_ forwardedItem: ChatItem, _ fromChatInfo: ChatInfo) async -> ChatItem? {
|
||||
if let chatItem = await apiForwardChatItem(
|
||||
toChatType: chat.chatInfo.chatType,
|
||||
toChatId: chat.chatInfo.apiId,
|
||||
fromChatType: fromChatInfo.chatType,
|
||||
fromChatId: fromChatInfo.apiId,
|
||||
itemId: forwardedItem.id
|
||||
) {
|
||||
await MainActor.run {
|
||||
chatModel.addChatItem(chat.chatInfo, chatItem)
|
||||
}
|
||||
return chatItem
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func checkLinkPreview() -> MsgContent {
|
||||
switch (composeState.preview) {
|
||||
case let .linkPreview(linkPreview: linkPreview):
|
||||
if let url = parseMessage(msgText),
|
||||
if let url = parseMessage(msgText).url,
|
||||
let linkPreview = linkPreview,
|
||||
url == linkPreview.uri {
|
||||
return .link(text: msgText, preview: linkPreview)
|
||||
@@ -947,7 +1038,7 @@ struct ComposeView: View {
|
||||
|
||||
private func showLinkPreview(_ s: String) {
|
||||
prevLinkUrl = linkUrl
|
||||
linkUrl = parseMessage(s)
|
||||
(linkUrl, hasSimplexLink) = parseMessage(s)
|
||||
if let url = linkUrl {
|
||||
if url != composeState.linkPreview?.uri && url != pendingLinkUrl {
|
||||
pendingLinkUrl = url
|
||||
@@ -964,13 +1055,17 @@ struct ComposeView: View {
|
||||
}
|
||||
}
|
||||
|
||||
private func parseMessage(_ msg: String) -> URL? {
|
||||
let parsedMsg = parseSimpleXMarkdown(msg)
|
||||
let uri = parsedMsg?.first(where: { ft in
|
||||
private func parseMessage(_ msg: String) -> (url: URL?, hasSimplexLink: Bool) {
|
||||
guard let parsedMsg = parseSimpleXMarkdown(msg) else { return (nil, false) }
|
||||
let url: URL? = if let uri = parsedMsg.first(where: { ft in
|
||||
ft.format == .uri && !cancelledLinks.contains(ft.text) && !isSimplexLink(ft.text)
|
||||
})
|
||||
if let uri = uri { return URL(string: uri.text) }
|
||||
else { return nil }
|
||||
}) {
|
||||
URL(string: uri.text)
|
||||
} else {
|
||||
nil
|
||||
}
|
||||
let simplexLink = parsedMsg.contains(where: { ft in ft.format?.isSimplexLink ?? false })
|
||||
return (url, simplexLink)
|
||||
}
|
||||
|
||||
private func isSimplexLink(_ link: String) -> Bool {
|
||||
@@ -978,6 +1073,9 @@ struct ComposeView: View {
|
||||
}
|
||||
|
||||
private func cancelLinkPreview() {
|
||||
if let pendingLink = pendingLinkUrl?.absoluteString {
|
||||
cancelledLinks.insert(pendingLink)
|
||||
}
|
||||
if let uri = composeState.linkPreview?.uri.absoluteString {
|
||||
cancelledLinks.insert(uri)
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ struct ContextItemView: View {
|
||||
let contextItem: ChatItem
|
||||
let contextIcon: String
|
||||
let cancelContextItem: () -> Void
|
||||
var showSender: Bool = true
|
||||
|
||||
var body: some View {
|
||||
HStack {
|
||||
@@ -23,7 +24,7 @@ struct ContextItemView: View {
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(width: 16, height: 16)
|
||||
.foregroundColor(.secondary)
|
||||
if let sender = contextItem.memberDisplayName {
|
||||
if showSender, let sender = contextItem.memberDisplayName {
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
Text(sender).font(.caption).foregroundColor(.secondary)
|
||||
msgContentView(lines: 2)
|
||||
@@ -48,14 +49,26 @@ struct ContextItemView: View {
|
||||
}
|
||||
|
||||
private func msgContentView(lines: Int) -> some View {
|
||||
MsgContentView(
|
||||
chat: chat,
|
||||
text: contextItem.text,
|
||||
formattedText: contextItem.formattedText,
|
||||
showSecrets: false
|
||||
)
|
||||
.multilineTextAlignment(isRightToLeft(contextItem.text) ? .trailing : .leading)
|
||||
.lineLimit(lines)
|
||||
contextMsgPreview()
|
||||
.multilineTextAlignment(isRightToLeft(contextItem.text) ? .trailing : .leading)
|
||||
.lineLimit(lines)
|
||||
}
|
||||
|
||||
private func contextMsgPreview() -> Text {
|
||||
return attachment() + messageText(contextItem.text, contextItem.formattedText, nil, preview: true, showSecrets: false)
|
||||
|
||||
func attachment() -> Text {
|
||||
switch contextItem.content.msgContent {
|
||||
case .file: return image("doc.fill")
|
||||
case .image: return image("photo")
|
||||
case .voice: return image("play.fill")
|
||||
default: return Text("")
|
||||
}
|
||||
}
|
||||
|
||||
func image(_ s: String) -> Text {
|
||||
Text(Image(systemName: s)).foregroundColor(Color(uiColor: .tertiaryLabel)) + Text(" ")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -16,7 +16,6 @@ struct NativeTextEditor: UIViewRepresentable {
|
||||
@Binding var disableEditing: Bool
|
||||
@Binding var height: CGFloat
|
||||
@Binding var focused: Bool
|
||||
let alignment: TextAlignment
|
||||
let onImagesAdded: ([UploadContent]) -> Void
|
||||
|
||||
private let minHeight: CGFloat = 37
|
||||
@@ -30,13 +29,16 @@ struct NativeTextEditor: UIViewRepresentable {
|
||||
func makeUIView(context: Context) -> UITextView {
|
||||
let field = CustomUITextField(height: _height)
|
||||
field.text = text
|
||||
field.textAlignment = alignment == .leading ? .left : .right
|
||||
field.textAlignment = alignment(text)
|
||||
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
|
||||
field.textAlignment = alignment(text)
|
||||
updateFont(field)
|
||||
// Speed up the process of updating layout, reduce jumping content on screen
|
||||
updateHeight(field)
|
||||
self.height = field.frame.size.height
|
||||
} else {
|
||||
field.text = text
|
||||
}
|
||||
@@ -53,10 +55,12 @@ struct NativeTextEditor: UIViewRepresentable {
|
||||
}
|
||||
|
||||
func updateUIView(_ field: UITextView, context: Context) {
|
||||
field.text = text
|
||||
field.textAlignment = alignment == .leading ? .left : .right
|
||||
updateFont(field)
|
||||
updateHeight(field)
|
||||
if field.markedTextRange == nil && field.text != text {
|
||||
field.text = text
|
||||
field.textAlignment = alignment(text)
|
||||
updateFont(field)
|
||||
updateHeight(field)
|
||||
}
|
||||
}
|
||||
|
||||
private func updateHeight(_ field: UITextView) {
|
||||
@@ -73,12 +77,19 @@ struct NativeTextEditor: UIViewRepresentable {
|
||||
}
|
||||
|
||||
private func updateFont(_ field: UITextView) {
|
||||
field.font = isShortEmoji(field.text)
|
||||
let newFont = isShortEmoji(field.text)
|
||||
? (field.text.count < 4 ? largeEmojiUIFont : mediumEmojiUIFont)
|
||||
: UIFont.preferredFont(forTextStyle: .body)
|
||||
if field.font != newFont {
|
||||
field.font = newFont
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func alignment(_ text: String) -> NSTextAlignment {
|
||||
isRightToLeft(text) ? .right : .left
|
||||
}
|
||||
|
||||
private class CustomUITextField: UITextView, UITextViewDelegate {
|
||||
var height: Binding<CGFloat>
|
||||
var newHeight: CGFloat = 0
|
||||
@@ -205,7 +216,6 @@ struct NativeTextEditor_Previews: PreviewProvider{
|
||||
disableEditing: Binding.constant(false),
|
||||
height: Binding.constant(100),
|
||||
focused: Binding.constant(false),
|
||||
alignment: TextAlignment.leading,
|
||||
onImagesAdded: { _ in }
|
||||
)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
|
||||
@@ -20,6 +20,7 @@ struct SendMessageView: View {
|
||||
var nextSendGrpInv: Bool = false
|
||||
var showVoiceMessageButton: Bool = true
|
||||
var voiceMessageAllowed: Bool = true
|
||||
var disableSendButton = false
|
||||
var showEnableVoiceMessagesAlert: ChatInfo.ShowEnableVoiceMessagesAlert = .other
|
||||
var startVoiceMessageRecording: (() -> Void)? = nil
|
||||
var finishVoiceMessageRecording: (() -> Void)? = nil
|
||||
@@ -53,13 +54,11 @@ struct SendMessageView: View {
|
||||
.padding(.vertical, 8)
|
||||
.frame(maxWidth: .infinity)
|
||||
} else {
|
||||
let alignment: TextAlignment = isRightToLeft(composeState.message) ? .trailing : .leading
|
||||
NativeTextEditor(
|
||||
text: $composeState.message,
|
||||
disableEditing: $composeState.inProgress,
|
||||
height: $teHeight,
|
||||
focused: $keyboardVisible,
|
||||
alignment: alignment,
|
||||
onImagesAdded: onMediaAdded
|
||||
)
|
||||
.allowsTightening(false)
|
||||
@@ -109,6 +108,7 @@ struct SendMessageView: View {
|
||||
} else if showVoiceMessageButton
|
||||
&& composeState.message.isEmpty
|
||||
&& !composeState.editing
|
||||
&& !composeState.forwarding
|
||||
&& composeState.liveMessage == nil
|
||||
&& ((composeState.noPreview && vmrs == .noRecording)
|
||||
|| (vmrs == .recording && holdingVMR)) {
|
||||
@@ -184,7 +184,8 @@ struct SendMessageView: View {
|
||||
!composeState.sendEnabled ||
|
||||
composeState.inProgress ||
|
||||
(!voiceMessageAllowed && composeState.voicePreview) ||
|
||||
composeState.endLiveDisabled
|
||||
composeState.endLiveDisabled ||
|
||||
disableSendButton
|
||||
)
|
||||
.frame(width: 29, height: 29)
|
||||
.contextMenu{
|
||||
@@ -223,7 +224,8 @@ struct SendMessageView: View {
|
||||
|
||||
@ViewBuilder private func sendButtonContextMenuItems() -> some View {
|
||||
if composeState.liveMessage == nil,
|
||||
!composeState.editing {
|
||||
!composeState.editing,
|
||||
!composeState.forwarding {
|
||||
if case .noContextItem = composeState.contextItem,
|
||||
!composeState.voicePreview,
|
||||
let send = sendLiveMessage,
|
||||
|
||||
@@ -35,7 +35,7 @@ struct ContactPreferencesView: View {
|
||||
.disabled(currentFeaturesAllowed == featuresAllowed)
|
||||
}
|
||||
}
|
||||
.modifier(BackButton {
|
||||
.modifier(BackButton(disabled: Binding.constant(false)) {
|
||||
if currentFeaturesAllowed == featuresAllowed {
|
||||
dismiss()
|
||||
} else {
|
||||
|
||||
@@ -62,7 +62,7 @@ struct GroupChatInfoView: View {
|
||||
NavigationView {
|
||||
let members = chatModel.groupMembers
|
||||
.filter { m in let status = m.wrapped.memberStatus; return status != .memLeft && status != .memRemoved }
|
||||
.sorted { $0.displayName.lowercased() < $1.displayName.lowercased() }
|
||||
.sorted { $0.wrapped.memberRole > $1.wrapped.memberRole }
|
||||
|
||||
List {
|
||||
groupInfoHeader()
|
||||
@@ -234,39 +234,29 @@ struct GroupChatInfoView: View {
|
||||
Spacer()
|
||||
memberInfo(member)
|
||||
}
|
||||
|
||||
// revert from this:
|
||||
|
||||
if user {
|
||||
v
|
||||
} else if member.canBeRemoved(groupInfo: groupInfo) {
|
||||
removeSwipe(member, blockSwipe(member, v))
|
||||
} else if groupInfo.membership.memberRole >= .admin {
|
||||
// TODO if there are more actions, refactor with lists of swipeActions
|
||||
let canBlockForAll = member.canBlockForAll(groupInfo: groupInfo)
|
||||
let canRemove = member.canBeRemoved(groupInfo: groupInfo)
|
||||
if canBlockForAll && canRemove {
|
||||
removeSwipe(member, blockForAllSwipe(member, v))
|
||||
} else if canBlockForAll {
|
||||
blockForAllSwipe(member, v)
|
||||
} else if canRemove {
|
||||
removeSwipe(member, v)
|
||||
} else {
|
||||
v
|
||||
}
|
||||
} else {
|
||||
blockSwipe(member, v)
|
||||
if !member.blockedByAdmin {
|
||||
blockSwipe(member, v)
|
||||
} else {
|
||||
v
|
||||
}
|
||||
}
|
||||
// revert to this: vvv
|
||||
// if user {
|
||||
// v
|
||||
// } else if groupInfo.membership.memberRole >= .admin {
|
||||
// // TODO if there are more actions, refactor with lists of swipeActions
|
||||
// let canBlockForAll = member.canBlockForAll(groupInfo: groupInfo)
|
||||
// let canRemove = member.canBeRemoved(groupInfo: groupInfo)
|
||||
// if canBlockForAll && canRemove {
|
||||
// removeSwipe(member, blockForAllSwipe(member, v))
|
||||
// } else if canBlockForAll {
|
||||
// blockForAllSwipe(member, v)
|
||||
// } else if canRemove {
|
||||
// removeSwipe(member, v)
|
||||
// } else {
|
||||
// v
|
||||
// }
|
||||
// } else {
|
||||
// if !member.blockedByAdmin {
|
||||
// blockSwipe(member, v)
|
||||
// } else {
|
||||
// v
|
||||
// }
|
||||
// }
|
||||
// ^^^
|
||||
}
|
||||
|
||||
@ViewBuilder private func memberInfo(_ member: GroupMember) -> some View {
|
||||
@@ -370,7 +360,11 @@ struct GroupChatInfoView: View {
|
||||
|
||||
private func addOrEditWelcomeMessage() -> some View {
|
||||
NavigationLink {
|
||||
GroupWelcomeView(groupId: groupInfo.groupId, groupInfo: $groupInfo)
|
||||
GroupWelcomeView(
|
||||
groupInfo: $groupInfo,
|
||||
groupProfile: groupInfo.groupProfile,
|
||||
welcomeText: groupInfo.groupProfile.description ?? ""
|
||||
)
|
||||
.navigationTitle("Welcome message")
|
||||
.navigationBarTitleDisplayMode(.large)
|
||||
} label: {
|
||||
|
||||
@@ -83,7 +83,7 @@ struct GroupMemberInfoView: View {
|
||||
Section {
|
||||
if let contactId = member.memberContactId, let chat = knownDirectChat(contactId) {
|
||||
knownDirectChatButton(chat)
|
||||
} else if groupInfo.fullGroupPreferences.directMessages.on {
|
||||
} else if groupInfo.fullGroupPreferences.directMessages.on(for: groupInfo.membership) {
|
||||
if let contactId = member.memberContactId {
|
||||
newDirectChatButton(contactId)
|
||||
} else if member.activeConn?.peerChatVRange.isCompatibleRange(CREATE_MEMBER_CONTACT_VRANGE) ?? false {
|
||||
@@ -110,7 +110,7 @@ struct GroupMemberInfoView: View {
|
||||
Label("Share address", systemImage: "square.and.arrow.up")
|
||||
}
|
||||
if let contactId = member.memberContactId {
|
||||
if knownDirectChat(contactId) == nil && !groupInfo.fullGroupPreferences.directMessages.on {
|
||||
if knownDirectChat(contactId) == nil && !groupInfo.fullGroupPreferences.directMessages.on(for: groupInfo.membership) {
|
||||
connectViaAddressButton(contactLink)
|
||||
}
|
||||
} else {
|
||||
@@ -168,24 +168,11 @@ struct GroupMemberInfoView: View {
|
||||
}
|
||||
}
|
||||
|
||||
// revert from this:
|
||||
Section {
|
||||
if member.memberSettings.showMessages {
|
||||
blockMemberButton(member)
|
||||
} else {
|
||||
unblockMemberButton(member)
|
||||
}
|
||||
if member.canBeRemoved(groupInfo: groupInfo) {
|
||||
removeMemberButton(member)
|
||||
}
|
||||
if groupInfo.membership.memberRole >= .admin {
|
||||
adminDestructiveSection(member)
|
||||
} else {
|
||||
nonAdminBlockSection(member)
|
||||
}
|
||||
// revert to this: vvv
|
||||
// if groupInfo.membership.memberRole >= .admin {
|
||||
// adminDestructiveSection(member)
|
||||
// } else {
|
||||
// nonAdminBlockSection(member)
|
||||
// }
|
||||
// ^^^
|
||||
|
||||
if developerTools {
|
||||
Section("For console") {
|
||||
|
||||
@@ -9,6 +9,12 @@
|
||||
import SwiftUI
|
||||
import SimpleXChat
|
||||
|
||||
private let featureRoles: [(role: GroupMemberRole?, text: LocalizedStringKey)] = [
|
||||
(nil, "all members"),
|
||||
(.admin, "admins"),
|
||||
(.owner, "owners")
|
||||
]
|
||||
|
||||
struct GroupPreferencesView: View {
|
||||
@Environment(\.dismiss) var dismiss: DismissAction
|
||||
@EnvironmentObject var chatModel: ChatModel
|
||||
@@ -24,10 +30,12 @@ struct GroupPreferencesView: View {
|
||||
List {
|
||||
featureSection(.timedMessages, $preferences.timedMessages.enable)
|
||||
featureSection(.fullDelete, $preferences.fullDelete.enable)
|
||||
featureSection(.directMessages, $preferences.directMessages.enable)
|
||||
featureSection(.directMessages, $preferences.directMessages.enable, $preferences.directMessages.role)
|
||||
featureSection(.reactions, $preferences.reactions.enable)
|
||||
featureSection(.voice, $preferences.voice.enable)
|
||||
featureSection(.files, $preferences.files.enable)
|
||||
featureSection(.voice, $preferences.voice.enable, $preferences.voice.role)
|
||||
featureSection(.files, $preferences.files.enable, $preferences.files.role)
|
||||
// TODO enable simplexLinks preference in 5.8
|
||||
// featureSection(.simplexLinks, $preferences.simplexLinks.enable, $preferences.simplexLinks.role)
|
||||
featureSection(.history, $preferences.history.enable)
|
||||
|
||||
if groupInfo.canEdit {
|
||||
@@ -48,7 +56,7 @@ struct GroupPreferencesView: View {
|
||||
preferences.timedMessages.ttl = currentPreferences.timedMessages.ttl
|
||||
}
|
||||
}
|
||||
.modifier(BackButton {
|
||||
.modifier(BackButton(disabled: Binding.constant(false)) {
|
||||
if currentPreferences == preferences {
|
||||
dismiss()
|
||||
} else {
|
||||
@@ -64,7 +72,7 @@ struct GroupPreferencesView: View {
|
||||
}
|
||||
}
|
||||
|
||||
private func featureSection(_ feature: GroupFeature, _ enableFeature: Binding<GroupFeatureEnabled>) -> some View {
|
||||
private func featureSection(_ feature: GroupFeature, _ enableFeature: Binding<GroupFeatureEnabled>, _ enableForRole: Binding<GroupMemberRole?>? = nil) -> some View {
|
||||
Section {
|
||||
let color: Color = enableFeature.wrappedValue == .on ? .green : .secondary
|
||||
let icon = enableFeature.wrappedValue == .on ? feature.iconFilled : feature.icon
|
||||
@@ -87,6 +95,16 @@ struct GroupPreferencesView: View {
|
||||
)
|
||||
.frame(height: 36)
|
||||
}
|
||||
if enableFeature.wrappedValue == .on, let enableForRole {
|
||||
Picker("Enabled for", selection: enableForRole) {
|
||||
ForEach(featureRoles, id: \.role) { fr in
|
||||
Text(fr.text)
|
||||
}
|
||||
}
|
||||
.frame(height: 36)
|
||||
// remove in v5.8
|
||||
.disabled(true)
|
||||
}
|
||||
} else {
|
||||
settingsRow(icon, color: color) {
|
||||
infoRow(Text(feature.text), enableFeature.wrappedValue.text)
|
||||
@@ -94,10 +112,26 @@ struct GroupPreferencesView: View {
|
||||
if timedOn {
|
||||
infoRow("Delete after", timeText(preferences.timedMessages.ttl))
|
||||
}
|
||||
if enableFeature.wrappedValue == .on, let enableForRole {
|
||||
HStack {
|
||||
Text("Enabled for").foregroundColor(.secondary)
|
||||
Spacer()
|
||||
Text(
|
||||
featureRoles.first(where: { fr in fr.role == enableForRole.wrappedValue })?.text
|
||||
?? "all members"
|
||||
)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
}
|
||||
}
|
||||
} footer: {
|
||||
Text(feature.enableDescription(enableFeature.wrappedValue, groupInfo.canEdit))
|
||||
}
|
||||
.onChange(of: enableFeature.wrappedValue) { enabled in
|
||||
if case .off = enabled {
|
||||
enableForRole?.wrappedValue = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func savePreferences() {
|
||||
|
||||
@@ -11,29 +11,32 @@ import SimpleXChat
|
||||
|
||||
struct GroupWelcomeView: View {
|
||||
@Environment(\.dismiss) var dismiss: DismissAction
|
||||
@EnvironmentObject private var m: ChatModel
|
||||
var groupId: Int64
|
||||
@Binding var groupInfo: GroupInfo
|
||||
@State private var welcomeText: String = ""
|
||||
@State var groupProfile: GroupProfile
|
||||
@State var welcomeText: String
|
||||
@State private var editMode = true
|
||||
@FocusState private var keyboardVisible: Bool
|
||||
@State private var showSaveDialog = false
|
||||
|
||||
let maxByteCount = 1200
|
||||
|
||||
var body: some View {
|
||||
VStack {
|
||||
if groupInfo.canEdit {
|
||||
editorView()
|
||||
.modifier(BackButton {
|
||||
if welcomeText == groupInfo.groupProfile.description || (welcomeText == "" && groupInfo.groupProfile.description == nil) {
|
||||
.modifier(BackButton(disabled: Binding.constant(false)) {
|
||||
if welcomeTextUnchanged() {
|
||||
dismiss()
|
||||
} else {
|
||||
showSaveDialog = true
|
||||
}
|
||||
})
|
||||
.confirmationDialog("Save welcome message?", isPresented: $showSaveDialog) {
|
||||
Button("Save and update group profile") {
|
||||
save()
|
||||
dismiss()
|
||||
.confirmationDialog(
|
||||
welcomeTextFitsLimit() ? "Save welcome message?" : "Welcome message is too long",
|
||||
isPresented: $showSaveDialog
|
||||
) {
|
||||
if welcomeTextFitsLimit() {
|
||||
Button("Save and update group profile") { save() }
|
||||
}
|
||||
Button("Exit without saving") { dismiss() }
|
||||
}
|
||||
@@ -47,14 +50,15 @@ struct GroupWelcomeView: View {
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
welcomeText = groupInfo.groupProfile.description ?? ""
|
||||
keyboardVisible = true
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
|
||||
keyboardVisible = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func textPreview() -> some View {
|
||||
messageText(welcomeText, parseSimpleXMarkdown(welcomeText), nil, showSecrets: false)
|
||||
.frame(minHeight: 140, alignment: .topLeading)
|
||||
.frame(minHeight: 130, alignment: .topLeading)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
}
|
||||
|
||||
@@ -74,7 +78,7 @@ struct GroupWelcomeView: View {
|
||||
}
|
||||
.padding(.horizontal, -5)
|
||||
.padding(.top, -8)
|
||||
.frame(height: 140, alignment: .topLeading)
|
||||
.frame(height: 130, alignment: .topLeading)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
}
|
||||
} else {
|
||||
@@ -93,6 +97,9 @@ struct GroupWelcomeView: View {
|
||||
}
|
||||
.disabled(welcomeText.isEmpty)
|
||||
copyButton()
|
||||
} footer: {
|
||||
Text(!welcomeTextFitsLimit() ? "Message too large" : "")
|
||||
.foregroundColor(.red)
|
||||
}
|
||||
|
||||
Section {
|
||||
@@ -113,7 +120,15 @@ struct GroupWelcomeView: View {
|
||||
Button("Save and update group profile") {
|
||||
save()
|
||||
}
|
||||
.disabled(welcomeText == groupInfo.groupProfile.description || (welcomeText == "" && groupInfo.groupProfile.description == nil))
|
||||
.disabled(welcomeTextUnchanged() || !welcomeTextFitsLimit())
|
||||
}
|
||||
|
||||
private func welcomeTextUnchanged() -> Bool {
|
||||
welcomeText == groupInfo.groupProfile.description || (welcomeText == "" && groupInfo.groupProfile.description == nil)
|
||||
}
|
||||
|
||||
private func welcomeTextFitsLimit() -> Bool {
|
||||
chatJsonLength(welcomeText) <= maxByteCount
|
||||
}
|
||||
|
||||
private func save() {
|
||||
@@ -123,11 +138,13 @@ struct GroupWelcomeView: View {
|
||||
if welcome?.count == 0 {
|
||||
welcome = nil
|
||||
}
|
||||
var groupProfileUpdated = groupInfo.groupProfile
|
||||
groupProfileUpdated.description = welcome
|
||||
groupInfo = try await apiUpdateGroup(groupId, groupProfileUpdated)
|
||||
m.updateGroup(groupInfo)
|
||||
welcomeText = welcome ?? ""
|
||||
groupProfile.description = welcome
|
||||
let gInfo = try await apiUpdateGroup(groupInfo.groupId, groupProfile)
|
||||
await MainActor.run {
|
||||
groupInfo = gInfo
|
||||
ChatModel.shared.updateGroup(gInfo)
|
||||
dismiss()
|
||||
}
|
||||
} catch let error {
|
||||
logger.error("apiUpdateGroup error: \(responseError(error))")
|
||||
}
|
||||
@@ -137,6 +154,6 @@ struct GroupWelcomeView: View {
|
||||
|
||||
struct GroupWelcomeView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
GroupWelcomeView(groupId: 1, groupInfo: Binding.constant(GroupInfo.sampleData))
|
||||
GroupProfileView(groupInfo: Binding.constant(GroupInfo.sampleData), groupProfile: GroupProfile.sampleData)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -92,7 +92,7 @@ struct ChatListNavLink: View {
|
||||
.swipeActions(edge: .leading, allowsFullSwipe: true) {
|
||||
markReadButton()
|
||||
toggleFavoriteButton()
|
||||
toggleNtfsButton(chat)
|
||||
ToggleNtfsButton(chat: chat)
|
||||
}
|
||||
.swipeActions(edge: .trailing, allowsFullSwipe: true) {
|
||||
if !chat.chatItems.isEmpty {
|
||||
@@ -181,7 +181,7 @@ struct ChatListNavLink: View {
|
||||
.swipeActions(edge: .leading, allowsFullSwipe: true) {
|
||||
markReadButton()
|
||||
toggleFavoriteButton()
|
||||
toggleNtfsButton(chat)
|
||||
ToggleNtfsButton(chat: chat)
|
||||
}
|
||||
.swipeActions(edge: .trailing, allowsFullSwipe: true) {
|
||||
if !chat.chatItems.isEmpty {
|
||||
|
||||
@@ -264,7 +264,9 @@ struct ChatListView: View {
|
||||
}
|
||||
|
||||
func filtered(_ chat: Chat) -> Bool {
|
||||
(chat.chatInfo.chatSettings?.favorite ?? false) || chat.chatStats.unreadCount > 0 || chat.chatStats.unreadChat
|
||||
(chat.chatInfo.chatSettings?.favorite ?? false) ||
|
||||
chat.chatStats.unreadChat ||
|
||||
(chat.chatInfo.ntfsEnabled && chat.chatStats.unreadCount > 0)
|
||||
}
|
||||
|
||||
func viewNameContains(_ cInfo: ChatInfo, _ s: String) -> Bool {
|
||||
|
||||
@@ -34,7 +34,7 @@ struct ChatPreviewView: View {
|
||||
HStack(alignment: .top) {
|
||||
chatPreviewTitle()
|
||||
Spacer()
|
||||
(cItem?.timestampText ?? formatTimestampText(chat.chatInfo.updatedAt))
|
||||
(cItem?.timestampText ?? formatTimestampText(chat.chatInfo.chatTs))
|
||||
.font(.subheadline)
|
||||
.frame(minWidth: 60, alignment: .trailing)
|
||||
.foregroundColor(.secondary)
|
||||
@@ -171,10 +171,21 @@ struct ChatPreviewView: View {
|
||||
}
|
||||
|
||||
func chatItemPreview(_ cItem: ChatItem) -> Text {
|
||||
let itemText = cItem.meta.itemDeleted == nil ? cItem.text : NSLocalizedString("marked deleted", comment: "marked deleted chat item preview text")
|
||||
let itemText = cItem.meta.itemDeleted == nil ? cItem.text : markedDeletedText()
|
||||
let itemFormattedText = cItem.meta.itemDeleted == nil ? cItem.formattedText : nil
|
||||
return messageText(itemText, itemFormattedText, cItem.memberDisplayName, icon: attachment(), preview: true, showSecrets: false)
|
||||
|
||||
// same texts are in markedDeletedText in MarkedDeletedItemView, but it returns LocalizedStringKey;
|
||||
// can be refactored into a single function if functions calling these are changed to return same type
|
||||
func markedDeletedText() -> String {
|
||||
switch cItem.meta.itemDeleted {
|
||||
case let .moderated(_, byGroupMember): String.localizedStringWithFormat(NSLocalizedString("moderated by %@", comment: "marked deleted chat item preview text"), byGroupMember.displayName)
|
||||
case .blocked: NSLocalizedString("blocked", comment: "marked deleted chat item preview text")
|
||||
case .blockedByAdmin: NSLocalizedString("blocked by admin", comment: "marked deleted chat item preview text")
|
||||
case .deleted, nil: NSLocalizedString("marked deleted", comment: "marked deleted chat item preview text")
|
||||
}
|
||||
}
|
||||
|
||||
func attachment() -> String? {
|
||||
switch cItem.content.msgContent {
|
||||
case .file: return "doc.fill"
|
||||
|
||||
@@ -12,6 +12,7 @@ private let fillColorLight = Color(uiColor: UIColor(red: 0.99, green: 0.99, blue
|
||||
struct UserPicker: View {
|
||||
@EnvironmentObject var m: ChatModel
|
||||
@Environment(\.colorScheme) var colorScheme
|
||||
@Environment(\.scenePhase) var scenePhase
|
||||
@Binding var showSettings: Bool
|
||||
@Binding var showConnectDesktop: Bool
|
||||
@Binding var userPickerVisible: Bool
|
||||
@@ -91,7 +92,10 @@ struct UserPicker: View {
|
||||
.opacity(userPickerVisible ? 1.0 : 0.0)
|
||||
.onAppear {
|
||||
do {
|
||||
m.users = try listUsers()
|
||||
// This check prevents the call of listUsers after the app is suspended, and the database is closed.
|
||||
if case .active = scenePhase {
|
||||
m.users = try listUsers()
|
||||
}
|
||||
} catch let error {
|
||||
logger.error("Error loading users \(responseError(error))")
|
||||
}
|
||||
|
||||
@@ -36,6 +36,7 @@ enum DatabaseEncryptionAlert: Identifiable {
|
||||
struct DatabaseEncryptionView: View {
|
||||
@EnvironmentObject private var m: ChatModel
|
||||
@Binding var useKeychain: Bool
|
||||
var migration: Bool
|
||||
@State private var alert: DatabaseEncryptionAlert? = nil
|
||||
@State private var progressIndicator = false
|
||||
@State private var useKeychainToggle = storeDBPassphraseGroupDefault.get()
|
||||
@@ -48,7 +49,12 @@ struct DatabaseEncryptionView: View {
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
databaseEncryptionView()
|
||||
List {
|
||||
if migration {
|
||||
chatStoppedView()
|
||||
}
|
||||
databaseEncryptionView()
|
||||
}
|
||||
if progressIndicator {
|
||||
ProgressView().scaleEffect(2)
|
||||
}
|
||||
@@ -56,72 +62,71 @@ struct DatabaseEncryptionView: View {
|
||||
}
|
||||
|
||||
private func databaseEncryptionView() -> some View {
|
||||
List {
|
||||
Section {
|
||||
settingsRow(storedKey ? "key.fill" : "key", color: storedKey ? .green : .secondary) {
|
||||
Toggle("Save passphrase in Keychain", isOn: $useKeychainToggle)
|
||||
Section {
|
||||
settingsRow(storedKey ? "key.fill" : "key", color: storedKey ? .green : .secondary) {
|
||||
Toggle("Save passphrase in Keychain", isOn: $useKeychainToggle)
|
||||
.onChange(of: useKeychainToggle) { _ in
|
||||
if useKeychainToggle {
|
||||
setUseKeychain(true)
|
||||
} else if storedKey {
|
||||
} else if storedKey && !migration {
|
||||
// Don't show in migration process since it will remove the key after successfull encryption
|
||||
alert = .keychainRemoveKey
|
||||
} else {
|
||||
setUseKeychain(false)
|
||||
}
|
||||
}
|
||||
.disabled(initialRandomDBPassphrase)
|
||||
}
|
||||
.disabled(initialRandomDBPassphrase && !migration)
|
||||
}
|
||||
|
||||
if !initialRandomDBPassphrase && m.chatDbEncrypted == true {
|
||||
PassphraseField(key: $currentKey, placeholder: "Current passphrase…", valid: validKey(currentKey))
|
||||
}
|
||||
if !initialRandomDBPassphrase && m.chatDbEncrypted == true {
|
||||
PassphraseField(key: $currentKey, placeholder: "Current passphrase…", valid: validKey(currentKey))
|
||||
}
|
||||
|
||||
PassphraseField(key: $newKey, placeholder: "New passphrase…", valid: validKey(newKey), showStrength: true)
|
||||
PassphraseField(key: $confirmNewKey, placeholder: "Confirm new passphrase…", valid: confirmNewKey == "" || newKey == confirmNewKey)
|
||||
PassphraseField(key: $newKey, placeholder: "New passphrase…", valid: validKey(newKey), showStrength: true)
|
||||
PassphraseField(key: $confirmNewKey, placeholder: "Confirm new passphrase…", valid: confirmNewKey == "" || newKey == confirmNewKey)
|
||||
|
||||
settingsRow("lock.rotation") {
|
||||
Button("Update database passphrase") {
|
||||
alert = currentKey == ""
|
||||
? (useKeychain ? .encryptDatabaseSaved : .encryptDatabase)
|
||||
: (useKeychain ? .changeDatabaseKeySaved : .changeDatabaseKey)
|
||||
}
|
||||
settingsRow("lock.rotation") {
|
||||
Button(migration ? "Set passphrase" : "Update database passphrase") {
|
||||
alert = currentKey == ""
|
||||
? (useKeychain ? .encryptDatabaseSaved : .encryptDatabase)
|
||||
: (useKeychain ? .changeDatabaseKeySaved : .changeDatabaseKey)
|
||||
}
|
||||
.disabled(
|
||||
(m.chatDbEncrypted == true && currentKey == "") ||
|
||||
currentKey == newKey ||
|
||||
newKey != confirmNewKey ||
|
||||
newKey == "" ||
|
||||
!validKey(currentKey) ||
|
||||
!validKey(newKey)
|
||||
)
|
||||
} header: {
|
||||
Text("")
|
||||
} footer: {
|
||||
VStack(alignment: .leading, spacing: 16) {
|
||||
if m.chatDbEncrypted == false {
|
||||
Text("Your chat database is not encrypted - set passphrase to encrypt it.")
|
||||
} else if useKeychain {
|
||||
if storedKey {
|
||||
Text("iOS Keychain is used to securely store passphrase - it allows receiving push notifications.")
|
||||
if initialRandomDBPassphrase {
|
||||
Text("Database is encrypted using a random passphrase, you can change it.")
|
||||
} else {
|
||||
Text("**Please note**: you will NOT be able to recover or change passphrase if you lose it.")
|
||||
}
|
||||
}
|
||||
.disabled(
|
||||
(m.chatDbEncrypted == true && currentKey == "") ||
|
||||
currentKey == newKey ||
|
||||
newKey != confirmNewKey ||
|
||||
newKey == "" ||
|
||||
!validKey(currentKey) ||
|
||||
!validKey(newKey)
|
||||
)
|
||||
} header: {
|
||||
Text(migration ? "Database passphrase" : "")
|
||||
} footer: {
|
||||
VStack(alignment: .leading, spacing: 16) {
|
||||
if m.chatDbEncrypted == false {
|
||||
Text("Your chat database is not encrypted - set passphrase to encrypt it.")
|
||||
} else if useKeychain {
|
||||
if storedKey {
|
||||
Text("iOS Keychain is used to securely store passphrase - it allows receiving push notifications.")
|
||||
if initialRandomDBPassphrase && !migration {
|
||||
Text("Database is encrypted using a random passphrase, you can change it.")
|
||||
} else {
|
||||
Text("iOS Keychain will be used to securely store passphrase after you restart the app or change passphrase - it will allow receiving push notifications.")
|
||||
Text("**Please note**: you will NOT be able to recover or change passphrase if you lose it.")
|
||||
}
|
||||
} else {
|
||||
Text("You have to enter passphrase every time the app starts - it is not stored on the device.")
|
||||
Text("**Please note**: you will NOT be able to recover or change passphrase if you lose it.")
|
||||
if m.notificationMode == .instant && m.notificationPreview != .hidden {
|
||||
Text("**Warning**: Instant push notifications require passphrase saved in Keychain.")
|
||||
}
|
||||
Text("iOS Keychain will be used to securely store passphrase after you restart the app or change passphrase - it will allow receiving push notifications.")
|
||||
}
|
||||
} else {
|
||||
Text("You have to enter passphrase every time the app starts - it is not stored on the device.")
|
||||
Text("**Please note**: you will NOT be able to recover or change passphrase if you lose it.")
|
||||
if m.notificationMode == .instant && m.notificationPreview != .hidden && !migration {
|
||||
Text("**Warning**: Instant push notifications require passphrase saved in Keychain.")
|
||||
}
|
||||
}
|
||||
.padding(.top, 1)
|
||||
.font(.callout)
|
||||
}
|
||||
.padding(.top, 1)
|
||||
.font(.callout)
|
||||
}
|
||||
.onAppear {
|
||||
if initialRandomDBPassphrase { currentKey = kcDatabasePassword.get() ?? "" }
|
||||
@@ -136,9 +141,15 @@ struct DatabaseEncryptionView: View {
|
||||
do {
|
||||
encryptionStartedDefault.set(true)
|
||||
encryptionStartedAtDefault.set(Date.now)
|
||||
if !m.chatDbChanged {
|
||||
try apiSaveAppSettings(settings: AppSettings.current.prepareForExport())
|
||||
}
|
||||
try await apiStorageEncryption(currentKey: currentKey, newKey: newKey)
|
||||
encryptionStartedDefault.set(false)
|
||||
initialRandomDBPassphraseGroupDefault.set(false)
|
||||
if migration {
|
||||
storeDBPassphraseGroupDefault.set(useKeychain)
|
||||
}
|
||||
if useKeychain {
|
||||
if kcDatabasePassword.set(newKey) {
|
||||
await resetFormAfterEncryption(true)
|
||||
@@ -148,6 +159,9 @@ struct DatabaseEncryptionView: View {
|
||||
await operationEnded(.error(title: "Keychain error", error: "Error saving passphrase to keychain"))
|
||||
}
|
||||
} else {
|
||||
if migration {
|
||||
removePassphraseFromKeyChain()
|
||||
}
|
||||
await resetFormAfterEncryption()
|
||||
await operationEnded(.databaseEncrypted)
|
||||
}
|
||||
@@ -174,7 +188,10 @@ struct DatabaseEncryptionView: View {
|
||||
|
||||
private func setUseKeychain(_ value: Bool) {
|
||||
useKeychain = value
|
||||
storeDBPassphraseGroupDefault.set(value)
|
||||
// Postpone it when migrating to the end of encryption process
|
||||
if !migration {
|
||||
storeDBPassphraseGroupDefault.set(value)
|
||||
}
|
||||
}
|
||||
|
||||
private func databaseEncryptionAlert(_ alertItem: DatabaseEncryptionAlert) -> Alert {
|
||||
@@ -184,13 +201,7 @@ struct DatabaseEncryptionView: View {
|
||||
title: Text("Remove passphrase from keychain?"),
|
||||
message: Text("Instant push notifications will be hidden!\n") + storeSecurelyDanger(),
|
||||
primaryButton: .destructive(Text("Remove")) {
|
||||
if kcDatabasePassword.remove() {
|
||||
logger.debug("passphrase removed from keychain")
|
||||
setUseKeychain(false)
|
||||
storedKey = false
|
||||
} else {
|
||||
alert = .error(title: "Keychain error", error: "Failed to remove passphrase")
|
||||
}
|
||||
removePassphraseFromKeyChain()
|
||||
},
|
||||
secondaryButton: .cancel() {
|
||||
withAnimation { useKeychainToggle = true }
|
||||
@@ -236,6 +247,16 @@ struct DatabaseEncryptionView: View {
|
||||
}
|
||||
}
|
||||
|
||||
private func removePassphraseFromKeyChain() {
|
||||
if kcDatabasePassword.remove() {
|
||||
logger.debug("passphrase removed from keychain")
|
||||
setUseKeychain(false)
|
||||
storedKey = false
|
||||
} else {
|
||||
alert = .error(title: "Keychain error", error: "Failed to remove passphrase")
|
||||
}
|
||||
}
|
||||
|
||||
private func storeSecurelySaved() -> Text {
|
||||
Text("Please store passphrase securely, you will NOT be able to change it if you lose it.")
|
||||
}
|
||||
@@ -346,6 +367,6 @@ func validKey(_ s: String) -> Bool {
|
||||
|
||||
struct DatabaseEncryptionView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
DatabaseEncryptionView(useKeychain: Binding.constant(true))
|
||||
DatabaseEncryptionView(useKeychain: Binding.constant(true), migration: false)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -64,7 +64,7 @@ struct DatabaseErrorView: View {
|
||||
case let .migrationError(mtrError):
|
||||
titleText("Incompatible database version")
|
||||
fileNameText(dbFile)
|
||||
Text("Error: ") + Text(mtrErrorDescription(mtrError))
|
||||
Text("Error: ") + Text(DatabaseErrorView.mtrErrorDescription(mtrError))
|
||||
}
|
||||
case let .errorSQL(dbFile, migrationSQLError):
|
||||
titleText("Database error")
|
||||
@@ -105,7 +105,7 @@ struct DatabaseErrorView: View {
|
||||
Text("Migrations: \(ms.joined(separator: ", "))")
|
||||
}
|
||||
|
||||
private func mtrErrorDescription(_ err: MTRError) -> LocalizedStringKey {
|
||||
static func mtrErrorDescription(_ err: MTRError) -> LocalizedStringKey {
|
||||
switch err {
|
||||
case let .noDown(dbMigrations):
|
||||
return "database version is newer than the app, but no down migration for: \(dbMigrations.joined(separator: ", "))"
|
||||
|
||||
@@ -116,7 +116,7 @@ struct DatabaseView: View {
|
||||
let color: Color = unencrypted ? .orange : .secondary
|
||||
settingsRow(unencrypted ? "lock.open" : useKeychain ? "key" : "lock", color: color) {
|
||||
NavigationLink {
|
||||
DatabaseEncryptionView(useKeychain: $useKeychain)
|
||||
DatabaseEncryptionView(useKeychain: $useKeychain, migration: false)
|
||||
.navigationTitle("Database passphrase")
|
||||
} label: {
|
||||
Text("Database passphrase")
|
||||
@@ -485,6 +485,10 @@ func deleteChatAsync() async throws {
|
||||
_ = kcDatabasePassword.remove()
|
||||
storeDBPassphraseGroupDefault.set(true)
|
||||
deleteAppDatabaseAndFiles()
|
||||
// Clean state so when creating new user the app will start chat automatically (see CreateProfile:createProfile())
|
||||
DispatchQueue.main.async {
|
||||
ChatModel.shared.users = []
|
||||
}
|
||||
}
|
||||
|
||||
struct DatabaseView_Previews: PreviewProvider {
|
||||
|
||||
@@ -188,6 +188,7 @@ struct MigrateToAppGroupView: View {
|
||||
let config = ArchiveConfig(archivePath: getDocumentsDirectory().appendingPathComponent(archiveName).path)
|
||||
Task {
|
||||
do {
|
||||
try apiSaveAppSettings(settings: AppSettings.current.prepareForExport())
|
||||
try await apiExportArchive(config: config)
|
||||
await MainActor.run { setV3DBMigration(.exported) }
|
||||
} catch let error {
|
||||
@@ -204,7 +205,11 @@ struct MigrateToAppGroupView: View {
|
||||
resetChatCtrl()
|
||||
try await MainActor.run { try initializeChat(start: false) }
|
||||
let _ = try await apiImportArchive(config: config)
|
||||
await MainActor.run { setV3DBMigration(.migrated) }
|
||||
let appSettings = try apiGetAppSettings(settings: AppSettings.current.prepareForExport())
|
||||
await MainActor.run {
|
||||
appSettings.importIntoApp()
|
||||
setV3DBMigration(.migrated)
|
||||
}
|
||||
} catch let error {
|
||||
dbContainerGroupDefault.set(.documents)
|
||||
await MainActor.run {
|
||||
@@ -216,16 +221,22 @@ struct MigrateToAppGroupView: View {
|
||||
}
|
||||
}
|
||||
|
||||
func exportChatArchive() async throws -> URL {
|
||||
func exportChatArchive(_ storagePath: URL? = nil) async throws -> URL {
|
||||
let archiveTime = Date.now
|
||||
let ts = archiveTime.ISO8601Format(Date.ISO8601FormatStyle(timeSeparator: .omitted))
|
||||
let archiveName = "simplex-chat.\(ts).zip"
|
||||
let archivePath = getDocumentsDirectory().appendingPathComponent(archiveName)
|
||||
let archivePath = (storagePath ?? getDocumentsDirectory()).appendingPathComponent(archiveName)
|
||||
let config = ArchiveConfig(archivePath: archivePath.path)
|
||||
// Settings should be saved before changing a passphrase, otherwise the database needs to be migrated first
|
||||
if !ChatModel.shared.chatDbChanged {
|
||||
try apiSaveAppSettings(settings: AppSettings.current.prepareForExport())
|
||||
}
|
||||
try await apiExportArchive(config: config)
|
||||
deleteOldArchive()
|
||||
UserDefaults.standard.set(archiveName, forKey: DEFAULT_CHAT_ARCHIVE_NAME)
|
||||
chatArchiveTimeDefault.set(archiveTime)
|
||||
if storagePath == nil {
|
||||
deleteOldArchive()
|
||||
UserDefaults.standard.set(archiveName, forKey: DEFAULT_CHAT_ARCHIVE_NAME)
|
||||
chatArchiveTimeDefault.set(archiveTime)
|
||||
}
|
||||
return archivePath
|
||||
}
|
||||
|
||||
|
||||
@@ -11,28 +11,43 @@ import UIKit
|
||||
import SwiftUI
|
||||
|
||||
extension View {
|
||||
func uiKitContextMenu(menu: Binding<UIMenu>, allowMenu: Binding<Bool>) -> some View {
|
||||
self.overlay {
|
||||
if allowMenu.wrappedValue {
|
||||
self.overlay(Color(uiColor: .systemBackground)).overlay(InteractionView(content: self, menu: menu))
|
||||
func uiKitContextMenu(hasImageOrVideo: Bool, maxWidth: CGFloat, itemWidth: Binding<CGFloat>, menu: Binding<UIMenu>, allowMenu: Binding<Bool>) -> some View {
|
||||
Group {
|
||||
if allowMenu.wrappedValue {
|
||||
if hasImageOrVideo {
|
||||
InteractionView(content:
|
||||
self.environmentObject(ChatModel.shared)
|
||||
.overlay(DetermineWidthImageVideoItem())
|
||||
.onPreferenceChange(DetermineWidthImageVideoItem.Key.self) { itemWidth.wrappedValue = $0 == 0 ? maxWidth : $0 }
|
||||
, maxWidth: maxWidth, itemWidth: itemWidth, menu: menu)
|
||||
.frame(maxWidth: itemWidth.wrappedValue)
|
||||
} else {
|
||||
InteractionView(content: self.environmentObject(ChatModel.shared), maxWidth: maxWidth, itemWidth: itemWidth, menu: menu)
|
||||
.fixedSize(horizontal: true, vertical: false)
|
||||
}
|
||||
} else {
|
||||
self
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private struct InteractionConfig<Content: View> {
|
||||
let content: Content
|
||||
let menu: UIMenu
|
||||
private class HostingViewHolder: UIView {
|
||||
var contentSize: CGSize = CGSizeMake(0, 0)
|
||||
override var intrinsicContentSize: CGSize { get { contentSize } }
|
||||
}
|
||||
|
||||
private struct InteractionView<Content: View>: UIViewRepresentable {
|
||||
struct InteractionView<Content: View>: UIViewRepresentable {
|
||||
let content: Content
|
||||
var maxWidth: CGFloat
|
||||
var itemWidth: Binding<CGFloat>
|
||||
@Binding var menu: UIMenu
|
||||
|
||||
func makeUIView(context: Context) -> UIView {
|
||||
let view = UIView()
|
||||
let view = HostingViewHolder()
|
||||
view.backgroundColor = .clear
|
||||
let hostView = UIHostingController(rootView: content)
|
||||
view.contentSize = hostView.view.intrinsicContentSize
|
||||
hostView.view.translatesAutoresizingMaskIntoConstraints = false
|
||||
let constraints = [
|
||||
hostView.view.topAnchor.constraint(equalTo: view.topAnchor),
|
||||
@@ -44,12 +59,20 @@ private struct InteractionView<Content: View>: UIViewRepresentable {
|
||||
]
|
||||
view.addSubview(hostView.view)
|
||||
view.addConstraints(constraints)
|
||||
view.layer.cornerRadius = 18
|
||||
hostView.view.layer.cornerRadius = 18
|
||||
let menuInteraction = UIContextMenuInteraction(delegate: context.coordinator)
|
||||
view.addInteraction(menuInteraction)
|
||||
return view
|
||||
}
|
||||
|
||||
func updateUIView(_ uiView: UIView, context: Context) {}
|
||||
func updateUIView(_ uiView: UIView, context: Context) {
|
||||
let was = (uiView as! HostingViewHolder).contentSize
|
||||
(uiView as! HostingViewHolder).contentSize = uiView.subviews[0].sizeThatFits(CGSizeMake(itemWidth.wrappedValue, .infinity))
|
||||
if was != (uiView as! HostingViewHolder).contentSize {
|
||||
uiView.invalidateIntrinsicContentSize()
|
||||
}
|
||||
}
|
||||
|
||||
func makeCoordinator() -> Coordinator {
|
||||
Coordinator(self)
|
||||
|
||||
@@ -21,6 +21,19 @@ struct DetermineWidth: View {
|
||||
}
|
||||
}
|
||||
|
||||
struct DetermineWidthImageVideoItem: View {
|
||||
typealias Key = MaximumWidthImageVideoPreferenceKey
|
||||
var body: some View {
|
||||
GeometryReader { proxy in
|
||||
Color.clear
|
||||
.preference(
|
||||
key: MaximumWidthImageVideoPreferenceKey.self,
|
||||
value: proxy.size.width
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct MaximumWidthPreferenceKey: PreferenceKey {
|
||||
static var defaultValue: CGFloat = 0
|
||||
static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {
|
||||
@@ -28,6 +41,13 @@ struct MaximumWidthPreferenceKey: PreferenceKey {
|
||||
}
|
||||
}
|
||||
|
||||
struct MaximumWidthImageVideoPreferenceKey: PreferenceKey {
|
||||
static var defaultValue: CGFloat = 0
|
||||
static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {
|
||||
value = max(value, nextValue())
|
||||
}
|
||||
}
|
||||
|
||||
struct DetermineWidth_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
DetermineWidth()
|
||||
|
||||
@@ -0,0 +1,734 @@
|
||||
//
|
||||
// MigrateFromDevice.swift
|
||||
// SimpleX (iOS)
|
||||
//
|
||||
// Created by Avently on 14.02.2024.
|
||||
// Copyright © 2024 SimpleX Chat. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import SimpleXChat
|
||||
|
||||
private enum MigrationFromState: Equatable {
|
||||
case chatStopInProgress
|
||||
case chatStopFailed(reason: String)
|
||||
case passphraseNotSet
|
||||
case passphraseConfirmation
|
||||
case uploadConfirmation
|
||||
case archiving
|
||||
case uploadProgress(uploadedBytes: Int64, totalBytes: Int64, fileId: Int64, archivePath: URL, ctrl: chat_ctrl?)
|
||||
case uploadFailed(totalBytes: Int64, archivePath: URL)
|
||||
case linkCreation
|
||||
case linkShown(fileId: Int64, link: String, archivePath: URL, ctrl: chat_ctrl)
|
||||
case finished(chatDeletion: Bool)
|
||||
}
|
||||
|
||||
private enum MigrateFromDeviceViewAlert: Identifiable {
|
||||
case deleteChat(_ title: LocalizedStringKey = "Delete chat profile?", _ text: LocalizedStringKey = "This action cannot be undone - your profile, contacts, messages and files will be irreversibly lost.")
|
||||
case startChat(_ title: LocalizedStringKey = "Start chat?", _ text: LocalizedStringKey = "Warning: starting chat on multiple devices is not supported and will cause message delivery failures")
|
||||
|
||||
case wrongPassphrase(title: LocalizedStringKey = "Wrong passphrase!", message: LocalizedStringKey = "Enter correct passphrase.")
|
||||
case invalidConfirmation(title: LocalizedStringKey = "Invalid migration confirmation")
|
||||
case keychainError(_ title: LocalizedStringKey = "Keychain error")
|
||||
case databaseError(_ title: LocalizedStringKey = "Database error", message: String)
|
||||
case unknownError(_ title: LocalizedStringKey = "Unknown error", message: String)
|
||||
|
||||
case error(title: LocalizedStringKey, error: String = "")
|
||||
|
||||
var id: String {
|
||||
switch self {
|
||||
case let .deleteChat(title, text): return "\(title) \(text)"
|
||||
case let .startChat(title, text): return "\(title) \(text)"
|
||||
|
||||
case .wrongPassphrase: return "wrongPassphrase"
|
||||
case .invalidConfirmation: return "invalidConfirmation"
|
||||
case .keychainError: return "keychainError"
|
||||
case let .databaseError(title, message): return "\(title) \(message)"
|
||||
case let .unknownError(title, message): return "\(title) \(message)"
|
||||
|
||||
case let .error(title, _): return "error \(title)"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct MigrateFromDevice: View {
|
||||
@EnvironmentObject var m: ChatModel
|
||||
@Environment(\.dismiss) var dismiss: DismissAction
|
||||
@Binding var showSettings: Bool
|
||||
@Binding var showProgressOnSettings: Bool
|
||||
@State private var migrationState: MigrationFromState = .chatStopInProgress
|
||||
@State private var useKeychain = storeDBPassphraseGroupDefault.get()
|
||||
@AppStorage(GROUP_DEFAULT_INITIAL_RANDOM_DB_PASSPHRASE, store: groupDefaults) private var initialRandomDBPassphrase: Bool = false
|
||||
@State private var alert: MigrateFromDeviceViewAlert?
|
||||
@State private var authorized = !UserDefaults.standard.bool(forKey: DEFAULT_PERFORM_LA)
|
||||
private let tempDatabaseUrl = urlForTemporaryDatabase()
|
||||
@State private var chatReceiver: MigrationChatReceiver? = nil
|
||||
@State private var backDisabled: Bool = false
|
||||
|
||||
var body: some View {
|
||||
if authorized {
|
||||
migrateView()
|
||||
} else {
|
||||
Button(action: runAuth) { Label("Unlock", systemImage: "lock") }
|
||||
.onAppear(perform: runAuth)
|
||||
}
|
||||
}
|
||||
|
||||
private func runAuth() { authorize(NSLocalizedString("Open migration to another device", comment: "authentication reason"), $authorized) }
|
||||
|
||||
func migrateView() -> some View {
|
||||
VStack {
|
||||
switch migrationState {
|
||||
case .chatStopInProgress:
|
||||
chatStopInProgressView()
|
||||
case let .chatStopFailed(reason):
|
||||
chatStopFailedView(reason)
|
||||
case .passphraseNotSet:
|
||||
passphraseNotSetView()
|
||||
case .passphraseConfirmation:
|
||||
PassphraseConfirmationView(migrationState: $migrationState, alert: $alert)
|
||||
case .uploadConfirmation:
|
||||
uploadConfirmationView()
|
||||
case .archiving:
|
||||
archivingView()
|
||||
case let .uploadProgress(uploaded, total, _, archivePath, _):
|
||||
uploadProgressView(uploaded, totalBytes: total, archivePath)
|
||||
case let .uploadFailed(total, archivePath):
|
||||
uploadFailedView(totalBytes: total, archivePath)
|
||||
case .linkCreation:
|
||||
linkCreationView()
|
||||
case let .linkShown(fileId, link, archivePath, ctrl):
|
||||
linkShownView(fileId, link, archivePath, ctrl)
|
||||
case let .finished(chatDeletion):
|
||||
finishedView(chatDeletion)
|
||||
}
|
||||
}
|
||||
.modifier(BackButton(label: "Back", disabled: $backDisabled) {
|
||||
dismiss()
|
||||
})
|
||||
.onChange(of: migrationState) { state in
|
||||
backDisabled = switch migrationState {
|
||||
case .chatStopInProgress, .archiving, .linkShown, .finished: true
|
||||
case .chatStopFailed, .passphraseNotSet, .passphraseConfirmation, .uploadConfirmation, .uploadProgress, .uploadFailed, .linkCreation: false
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
stopChat()
|
||||
}
|
||||
.onDisappear {
|
||||
Task {
|
||||
if !backDisabled {
|
||||
await MainActor.run {
|
||||
showProgressOnSettings = true
|
||||
}
|
||||
await startChatAndDismiss(false)
|
||||
await MainActor.run {
|
||||
showProgressOnSettings = false
|
||||
}
|
||||
}
|
||||
if case let .uploadProgress(_, _, fileId, _, ctrl) = migrationState, let ctrl {
|
||||
await cancelUploadedArchive(fileId, ctrl)
|
||||
}
|
||||
chatReceiver?.stopAndCleanUp()
|
||||
try? FileManager.default.removeItem(at: getMigrationTempFilesDirectory())
|
||||
}
|
||||
}
|
||||
.alert(item: $alert) { alert in
|
||||
switch alert {
|
||||
case let .startChat(title, text):
|
||||
return Alert(
|
||||
title: Text(title),
|
||||
message: Text(text),
|
||||
primaryButton: .destructive(Text("Start chat")) {
|
||||
Task {
|
||||
await startChatAndDismiss()
|
||||
}
|
||||
},
|
||||
secondaryButton: .cancel()
|
||||
)
|
||||
case let .deleteChat(title, text):
|
||||
return Alert(
|
||||
title: Text(title),
|
||||
message: Text(text),
|
||||
primaryButton: .default(Text("Delete")) {
|
||||
deleteChatAndDismiss()
|
||||
},
|
||||
secondaryButton: .cancel()
|
||||
)
|
||||
case let .wrongPassphrase(title, message):
|
||||
return Alert(title: Text(title), message: Text(message))
|
||||
case let .invalidConfirmation(title):
|
||||
return Alert(title: Text(title))
|
||||
case let .keychainError(title):
|
||||
return Alert(title: Text(title))
|
||||
case let .databaseError(title, message):
|
||||
return Alert(title: Text(title), message: Text(message))
|
||||
case let .unknownError(title, message):
|
||||
return Alert(title: Text(title), message: Text(message))
|
||||
case let .error(title, error):
|
||||
return Alert(title: Text(title), message: Text(error))
|
||||
}
|
||||
}
|
||||
.interactiveDismissDisabled(backDisabled)
|
||||
}
|
||||
|
||||
private func chatStopInProgressView() -> some View {
|
||||
ZStack {
|
||||
List {
|
||||
Section {} header: {
|
||||
Text("Stopping chat")
|
||||
}
|
||||
}
|
||||
progressView()
|
||||
}
|
||||
}
|
||||
|
||||
private func chatStopFailedView(_ reason: String) -> some View {
|
||||
List {
|
||||
Section {
|
||||
Text(reason)
|
||||
Button(action: stopChat) {
|
||||
settingsRow("stop.fill") {
|
||||
Text("Stop chat").foregroundColor(.red)
|
||||
}
|
||||
}
|
||||
} header: {
|
||||
Text("Error stopping chat")
|
||||
} footer: {
|
||||
Text("In order to continue, chat should be stopped.")
|
||||
.font(.callout)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func passphraseNotSetView() -> some View {
|
||||
DatabaseEncryptionView(useKeychain: $useKeychain, migration: true)
|
||||
.onChange(of: initialRandomDBPassphrase) { initial in
|
||||
if !initial {
|
||||
migrationState = .uploadConfirmation
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func uploadConfirmationView() -> some View {
|
||||
List {
|
||||
Section {
|
||||
Button(action: { migrationState = .archiving }) {
|
||||
settingsRow("tray.and.arrow.up") {
|
||||
Text("Archive and upload").foregroundColor(.accentColor)
|
||||
}
|
||||
}
|
||||
} header: {
|
||||
Text("Confirm upload")
|
||||
} footer: {
|
||||
Text("All your contacts, conversations and files will be securely encrypted and uploaded in chunks to configured XFTP relays.")
|
||||
.font(.callout)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func archivingView() -> some View {
|
||||
ZStack {
|
||||
List {
|
||||
Section {} header: {
|
||||
Text("Archiving database")
|
||||
}
|
||||
}
|
||||
progressView()
|
||||
}
|
||||
.onAppear {
|
||||
exportArchive()
|
||||
}
|
||||
}
|
||||
|
||||
private func uploadProgressView(_ uploadedBytes: Int64, totalBytes: Int64, _ archivePath: URL) -> some View {
|
||||
ZStack {
|
||||
List {
|
||||
Section {} header: {
|
||||
Text("Uploading archive")
|
||||
}
|
||||
}
|
||||
let ratio = Float(uploadedBytes) / Float(totalBytes)
|
||||
MigrateFromDevice.largeProgressView(ratio, "\(Int(ratio * 100))%", "\(ByteCountFormatter.string(fromByteCount: uploadedBytes, countStyle: .binary)) uploaded")
|
||||
}
|
||||
.onAppear {
|
||||
startUploading(totalBytes, archivePath)
|
||||
}
|
||||
}
|
||||
|
||||
private func uploadFailedView(totalBytes: Int64, _ archivePath: URL) -> some View {
|
||||
List {
|
||||
Section {
|
||||
Button(action: {
|
||||
migrationState = .uploadProgress(uploadedBytes: 0, totalBytes: totalBytes, fileId: 0, archivePath: archivePath, ctrl: nil)
|
||||
}) {
|
||||
settingsRow("tray.and.arrow.up") {
|
||||
Text("Repeat upload").foregroundColor(.accentColor)
|
||||
}
|
||||
}
|
||||
} header: {
|
||||
Text("Upload failed")
|
||||
} footer: {
|
||||
Text("You can give another try.")
|
||||
.font(.callout)
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
chatReceiver?.stopAndCleanUp()
|
||||
}
|
||||
}
|
||||
|
||||
private func linkCreationView() -> some View {
|
||||
ZStack {
|
||||
List {
|
||||
Section {} header: {
|
||||
Text("Creating archive link")
|
||||
}
|
||||
}
|
||||
progressView()
|
||||
}
|
||||
}
|
||||
|
||||
private func linkShownView(_ fileId: Int64, _ link: String, _ archivePath: URL, _ ctrl: chat_ctrl) -> some View {
|
||||
List {
|
||||
Section {
|
||||
Button(action: { cancelMigration(fileId, ctrl) }) {
|
||||
settingsRow("multiply") {
|
||||
Text("Cancel migration").foregroundColor(.red)
|
||||
}
|
||||
}
|
||||
Button(action: { finishMigration(fileId, ctrl) }) {
|
||||
settingsRow("checkmark") {
|
||||
Text("Finalize migration").foregroundColor(.accentColor)
|
||||
}
|
||||
}
|
||||
} footer: {
|
||||
VStack(alignment: .leading, spacing: 16) {
|
||||
Text("**Warning**: the archive will be removed.")
|
||||
Text("Choose _Migrate from another device_ on the new device and scan QR code.")
|
||||
}
|
||||
.font(.callout)
|
||||
}
|
||||
Section("Show QR code") {
|
||||
SimpleXLinkQRCode(uri: link)
|
||||
.padding()
|
||||
.background(
|
||||
RoundedRectangle(cornerRadius: 12, style: .continuous)
|
||||
.fill(Color(uiColor: .secondarySystemGroupedBackground))
|
||||
)
|
||||
.padding(.horizontal)
|
||||
.listRowBackground(Color.clear)
|
||||
.listRowSeparator(.hidden)
|
||||
.listRowInsets(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0))
|
||||
}
|
||||
|
||||
Section("Or securely share this file link") {
|
||||
shareLinkView(link)
|
||||
}
|
||||
.listRowInsets(EdgeInsets(top: 0, leading: 20, bottom: 0, trailing: 10))
|
||||
}
|
||||
}
|
||||
|
||||
private func finishedView(_ chatDeletion: Bool) -> some View {
|
||||
ZStack {
|
||||
List {
|
||||
Section {
|
||||
Button(action: { alert = .startChat() }) {
|
||||
settingsRow("play.fill") {
|
||||
Text("Start chat").foregroundColor(.red)
|
||||
}
|
||||
}
|
||||
Button(action: { alert = .deleteChat() }) {
|
||||
settingsRow("trash.fill") {
|
||||
Text("Delete database from this device").foregroundColor(.accentColor)
|
||||
}
|
||||
}
|
||||
} header: {
|
||||
Text("Migration complete")
|
||||
} footer: {
|
||||
VStack(alignment: .leading, spacing: 16) {
|
||||
Text("You **must not** use the same database on two devices.")
|
||||
Text("**Please note**: using the same database on two devices will break the decryption of messages from your connections, as a security protection.")
|
||||
}
|
||||
.font(.callout)
|
||||
}
|
||||
}
|
||||
if chatDeletion {
|
||||
progressView()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func shareLinkView(_ link: String) -> some View {
|
||||
HStack {
|
||||
linkTextView(link)
|
||||
Button {
|
||||
showShareSheet(items: [link])
|
||||
} label: {
|
||||
Image(systemName: "square.and.arrow.up")
|
||||
.padding(.top, -7)
|
||||
}
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
}
|
||||
|
||||
private func linkTextView(_ link: String) -> some View {
|
||||
Text(link)
|
||||
.lineLimit(1)
|
||||
.font(.caption)
|
||||
.truncationMode(.middle)
|
||||
}
|
||||
|
||||
static func largeProgressView(_ value: Float, _ title: String, _ description: LocalizedStringKey) -> some View {
|
||||
ZStack {
|
||||
VStack {
|
||||
Text(description)
|
||||
.font(.title3)
|
||||
.hidden()
|
||||
|
||||
Text(title)
|
||||
.font(.system(size: 54))
|
||||
.bold()
|
||||
.foregroundColor(.accentColor)
|
||||
|
||||
Text(description)
|
||||
.font(.title3)
|
||||
}
|
||||
|
||||
Circle()
|
||||
.trim(from: 0, to: CGFloat(value))
|
||||
.stroke(
|
||||
Color.accentColor,
|
||||
style: StrokeStyle(lineWidth: 27)
|
||||
)
|
||||
.rotationEffect(.degrees(180))
|
||||
.animation(.linear, value: value)
|
||||
.frame(maxWidth: .infinity)
|
||||
.padding(.horizontal)
|
||||
.padding(.horizontal)
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
}
|
||||
|
||||
private func stopChat() {
|
||||
Task {
|
||||
do {
|
||||
try await stopChatAsync()
|
||||
do {
|
||||
try apiSaveAppSettings(settings: AppSettings.current.prepareForExport())
|
||||
await MainActor.run {
|
||||
migrationState = initialRandomDBPassphraseGroupDefault.get() ? .passphraseNotSet : .passphraseConfirmation
|
||||
}
|
||||
} catch let error {
|
||||
alert = .error(title: "Error saving settings", error: error.localizedDescription)
|
||||
migrationState = .chatStopFailed(reason: NSLocalizedString("Error saving settings", comment: "when migrating"))
|
||||
}
|
||||
} catch let e {
|
||||
await MainActor.run {
|
||||
migrationState = .chatStopFailed(reason: e.localizedDescription)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func exportArchive() {
|
||||
Task {
|
||||
do {
|
||||
try? FileManager.default.createDirectory(at: getMigrationTempFilesDirectory(), withIntermediateDirectories: true)
|
||||
let archivePath = try await exportChatArchive(getMigrationTempFilesDirectory())
|
||||
if let attrs = try? FileManager.default.attributesOfItem(atPath: archivePath.path),
|
||||
let totalBytes = attrs[.size] as? Int64 {
|
||||
await MainActor.run {
|
||||
migrationState = .uploadProgress(uploadedBytes: 0, totalBytes: totalBytes, fileId: 0, archivePath: archivePath, ctrl: nil)
|
||||
}
|
||||
} else {
|
||||
await MainActor.run {
|
||||
alert = .error(title: "Exported file doesn't exist")
|
||||
migrationState = .uploadConfirmation
|
||||
}
|
||||
}
|
||||
} catch let error {
|
||||
await MainActor.run {
|
||||
alert = .error(title: "Error exporting chat database", error: responseError(error))
|
||||
migrationState = .uploadConfirmation
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func initTemporaryDatabase() -> (chat_ctrl, User)? {
|
||||
let (status, ctrl) = chatInitTemporaryDatabase(url: tempDatabaseUrl)
|
||||
showErrorOnMigrationIfNeeded(status, $alert)
|
||||
do {
|
||||
if let ctrl, let user = try startChatWithTemporaryDatabase(ctrl: ctrl) {
|
||||
return (ctrl, user)
|
||||
}
|
||||
} catch let error {
|
||||
logger.error("Error while starting chat in temporary database: \(error.localizedDescription)")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
private func startUploading(_ totalBytes: Int64, _ archivePath: URL) {
|
||||
Task {
|
||||
guard let ctrlAndUser = initTemporaryDatabase() else {
|
||||
return migrationState = .uploadFailed(totalBytes: totalBytes, archivePath: archivePath)
|
||||
}
|
||||
let (ctrl, user) = ctrlAndUser
|
||||
chatReceiver = MigrationChatReceiver(ctrl: ctrl, databaseUrl: tempDatabaseUrl) { msg in
|
||||
await MainActor.run {
|
||||
switch msg {
|
||||
case let .sndFileProgressXFTP(_, _, fileTransferMeta, sentSize, totalSize):
|
||||
if case let .uploadProgress(uploaded, total, _, _, _) = migrationState, uploaded != total {
|
||||
migrationState = .uploadProgress(uploadedBytes: sentSize, totalBytes: totalSize, fileId: fileTransferMeta.fileId, archivePath: archivePath, ctrl: ctrl)
|
||||
}
|
||||
case .sndFileRedirectStartXFTP:
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
|
||||
migrationState = .linkCreation
|
||||
}
|
||||
case let .sndStandaloneFileComplete(_, fileTransferMeta, rcvURIs):
|
||||
let cfg = getNetCfg()
|
||||
let data = MigrationFileLinkData.init(
|
||||
networkConfig: MigrationFileLinkData.NetworkConfig(
|
||||
socksProxy: cfg.socksProxy,
|
||||
hostMode: cfg.hostMode,
|
||||
requiredHostMode: cfg.requiredHostMode
|
||||
)
|
||||
)
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
|
||||
migrationState = .linkShown(fileId: fileTransferMeta.fileId, link: data.addToLink(link: rcvURIs[0]), archivePath: archivePath, ctrl: ctrl)
|
||||
}
|
||||
case .sndFileError:
|
||||
alert = .error(title: "Upload failed", error: "Check your internet connection and try again")
|
||||
migrationState = .uploadFailed(totalBytes: totalBytes, archivePath: archivePath)
|
||||
default:
|
||||
logger.debug("unsupported event: \(msg.responseType)")
|
||||
}
|
||||
}
|
||||
}
|
||||
chatReceiver?.start()
|
||||
|
||||
let (res, error) = await uploadStandaloneFile(user: user, file: CryptoFile.plain(archivePath.lastPathComponent), ctrl: ctrl)
|
||||
await MainActor.run {
|
||||
guard let res = res else {
|
||||
migrationState = .uploadFailed(totalBytes: totalBytes, archivePath: archivePath)
|
||||
return alert = .error(title: "Error uploading the archive", error: error ?? "")
|
||||
}
|
||||
migrationState = .uploadProgress(uploadedBytes: 0, totalBytes: res.fileSize, fileId: res.fileId, archivePath: archivePath, ctrl: ctrl)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func cancelUploadedArchive(_ fileId: Int64, _ ctrl: chat_ctrl) async {
|
||||
_ = await apiCancelFile(fileId: fileId, ctrl: ctrl)
|
||||
}
|
||||
|
||||
private func cancelMigration(_ fileId: Int64, _ ctrl: chat_ctrl) {
|
||||
Task {
|
||||
await cancelUploadedArchive(fileId, ctrl)
|
||||
await startChatAndDismiss()
|
||||
}
|
||||
}
|
||||
|
||||
private func finishMigration(_ fileId: Int64, _ ctrl: chat_ctrl) {
|
||||
Task {
|
||||
await cancelUploadedArchive(fileId, ctrl)
|
||||
await MainActor.run {
|
||||
migrationState = .finished(chatDeletion: false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func deleteChatAndDismiss() {
|
||||
Task {
|
||||
do {
|
||||
try await deleteChatAsync()
|
||||
m.chatDbChanged = true
|
||||
m.chatInitialized = false
|
||||
migrationState = .finished(chatDeletion: true)
|
||||
DispatchQueue.main.asyncAfter(deadline: .now()) {
|
||||
resetChatCtrl()
|
||||
do {
|
||||
try initializeChat(start: false)
|
||||
m.chatDbChanged = false
|
||||
AppChatState.shared.set(.active)
|
||||
} catch let error {
|
||||
fatalError("Error starting chat \(responseError(error))")
|
||||
}
|
||||
showSettings = false
|
||||
}
|
||||
} catch let error {
|
||||
alert = .error(title: "Error deleting database", error: responseError(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func startChatAndDismiss(_ dismiss: Bool = true) async {
|
||||
AppChatState.shared.set(.active)
|
||||
do {
|
||||
if m.chatDbChanged {
|
||||
resetChatCtrl()
|
||||
try initializeChat(start: true)
|
||||
m.chatDbChanged = false
|
||||
} else {
|
||||
try startChat(refreshInvitations: true)
|
||||
}
|
||||
} catch let error {
|
||||
alert = .error(title: "Error starting chat", error: responseError(error))
|
||||
}
|
||||
// Hide settings anyway if chatDbStatus is not ok, probably passphrase needs to be entered
|
||||
if dismiss || m.chatDbStatus != .ok {
|
||||
await MainActor.run {
|
||||
showSettings = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static func urlForTemporaryDatabase() -> URL {
|
||||
URL(fileURLWithPath: generateNewFileName(getMigrationTempFilesDirectory().path + "/" + "migration", "db", fullPath: true))
|
||||
}
|
||||
}
|
||||
|
||||
private struct PassphraseConfirmationView: View {
|
||||
@Binding var migrationState: MigrationFromState
|
||||
@State private var useKeychain = storeDBPassphraseGroupDefault.get()
|
||||
@State private var currentKey: String = ""
|
||||
@State private var verifyingPassphrase: Bool = false
|
||||
@FocusState private var keyboardVisible: Bool
|
||||
@Binding var alert: MigrateFromDeviceViewAlert?
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
List {
|
||||
chatStoppedView()
|
||||
Section {
|
||||
PassphraseField(key: $currentKey, placeholder: "Current passphrase…", valid: validKey(currentKey))
|
||||
.focused($keyboardVisible)
|
||||
Button(action: {
|
||||
verifyingPassphrase = true
|
||||
hideKeyboard()
|
||||
Task {
|
||||
await verifyDatabasePassphrase(currentKey)
|
||||
verifyingPassphrase = false
|
||||
}
|
||||
}) {
|
||||
settingsRow(useKeychain ? "key" : "lock", color: .secondary) {
|
||||
Text("Verify passphrase")
|
||||
}
|
||||
}
|
||||
.disabled(verifyingPassphrase || currentKey.isEmpty)
|
||||
} header: {
|
||||
Text("Verify database passphrase")
|
||||
} footer: {
|
||||
Text("Confirm that you remember database passphrase to migrate it.")
|
||||
.font(.callout)
|
||||
}
|
||||
.onAppear {
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
|
||||
keyboardVisible = true
|
||||
}
|
||||
}
|
||||
}
|
||||
if verifyingPassphrase {
|
||||
progressView()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func verifyDatabasePassphrase(_ dbKey: String) async {
|
||||
do {
|
||||
try await testStorageEncryption(key: dbKey)
|
||||
await MainActor.run {
|
||||
migrationState = .uploadConfirmation
|
||||
}
|
||||
} catch let error {
|
||||
if case .chatCmdError(_, .errorDatabase(.errorOpen(.errorNotADatabase))) = error as? ChatResponse {
|
||||
showErrorOnMigrationIfNeeded(.errorNotADatabase(dbFile: ""), $alert)
|
||||
} else {
|
||||
alert = .error(title: "Error", error: NSLocalizedString("Error verifying passphrase:", comment: "") + " " + String(String(describing: error)))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func showErrorOnMigrationIfNeeded(_ status: DBMigrationResult, _ alert: Binding<MigrateFromDeviceViewAlert?>) {
|
||||
switch status {
|
||||
case .invalidConfirmation:
|
||||
alert.wrappedValue = .invalidConfirmation()
|
||||
case .errorNotADatabase:
|
||||
alert.wrappedValue = .wrongPassphrase()
|
||||
case .errorKeychain:
|
||||
alert.wrappedValue = .keychainError()
|
||||
case let .errorSQL(_, error):
|
||||
alert.wrappedValue = .databaseError(message: error)
|
||||
case let .unknown(error):
|
||||
alert.wrappedValue = .unknownError(message: error)
|
||||
case .errorMigration: ()
|
||||
case .ok: ()
|
||||
}
|
||||
}
|
||||
|
||||
private func progressView() -> some View {
|
||||
VStack {
|
||||
ProgressView().scaleEffect(2)
|
||||
}
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity )
|
||||
}
|
||||
|
||||
func chatStoppedView() -> some View {
|
||||
settingsRow("exclamationmark.octagon.fill", color: .red) {
|
||||
Text("Chat is stopped")
|
||||
}
|
||||
}
|
||||
|
||||
private class MigrationChatReceiver {
|
||||
let ctrl: chat_ctrl
|
||||
let databaseUrl: URL
|
||||
let processReceivedMsg: (ChatResponse) async -> Void
|
||||
private var receiveLoop: Task<Void, Never>?
|
||||
private var receiveMessages = true
|
||||
|
||||
init(ctrl: chat_ctrl, databaseUrl: URL, _ processReceivedMsg: @escaping (ChatResponse) async -> Void) {
|
||||
self.ctrl = ctrl
|
||||
self.databaseUrl = databaseUrl
|
||||
self.processReceivedMsg = processReceivedMsg
|
||||
}
|
||||
|
||||
func start() {
|
||||
logger.debug("MigrationChatReceiver.start")
|
||||
receiveMessages = true
|
||||
if receiveLoop != nil { return }
|
||||
receiveLoop = Task { await receiveMsgLoop() }
|
||||
}
|
||||
|
||||
func receiveMsgLoop() async {
|
||||
// TODO use function that has timeout
|
||||
if let msg = await chatRecvMsg(ctrl) {
|
||||
Task {
|
||||
await TerminalItems.shared.add(.resp(.now, msg))
|
||||
}
|
||||
logger.debug("processReceivedMsg: \(msg.responseType)")
|
||||
await processReceivedMsg(msg)
|
||||
}
|
||||
if self.receiveMessages {
|
||||
_ = try? await Task.sleep(nanoseconds: 7_500_000)
|
||||
await receiveMsgLoop()
|
||||
}
|
||||
}
|
||||
|
||||
func stopAndCleanUp() {
|
||||
logger.debug("MigrationChatReceiver.stop")
|
||||
receiveMessages = false
|
||||
receiveLoop?.cancel()
|
||||
receiveLoop = nil
|
||||
chat_close_store(ctrl)
|
||||
try? FileManager.default.removeItem(atPath: "\(databaseUrl.path)_chat.db")
|
||||
try? FileManager.default.removeItem(atPath: "\(databaseUrl.path)_agent.db")
|
||||
}
|
||||
}
|
||||
|
||||
struct MigrateFromDevice_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
MigrateFromDevice(showSettings: Binding.constant(true), showProgressOnSettings: Binding.constant(false))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,717 @@
|
||||
//
|
||||
// MigrateToDevice.swift
|
||||
// SimpleX (iOS)
|
||||
//
|
||||
// Created by Avently on 23.02.2024.
|
||||
// Copyright © 2024 SimpleX Chat. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import SimpleXChat
|
||||
|
||||
enum MigrationToDeviceState: Codable, Equatable {
|
||||
case downloadProgress(link: String, archiveName: String)
|
||||
case archiveImport(archiveName: String)
|
||||
case passphrase
|
||||
|
||||
// Here we check whether it's needed to show migration process after app restart or not
|
||||
// It's important to NOT show the process when archive was corrupted/not fully downloaded
|
||||
static func makeMigrationState() -> MigrationToState? {
|
||||
let state: MigrationToDeviceState? = UserDefaults.standard.string(forKey: DEFAULT_MIGRATION_TO_STAGE) != nil ? decodeJSON(UserDefaults.standard.string(forKey: DEFAULT_MIGRATION_TO_STAGE)!) : nil
|
||||
var initial: MigrationToState? = .pasteOrScanLink
|
||||
//logger.debug("Inited with migrationState: \(String(describing: state))")
|
||||
switch state {
|
||||
case nil:
|
||||
initial = nil
|
||||
case .downloadProgress:
|
||||
// No migration happens at the moment actually since archive were not downloaded fully
|
||||
logger.debug("MigrateToDevice: archive wasn't fully downloaded, removed broken file")
|
||||
initial = nil
|
||||
case let .archiveImport(archiveName):
|
||||
let archivePath = getMigrationTempFilesDirectory().path + "/" + archiveName
|
||||
initial = .archiveImportFailed(archivePath: archivePath)
|
||||
case .passphrase:
|
||||
initial = .passphrase(passphrase: "")
|
||||
}
|
||||
if initial == nil {
|
||||
UserDefaults.standard.removeObject(forKey: DEFAULT_MIGRATION_TO_STAGE)
|
||||
try? FileManager.default.removeItem(at: getMigrationTempFilesDirectory())
|
||||
}
|
||||
return initial
|
||||
}
|
||||
|
||||
static func save(_ state: MigrationToDeviceState?) {
|
||||
if let state {
|
||||
UserDefaults.standard.setValue(encodeJSON(state), forKey: DEFAULT_MIGRATION_TO_STAGE)
|
||||
} else {
|
||||
UserDefaults.standard.removeObject(forKey: DEFAULT_MIGRATION_TO_STAGE)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum MigrationToState: Equatable {
|
||||
case pasteOrScanLink
|
||||
case linkDownloading(link: String)
|
||||
case downloadProgress(downloadedBytes: Int64, totalBytes: Int64, fileId: Int64, link: String, archivePath: String, ctrl: chat_ctrl?)
|
||||
case downloadFailed(totalBytes: Int64, link: String, archivePath: String)
|
||||
case archiveImport(archivePath: String)
|
||||
case archiveImportFailed(archivePath: String)
|
||||
case passphrase(passphrase: String)
|
||||
case migrationConfirmation(status: DBMigrationResult, passphrase: String, useKeychain: Bool)
|
||||
case migration(passphrase: String, confirmation: MigrationConfirmation, useKeychain: Bool)
|
||||
case onion(appSettings: AppSettings)
|
||||
}
|
||||
|
||||
private enum MigrateToDeviceViewAlert: Identifiable {
|
||||
case chatImportedWithErrors(title: LocalizedStringKey = "Chat database imported",
|
||||
text: LocalizedStringKey = "Some non-fatal errors occurred during import - you may see Chat console for more details.")
|
||||
|
||||
case wrongPassphrase(title: LocalizedStringKey = "Wrong passphrase!", message: LocalizedStringKey = "Enter correct passphrase.")
|
||||
case invalidConfirmation(title: LocalizedStringKey = "Invalid migration confirmation")
|
||||
case keychainError(_ title: LocalizedStringKey = "Keychain error")
|
||||
case databaseError(_ title: LocalizedStringKey = "Database error", message: String)
|
||||
case unknownError(_ title: LocalizedStringKey = "Unknown error", message: String)
|
||||
|
||||
case error(title: LocalizedStringKey, error: String = "")
|
||||
|
||||
var id: String {
|
||||
switch self {
|
||||
case .chatImportedWithErrors: return "chatImportedWithErrors"
|
||||
|
||||
case .wrongPassphrase: return "wrongPassphrase"
|
||||
case .invalidConfirmation: return "invalidConfirmation"
|
||||
case .keychainError: return "keychainError"
|
||||
case let .databaseError(title, message): return "\(title) \(message)"
|
||||
case let .unknownError(title, message): return "\(title) \(message)"
|
||||
|
||||
case let .error(title, _): return "error \(title)"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct MigrateToDevice: View {
|
||||
@EnvironmentObject var m: ChatModel
|
||||
@Environment(\.dismiss) var dismiss: DismissAction
|
||||
@AppStorage(DEFAULT_DEVELOPER_TOOLS) private var developerTools = false
|
||||
@Binding var migrationState: MigrationToState?
|
||||
@State private var useKeychain = storeDBPassphraseGroupDefault.get()
|
||||
@State private var alert: MigrateToDeviceViewAlert?
|
||||
private let tempDatabaseUrl = urlForTemporaryDatabase()
|
||||
@State private var chatReceiver: MigrationChatReceiver? = nil
|
||||
// Prevent from hiding the view until migration is finished or app deleted
|
||||
@State private var backDisabled: Bool = false
|
||||
@State private var showQRCodeScanner: Bool = true
|
||||
|
||||
var body: some View {
|
||||
VStack {
|
||||
switch migrationState {
|
||||
case nil: EmptyView()
|
||||
case .pasteOrScanLink:
|
||||
pasteOrScanLinkView()
|
||||
case let .linkDownloading(link):
|
||||
linkDownloadingView(link)
|
||||
case let .downloadProgress(downloaded, total, _, _, _, _):
|
||||
downloadProgressView(downloaded, totalBytes: total)
|
||||
case let .downloadFailed(total, link, archivePath):
|
||||
downloadFailedView(totalBytes: total, link, archivePath)
|
||||
case let .archiveImport(archivePath):
|
||||
archiveImportView(archivePath)
|
||||
case let .archiveImportFailed(archivePath):
|
||||
archiveImportFailedView(archivePath)
|
||||
case let .passphrase(passphrase):
|
||||
PassphraseEnteringView(migrationState: $migrationState, currentKey: passphrase, alert: $alert)
|
||||
case let .migrationConfirmation(status, passphrase, useKeychain):
|
||||
migrationConfirmationView(status, passphrase, useKeychain)
|
||||
case let .migration(passphrase, confirmation, useKeychain):
|
||||
migrationView(passphrase, confirmation, useKeychain)
|
||||
case let .onion(appSettings):
|
||||
OnionView(appSettings: appSettings, finishMigration: finishMigration)
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
backDisabled = switch migrationState {
|
||||
case nil, .pasteOrScanLink, .linkDownloading, .downloadProgress, .downloadFailed, .archiveImportFailed: false
|
||||
case .archiveImport, .passphrase, .migrationConfirmation, .migration, .onion: true
|
||||
}
|
||||
}
|
||||
.onChange(of: migrationState) { state in
|
||||
backDisabled = switch state {
|
||||
case nil, .pasteOrScanLink, .linkDownloading, .downloadProgress, .downloadFailed, .archiveImportFailed: false
|
||||
case .archiveImport, .passphrase, .migrationConfirmation, .migration, .onion: true
|
||||
}
|
||||
}
|
||||
.onDisappear {
|
||||
Task {
|
||||
if case .archiveImportFailed = migrationState {
|
||||
// Original database is not exist, nothing is setup correctly for showing to a user yet. Return to clean state
|
||||
deleteAppDatabaseAndFiles()
|
||||
initChatAndMigrate()
|
||||
} else if case let .downloadProgress(_, _, fileId, _, _, ctrl) = migrationState, let ctrl {
|
||||
await stopArchiveDownloading(fileId, ctrl)
|
||||
}
|
||||
chatReceiver?.stopAndCleanUp()
|
||||
if !backDisabled {
|
||||
try? FileManager.default.removeItem(at: getMigrationTempFilesDirectory())
|
||||
MigrationToDeviceState.save(nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
.alert(item: $alert) { alert in
|
||||
switch alert {
|
||||
case let .chatImportedWithErrors(title, text):
|
||||
return Alert(title: Text(title), message: Text(text))
|
||||
case let .wrongPassphrase(title, message):
|
||||
return Alert(title: Text(title), message: Text(message))
|
||||
case let .invalidConfirmation(title):
|
||||
return Alert(title: Text(title))
|
||||
case let .keychainError(title):
|
||||
return Alert(title: Text(title))
|
||||
case let .databaseError(title, message):
|
||||
return Alert(title: Text(title), message: Text(message))
|
||||
case let .unknownError(title, message):
|
||||
return Alert(title: Text(title), message: Text(message))
|
||||
case let .error(title, error):
|
||||
return Alert(title: Text(title), message: Text(error))
|
||||
}
|
||||
}
|
||||
.interactiveDismissDisabled(backDisabled)
|
||||
}
|
||||
|
||||
private func pasteOrScanLinkView() -> some View {
|
||||
ZStack {
|
||||
List {
|
||||
Section("Scan QR code") {
|
||||
ScannerInView(showQRCodeScanner: $showQRCodeScanner) { resp in
|
||||
switch resp {
|
||||
case let .success(r):
|
||||
let link = r.string
|
||||
if strHasSimplexFileLink(link.trimmingCharacters(in: .whitespaces)) {
|
||||
migrationState = .linkDownloading(link: link.trimmingCharacters(in: .whitespaces))
|
||||
} else {
|
||||
alert = .error(title: "Invalid link", error: "The text you pasted is not a SimpleX link.")
|
||||
}
|
||||
case let .failure(e):
|
||||
logger.error("processQRCode QR code error: \(e.localizedDescription)")
|
||||
alert = .error(title: "Invalid link", error: "The text you pasted is not a SimpleX link.")
|
||||
}
|
||||
}
|
||||
}
|
||||
if developerTools {
|
||||
Section("Or paste archive link") {
|
||||
pasteLinkView()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func pasteLinkView() -> some View {
|
||||
Button {
|
||||
if let str = UIPasteboard.general.string {
|
||||
if strHasSimplexFileLink(str.trimmingCharacters(in: .whitespaces)) {
|
||||
migrationState = .linkDownloading(link: str.trimmingCharacters(in: .whitespaces))
|
||||
} else {
|
||||
alert = .error(title: "Invalid link", error: "The text you pasted is not a SimpleX link.")
|
||||
}
|
||||
}
|
||||
} label: {
|
||||
Text("Tap to paste link")
|
||||
}
|
||||
.disabled(!ChatModel.shared.pasteboardHasStrings)
|
||||
.frame(maxWidth: .infinity, alignment: .center)
|
||||
}
|
||||
|
||||
private func linkDownloadingView(_ link: String) -> some View {
|
||||
ZStack {
|
||||
List {
|
||||
Section {} header: {
|
||||
Text("Downloading link details")
|
||||
}
|
||||
}
|
||||
progressView()
|
||||
}
|
||||
.onAppear {
|
||||
downloadLinkDetails(link)
|
||||
}
|
||||
}
|
||||
|
||||
private func downloadProgressView(_ downloadedBytes: Int64, totalBytes: Int64) -> some View {
|
||||
ZStack {
|
||||
List {
|
||||
Section {} header: {
|
||||
Text("Downloading archive")
|
||||
}
|
||||
}
|
||||
let ratio = Float(downloadedBytes) / Float(max(totalBytes, 1))
|
||||
MigrateFromDevice.largeProgressView(ratio, "\(Int(ratio * 100))%", "\(ByteCountFormatter.string(fromByteCount: downloadedBytes, countStyle: .binary)) downloaded")
|
||||
}
|
||||
}
|
||||
|
||||
private func downloadFailedView(totalBytes: Int64, _ link: String, _ archivePath: String) -> some View {
|
||||
List {
|
||||
Section {
|
||||
Button(action: {
|
||||
try? FileManager.default.removeItem(atPath: archivePath)
|
||||
migrationState = .linkDownloading(link: link)
|
||||
}) {
|
||||
settingsRow("tray.and.arrow.down") {
|
||||
Text("Repeat download").foregroundColor(.accentColor)
|
||||
}
|
||||
}
|
||||
} header: {
|
||||
Text("Download failed")
|
||||
} footer: {
|
||||
Text("You can give another try.")
|
||||
.font(.callout)
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
chatReceiver?.stopAndCleanUp()
|
||||
try? FileManager.default.removeItem(atPath: archivePath)
|
||||
MigrationToDeviceState.save(nil)
|
||||
}
|
||||
}
|
||||
|
||||
private func archiveImportView(_ archivePath: String) -> some View {
|
||||
ZStack {
|
||||
List {
|
||||
Section {} header: {
|
||||
Text("Importing archive")
|
||||
}
|
||||
}
|
||||
progressView()
|
||||
}
|
||||
.onAppear {
|
||||
importArchive(archivePath)
|
||||
}
|
||||
}
|
||||
|
||||
private func archiveImportFailedView(_ archivePath: String) -> some View {
|
||||
List {
|
||||
Section {
|
||||
Button(action: {
|
||||
migrationState = .archiveImport(archivePath: archivePath)
|
||||
}) {
|
||||
settingsRow("square.and.arrow.down") {
|
||||
Text("Repeat import").foregroundColor(.accentColor)
|
||||
}
|
||||
}
|
||||
} header: {
|
||||
Text("Import failed")
|
||||
} footer: {
|
||||
Text("You can give another try.")
|
||||
.font(.callout)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func migrationConfirmationView(_ status: DBMigrationResult, _ passphrase: String, _ useKeychain: Bool) -> some View {
|
||||
List {
|
||||
let (header, button, footer, confirmation): (LocalizedStringKey, LocalizedStringKey?, String, MigrationConfirmation?) = switch status {
|
||||
case let .errorMigration(_, migrationError):
|
||||
switch migrationError {
|
||||
case .upgrade:
|
||||
("Database upgrade",
|
||||
"Upgrade and open chat",
|
||||
"",
|
||||
.yesUp)
|
||||
case .downgrade:
|
||||
("Database downgrade",
|
||||
"Downgrade and open chat",
|
||||
NSLocalizedString("Warning: you may lose some data!", comment: ""),
|
||||
.yesUpDown)
|
||||
case let .migrationError(mtrError):
|
||||
("Incompatible database version",
|
||||
nil,
|
||||
"\(NSLocalizedString("Error: ", comment: "")) \(DatabaseErrorView.mtrErrorDescription(mtrError))",
|
||||
nil)
|
||||
}
|
||||
default: ("Error", nil, "Unknown error", nil)
|
||||
}
|
||||
Section {
|
||||
if let button, let confirmation {
|
||||
Button(action: {
|
||||
migrationState = .migration(passphrase: passphrase, confirmation: confirmation, useKeychain: useKeychain)
|
||||
}) {
|
||||
settingsRow("square.and.arrow.down") {
|
||||
Text(button).foregroundColor(.accentColor)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
EmptyView()
|
||||
}
|
||||
} header: {
|
||||
Text(header)
|
||||
} footer: {
|
||||
Text(footer)
|
||||
.font(.callout)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func migrationView(_ passphrase: String, _ confirmation: MigrationConfirmation, _ useKeychain: Bool) -> some View {
|
||||
ZStack {
|
||||
List {
|
||||
Section {} header: {
|
||||
Text("Migrating")
|
||||
}
|
||||
}
|
||||
progressView()
|
||||
}
|
||||
.onAppear {
|
||||
startChat(passphrase, confirmation, useKeychain)
|
||||
}
|
||||
}
|
||||
|
||||
struct OnionView: View {
|
||||
@State var appSettings: AppSettings
|
||||
@State private var onionHosts: OnionHosts = .no
|
||||
var finishMigration: (AppSettings) -> Void
|
||||
|
||||
var body: some View {
|
||||
List {
|
||||
Section {
|
||||
Button(action: {
|
||||
var updated = appSettings.networkConfig!
|
||||
let (hostMode, requiredHostMode) = onionHosts.hostMode
|
||||
updated.hostMode = hostMode
|
||||
updated.requiredHostMode = requiredHostMode
|
||||
updated.socksProxy = nil
|
||||
appSettings.networkConfig = updated
|
||||
finishMigration(appSettings)
|
||||
}) {
|
||||
settingsRow("checkmark") {
|
||||
Text("Apply").foregroundColor(.accentColor)
|
||||
}
|
||||
}
|
||||
} header: {
|
||||
Text("Confirm network settings")
|
||||
} footer: {
|
||||
Text("Please confirm that network settings are correct for this device.")
|
||||
.font(.callout)
|
||||
}
|
||||
|
||||
Section("Network settings") {
|
||||
Picker("Use .onion hosts", selection: $onionHosts) {
|
||||
ForEach(OnionHosts.values, id: \.self) { Text($0.text) }
|
||||
}
|
||||
.frame(height: 36)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func downloadLinkDetails(_ link: String) {
|
||||
let archiveTime = Date.now
|
||||
let ts = archiveTime.ISO8601Format(Date.ISO8601FormatStyle(timeSeparator: .omitted))
|
||||
let archiveName = "simplex-chat.\(ts).zip"
|
||||
let archivePath = getMigrationTempFilesDirectory().appendingPathComponent(archiveName)
|
||||
|
||||
startDownloading(0, link, archivePath.path)
|
||||
}
|
||||
|
||||
private func initTemporaryDatabase() -> (chat_ctrl, User)? {
|
||||
let (status, ctrl) = chatInitTemporaryDatabase(url: tempDatabaseUrl)
|
||||
showErrorOnMigrationIfNeeded(status, $alert)
|
||||
do {
|
||||
if let ctrl, let user = try startChatWithTemporaryDatabase(ctrl: ctrl) {
|
||||
return (ctrl, user)
|
||||
}
|
||||
} catch let error {
|
||||
logger.error("Error while starting chat in temporary database: \(error.localizedDescription)")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
private func startDownloading(_ totalBytes: Int64, _ link: String, _ archivePath: String) {
|
||||
Task {
|
||||
guard let ctrlAndUser = initTemporaryDatabase() else {
|
||||
return migrationState = .downloadFailed(totalBytes: totalBytes, link: link, archivePath: archivePath)
|
||||
}
|
||||
let (ctrl, user) = ctrlAndUser
|
||||
chatReceiver = MigrationChatReceiver(ctrl: ctrl, databaseUrl: tempDatabaseUrl) { msg in
|
||||
await MainActor.run {
|
||||
switch msg {
|
||||
case let .rcvFileProgressXFTP(_, _, receivedSize, totalSize, rcvFileTransfer):
|
||||
migrationState = .downloadProgress(downloadedBytes: receivedSize, totalBytes: totalSize, fileId: rcvFileTransfer.fileId, link: link, archivePath: archivePath, ctrl: ctrl)
|
||||
MigrationToDeviceState.save(.downloadProgress(link: link, archiveName: URL(fileURLWithPath: archivePath).lastPathComponent))
|
||||
case .rcvStandaloneFileComplete:
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
|
||||
// User closed the whole screen before new state was saved
|
||||
if migrationState == nil {
|
||||
MigrationToDeviceState.save(nil)
|
||||
} else {
|
||||
migrationState = .archiveImport(archivePath: archivePath)
|
||||
MigrationToDeviceState.save(.archiveImport(archiveName: URL(fileURLWithPath: archivePath).lastPathComponent))
|
||||
}
|
||||
}
|
||||
case .rcvFileError:
|
||||
alert = .error(title: "Download failed", error: "File was deleted or link is invalid")
|
||||
migrationState = .downloadFailed(totalBytes: totalBytes, link: link, archivePath: archivePath)
|
||||
case .chatError(_, .error(.noRcvFileUser)):
|
||||
alert = .error(title: "Download failed", error: "File was deleted or link is invalid")
|
||||
migrationState = .downloadFailed(totalBytes: totalBytes, link: link, archivePath: archivePath)
|
||||
default:
|
||||
logger.debug("unsupported event: \(msg.responseType)")
|
||||
}
|
||||
}
|
||||
}
|
||||
chatReceiver?.start()
|
||||
|
||||
let (res, error) = await downloadStandaloneFile(user: user, url: link, file: CryptoFile.plain(URL(fileURLWithPath: archivePath).lastPathComponent), ctrl: ctrl)
|
||||
if res == nil {
|
||||
await MainActor.run {
|
||||
migrationState = .downloadFailed(totalBytes: totalBytes, link: link, archivePath: archivePath)
|
||||
}
|
||||
return alert = .error(title: "Error downloading the archive", error: error ?? "")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func importArchive(_ archivePath: String) {
|
||||
Task {
|
||||
do {
|
||||
if !hasChatCtrl() {
|
||||
chatInitControllerRemovingDatabases()
|
||||
}
|
||||
try await apiDeleteStorage()
|
||||
do {
|
||||
let config = ArchiveConfig(archivePath: archivePath)
|
||||
let archiveErrors = try await apiImportArchive(config: config)
|
||||
if !archiveErrors.isEmpty {
|
||||
alert = .chatImportedWithErrors()
|
||||
}
|
||||
await MainActor.run {
|
||||
migrationState = .passphrase(passphrase: "")
|
||||
MigrationToDeviceState.save(.passphrase)
|
||||
}
|
||||
} catch let error {
|
||||
await MainActor.run {
|
||||
migrationState = .archiveImportFailed(archivePath: archivePath)
|
||||
}
|
||||
alert = .error(title: "Error importing chat database", error: responseError(error))
|
||||
}
|
||||
} catch let error {
|
||||
await MainActor.run {
|
||||
migrationState = .archiveImportFailed(archivePath: archivePath)
|
||||
}
|
||||
alert = .error(title: "Error deleting chat database", error: responseError(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private func stopArchiveDownloading(_ fileId: Int64, _ ctrl: chat_ctrl) async {
|
||||
_ = await apiCancelFile(fileId: fileId, ctrl: ctrl)
|
||||
}
|
||||
|
||||
private func startChat(_ passphrase: String, _ confirmation: MigrationConfirmation, _ useKeychain: Bool) {
|
||||
if useKeychain {
|
||||
_ = kcDatabasePassword.set(passphrase)
|
||||
} else {
|
||||
_ = kcDatabasePassword.remove()
|
||||
}
|
||||
storeDBPassphraseGroupDefault.set(useKeychain)
|
||||
initialRandomDBPassphraseGroupDefault.set(false)
|
||||
AppChatState.shared.set(.active)
|
||||
Task {
|
||||
do {
|
||||
resetChatCtrl()
|
||||
try initializeChat(start: false, confirmStart: false, dbKey: passphrase, refreshInvitations: true, confirmMigrations: confirmation)
|
||||
var appSettings = try apiGetAppSettings(settings: AppSettings.current.prepareForExport())
|
||||
let hasOnionConfigured = appSettings.networkConfig?.socksProxy != nil || appSettings.networkConfig?.hostMode == .onionHost
|
||||
appSettings.networkConfig?.socksProxy = nil
|
||||
appSettings.networkConfig?.hostMode = .publicHost
|
||||
appSettings.networkConfig?.requiredHostMode = true
|
||||
await MainActor.run {
|
||||
if hasOnionConfigured {
|
||||
migrationState = .onion(appSettings: appSettings)
|
||||
} else {
|
||||
finishMigration(appSettings)
|
||||
}
|
||||
}
|
||||
} catch let error {
|
||||
hideView()
|
||||
AlertManager.shared.showAlert(Alert(title: Text("Error starting chat"), message: Text(responseError(error))))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func finishMigration(_ appSettings: AppSettings) {
|
||||
do {
|
||||
try? FileManager.default.removeItem(at: getMigrationTempFilesDirectory())
|
||||
MigrationToDeviceState.save(nil)
|
||||
appSettings.importIntoApp()
|
||||
try SimpleX.startChat(refreshInvitations: true)
|
||||
AlertManager.shared.showAlertMsg(title: "Chat migrated!", message: "Finalize migration on another device.")
|
||||
} catch let error {
|
||||
AlertManager.shared.showAlert(Alert(title: Text("Error starting chat"), message: Text(responseError(error))))
|
||||
}
|
||||
hideView()
|
||||
}
|
||||
|
||||
private func hideView() {
|
||||
onboardingStageDefault.set(.onboardingComplete)
|
||||
m.onboardingStage = .onboardingComplete
|
||||
dismiss()
|
||||
}
|
||||
|
||||
private func strHasSimplexFileLink(_ text: String) -> Bool {
|
||||
text.starts(with: "simplex:/file") || text.starts(with: "https://simplex.chat/file")
|
||||
}
|
||||
|
||||
private static func urlForTemporaryDatabase() -> URL {
|
||||
URL(fileURLWithPath: generateNewFileName(getMigrationTempFilesDirectory().path + "/" + "migration", "db", fullPath: true))
|
||||
}
|
||||
}
|
||||
|
||||
private struct PassphraseEnteringView: View {
|
||||
@Binding var migrationState: MigrationToState?
|
||||
@State private var useKeychain = true
|
||||
@State var currentKey: String
|
||||
@State private var verifyingPassphrase: Bool = false
|
||||
@FocusState private var keyboardVisible: Bool
|
||||
@Binding var alert: MigrateToDeviceViewAlert?
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
List {
|
||||
Section {
|
||||
settingsRow("key", color: .secondary) {
|
||||
Toggle("Save passphrase in Keychain", isOn: $useKeychain)
|
||||
}
|
||||
|
||||
PassphraseField(key: $currentKey, placeholder: "Current passphrase…", valid: validKey(currentKey))
|
||||
.focused($keyboardVisible)
|
||||
Button(action: {
|
||||
verifyingPassphrase = true
|
||||
hideKeyboard()
|
||||
Task {
|
||||
let (status, _) = chatInitTemporaryDatabase(url: getAppDatabasePath(), key: currentKey, confirmation: .yesUp)
|
||||
let success = switch status {
|
||||
case .ok, .invalidConfirmation: true
|
||||
default: false
|
||||
}
|
||||
if success {
|
||||
await MainActor.run {
|
||||
migrationState = .migration(passphrase: currentKey, confirmation: .yesUp, useKeychain: useKeychain)
|
||||
}
|
||||
} else if case .errorMigration = status {
|
||||
await MainActor.run {
|
||||
migrationState = .migrationConfirmation(status: status, passphrase: currentKey, useKeychain: useKeychain)
|
||||
}
|
||||
} else {
|
||||
showErrorOnMigrationIfNeeded(status, $alert)
|
||||
}
|
||||
verifyingPassphrase = false
|
||||
}
|
||||
}) {
|
||||
settingsRow("key", color: .secondary) {
|
||||
Text("Open chat")
|
||||
}
|
||||
}
|
||||
.disabled(verifyingPassphrase || currentKey.isEmpty)
|
||||
} header: {
|
||||
Text("Enter passphrase")
|
||||
} footer: {
|
||||
VStack(alignment: .leading, spacing: 16) {
|
||||
if useKeychain {
|
||||
Text("iOS Keychain is used to securely store passphrase - it allows receiving push notifications.")
|
||||
} else {
|
||||
Text("You have to enter passphrase every time the app starts - it is not stored on the device.")
|
||||
Text("**Please note**: you will NOT be able to recover or change passphrase if you lose it.")
|
||||
Text("**Warning**: Instant push notifications require passphrase saved in Keychain.")
|
||||
}
|
||||
}
|
||||
.font(.callout)
|
||||
.padding(.top, 1)
|
||||
.onTapGesture { keyboardVisible = false }
|
||||
}
|
||||
.onAppear {
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
|
||||
keyboardVisible = true
|
||||
}
|
||||
}
|
||||
}
|
||||
if verifyingPassphrase {
|
||||
progressView()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func showErrorOnMigrationIfNeeded(_ status: DBMigrationResult, _ alert: Binding<MigrateToDeviceViewAlert?>) {
|
||||
switch status {
|
||||
case .invalidConfirmation:
|
||||
alert.wrappedValue = .invalidConfirmation()
|
||||
case .errorNotADatabase:
|
||||
alert.wrappedValue = .wrongPassphrase()
|
||||
case .errorKeychain:
|
||||
alert.wrappedValue = .keychainError()
|
||||
case let .errorSQL(_, error):
|
||||
alert.wrappedValue = .databaseError(message: error)
|
||||
case let .unknown(error):
|
||||
alert.wrappedValue = .unknownError(message: error)
|
||||
case .errorMigration: ()
|
||||
case .ok: ()
|
||||
}
|
||||
}
|
||||
|
||||
private func progressView() -> some View {
|
||||
VStack {
|
||||
ProgressView().scaleEffect(2)
|
||||
}
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity )
|
||||
}
|
||||
|
||||
private class MigrationChatReceiver {
|
||||
let ctrl: chat_ctrl
|
||||
let databaseUrl: URL
|
||||
let processReceivedMsg: (ChatResponse) async -> Void
|
||||
private var receiveLoop: Task<Void, Never>?
|
||||
private var receiveMessages = true
|
||||
|
||||
init(ctrl: chat_ctrl, databaseUrl: URL, _ processReceivedMsg: @escaping (ChatResponse) async -> Void) {
|
||||
self.ctrl = ctrl
|
||||
self.databaseUrl = databaseUrl
|
||||
self.processReceivedMsg = processReceivedMsg
|
||||
}
|
||||
|
||||
func start() {
|
||||
logger.debug("MigrationChatReceiver.start")
|
||||
receiveMessages = true
|
||||
if receiveLoop != nil { return }
|
||||
receiveLoop = Task { await receiveMsgLoop() }
|
||||
}
|
||||
|
||||
func receiveMsgLoop() async {
|
||||
// TODO use function that has timeout
|
||||
if let msg = await chatRecvMsg(ctrl) {
|
||||
Task {
|
||||
await TerminalItems.shared.add(.resp(.now, msg))
|
||||
}
|
||||
logger.debug("processReceivedMsg: \(msg.responseType)")
|
||||
await processReceivedMsg(msg)
|
||||
}
|
||||
if self.receiveMessages {
|
||||
_ = try? await Task.sleep(nanoseconds: 7_500_000)
|
||||
await receiveMsgLoop()
|
||||
}
|
||||
}
|
||||
|
||||
func stopAndCleanUp() {
|
||||
logger.debug("MigrationChatReceiver.stop")
|
||||
receiveMessages = false
|
||||
receiveLoop?.cancel()
|
||||
receiveLoop = nil
|
||||
chat_close_store(ctrl)
|
||||
try? FileManager.default.removeItem(atPath: "\(databaseUrl.path)_chat.db")
|
||||
try? FileManager.default.removeItem(atPath: "\(databaseUrl.path)_agent.db")
|
||||
}
|
||||
}
|
||||
|
||||
struct MigrateToDevice_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
MigrateToDevice(migrationState: Binding.constant(.pasteOrScanLink))
|
||||
}
|
||||
}
|
||||
@@ -86,7 +86,7 @@ struct NewChatView: View {
|
||||
}
|
||||
}
|
||||
if case .connect = selection {
|
||||
ConnectView(showQRCodeScanner: showQRCodeScanner, pastedLink: $pastedLink, alert: $alert)
|
||||
ConnectView(showQRCodeScanner: $showQRCodeScanner, pastedLink: $pastedLink, alert: $alert)
|
||||
.transition(.move(edge: .trailing))
|
||||
}
|
||||
}
|
||||
@@ -284,8 +284,7 @@ private struct InviteView: View {
|
||||
|
||||
private struct ConnectView: View {
|
||||
@Environment(\.dismiss) var dismiss: DismissAction
|
||||
@State var showQRCodeScanner = false
|
||||
@State private var cameraAuthorizationStatus: AVAuthorizationStatus?
|
||||
@Binding var showQRCodeScanner: Bool
|
||||
@Binding var pastedLink: String
|
||||
@Binding var alert: NewChatViewAlert?
|
||||
@State private var sheet: PlanAndConnectActionSheet?
|
||||
@@ -295,32 +294,13 @@ private struct ConnectView: View {
|
||||
Section("Paste the link you received") {
|
||||
pasteLinkView()
|
||||
}
|
||||
|
||||
scanCodeView()
|
||||
Section("Or scan QR code") {
|
||||
ScannerInView(showQRCodeScanner: $showQRCodeScanner, processQRCode: processQRCode)
|
||||
}
|
||||
}
|
||||
.actionSheet(item: $sheet) { s in
|
||||
planAndConnectActionSheet(s, dismiss: true, cleanup: { pastedLink = "" })
|
||||
}
|
||||
.onAppear {
|
||||
let status = AVCaptureDevice.authorizationStatus(for: .video)
|
||||
cameraAuthorizationStatus = status
|
||||
if showQRCodeScanner {
|
||||
switch status {
|
||||
case .notDetermined: askCameraAuthorization()
|
||||
case .restricted: showQRCodeScanner = false
|
||||
case .denied: showQRCodeScanner = false
|
||||
case .authorized: ()
|
||||
@unknown default: askCameraAuthorization()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func askCameraAuthorization(_ cb: (() -> Void)? = nil) {
|
||||
AVCaptureDevice.requestAccess(for: .video) { allowed in
|
||||
cameraAuthorizationStatus = AVCaptureDevice.authorizationStatus(for: .video)
|
||||
if allowed { cb?() }
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder private func pasteLinkView() -> some View {
|
||||
@@ -351,8 +331,45 @@ private struct ConnectView: View {
|
||||
}
|
||||
}
|
||||
|
||||
private func scanCodeView() -> some View {
|
||||
Section("Or scan QR code") {
|
||||
private func processQRCode(_ resp: Result<ScanResult, ScanError>) {
|
||||
switch resp {
|
||||
case let .success(r):
|
||||
let link = r.string
|
||||
if strIsSimplexLink(r.string) {
|
||||
connect(link)
|
||||
} else {
|
||||
alert = .newChatSomeAlert(alert: .someAlert(
|
||||
alert: mkAlert(title: "Invalid QR code", message: "The code you scanned is not a SimpleX link QR code."),
|
||||
id: "processQRCode: code is not a SimpleX link"
|
||||
))
|
||||
}
|
||||
case let .failure(e):
|
||||
logger.error("processQRCode QR code error: \(e.localizedDescription)")
|
||||
alert = .newChatSomeAlert(alert: .someAlert(
|
||||
alert: mkAlert(title: "Invalid QR code", message: "Error scanning code: \(e.localizedDescription)"),
|
||||
id: "processQRCode: failure"
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
private func connect(_ link: String) {
|
||||
planAndConnect(
|
||||
link,
|
||||
showAlert: { alert = .planAndConnectAlert(alert: $0) },
|
||||
showActionSheet: { sheet = $0 },
|
||||
dismiss: true,
|
||||
incognito: nil
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
struct ScannerInView: View {
|
||||
@Binding var showQRCodeScanner: Bool
|
||||
let processQRCode: (_ resp: Result<ScanResult, ScanError>) -> Void
|
||||
@State private var cameraAuthorizationStatus: AVAuthorizationStatus?
|
||||
|
||||
var body: some View {
|
||||
Group {
|
||||
if showQRCodeScanner, case .authorized = cameraAuthorizationStatus {
|
||||
CodeScannerView(codeTypes: [.qr], scanMode: .continuous, completion: processQRCode)
|
||||
.aspectRatio(1, contentMode: .fit)
|
||||
@@ -396,37 +413,26 @@ private struct ConnectView: View {
|
||||
.disabled(cameraAuthorizationStatus == .restricted)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func processQRCode(_ resp: Result<ScanResult, ScanError>) {
|
||||
switch resp {
|
||||
case let .success(r):
|
||||
let link = r.string
|
||||
if strIsSimplexLink(r.string) {
|
||||
connect(link)
|
||||
} else {
|
||||
alert = .newChatSomeAlert(alert: .someAlert(
|
||||
alert: mkAlert(title: "Invalid QR code", message: "The code you scanned is not a SimpleX link QR code."),
|
||||
id: "processQRCode: code is not a SimpleX link"
|
||||
))
|
||||
.onAppear {
|
||||
let status = AVCaptureDevice.authorizationStatus(for: .video)
|
||||
cameraAuthorizationStatus = status
|
||||
if showQRCodeScanner {
|
||||
switch status {
|
||||
case .notDetermined: askCameraAuthorization()
|
||||
case .restricted: showQRCodeScanner = false
|
||||
case .denied: showQRCodeScanner = false
|
||||
case .authorized: ()
|
||||
@unknown default: askCameraAuthorization()
|
||||
}
|
||||
}
|
||||
case let .failure(e):
|
||||
logger.error("processQRCode QR code error: \(e.localizedDescription)")
|
||||
alert = .newChatSomeAlert(alert: .someAlert(
|
||||
alert: mkAlert(title: "Invalid QR code", message: "Error scanning code: \(e.localizedDescription)"),
|
||||
id: "processQRCode: failure"
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
private func connect(_ link: String) {
|
||||
planAndConnect(
|
||||
link,
|
||||
showAlert: { alert = .planAndConnectAlert(alert: $0) },
|
||||
showActionSheet: { sheet = $0 },
|
||||
dismiss: true,
|
||||
incognito: nil
|
||||
)
|
||||
func askCameraAuthorization(_ cb: (() -> Void)? = nil) {
|
||||
AVCaptureDevice.requestAccess(for: .video) { allowed in
|
||||
cameraAuthorizationStatus = AVCaptureDevice.authorizationStatus(for: .video)
|
||||
if allowed { cb?() }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -166,8 +166,10 @@ private func createProfile(_ displayName: String, showAlert: (UserProfileAlert)
|
||||
)
|
||||
let m = ChatModel.shared
|
||||
do {
|
||||
AppChatState.shared.set(.active)
|
||||
m.currentUser = try apiCreateActiveUser(profile)
|
||||
if m.users.isEmpty {
|
||||
// .isEmpty check is redundant here, but it makes it clearer what is going on
|
||||
if m.users.isEmpty || m.users.allSatisfy({ $0.user.hidden }) {
|
||||
try startChat()
|
||||
withAnimation {
|
||||
onboardingStageDefault.set(.step3_CreateSimpleXAddress)
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import SimpleXChat
|
||||
|
||||
struct SimpleXInfo: View {
|
||||
@EnvironmentObject var m: ChatModel
|
||||
@@ -44,6 +45,15 @@ struct SimpleXInfo: View {
|
||||
if onboarding {
|
||||
OnboardingActionButton()
|
||||
Spacer()
|
||||
|
||||
Button {
|
||||
m.migrationState = .pasteOrScanLink
|
||||
} label: {
|
||||
Label("Migrate from another device", systemImage: "tray.and.arrow.down")
|
||||
.font(.subheadline)
|
||||
}
|
||||
.padding(.bottom, 8)
|
||||
.frame(maxWidth: .infinity)
|
||||
}
|
||||
|
||||
Button {
|
||||
@@ -54,9 +64,24 @@ struct SimpleXInfo: View {
|
||||
}
|
||||
.padding(.bottom, 8)
|
||||
.frame(maxWidth: .infinity)
|
||||
|
||||
}
|
||||
.frame(minHeight: g.size.height)
|
||||
}
|
||||
.sheet(isPresented: Binding(
|
||||
get: { m.migrationState != nil },
|
||||
set: { _ in
|
||||
m.migrationState = nil
|
||||
MigrationToDeviceState.save(nil) }
|
||||
)) {
|
||||
NavigationView {
|
||||
VStack(alignment: .leading) {
|
||||
MigrateToDevice(migrationState: $m.migrationState)
|
||||
}
|
||||
.navigationTitle("Migrate here")
|
||||
.background(colorScheme == .light ? Color(uiColor: .tertiarySystemGroupedBackground) : .clear)
|
||||
}
|
||||
}
|
||||
.sheet(isPresented: $showHowItWorks) {
|
||||
HowItWorks(onboarding: onboarding)
|
||||
}
|
||||
@@ -87,6 +112,7 @@ struct SimpleXInfo: View {
|
||||
|
||||
struct OnboardingActionButton: View {
|
||||
@EnvironmentObject var m: ChatModel
|
||||
@Environment(\.colorScheme) var colorScheme
|
||||
|
||||
var body: some View {
|
||||
if m.currentUser == nil {
|
||||
@@ -111,6 +137,21 @@ struct OnboardingActionButton: View {
|
||||
.frame(maxWidth: .infinity)
|
||||
.padding(.bottom)
|
||||
}
|
||||
|
||||
private func actionButton(_ label: LocalizedStringKey, action: @escaping () -> Void) -> some View {
|
||||
Button {
|
||||
withAnimation {
|
||||
action()
|
||||
}
|
||||
} label: {
|
||||
HStack {
|
||||
Text(label).font(.title2)
|
||||
Image(systemName: "greaterthan")
|
||||
}
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
.padding(.bottom)
|
||||
}
|
||||
}
|
||||
|
||||
struct SimpleXInfo_Previews: PreviewProvider {
|
||||
|
||||
@@ -344,6 +344,37 @@ private let versionDescriptions: [VersionDescription] = [
|
||||
description: "Thanks to the users – [contribute via Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)!"
|
||||
),
|
||||
]
|
||||
),
|
||||
VersionDescription(
|
||||
version: "v5.6",
|
||||
post: URL(string: "https://simplex.chat/blog/20240323-simplex-network-privacy-non-profit-v5-6-quantum-resistant-e2e-encryption-simple-migration.html"),
|
||||
features: [
|
||||
FeatureDescription(
|
||||
icon: "key",
|
||||
title: "Quantum resistant encryption",
|
||||
description: "Enable in direct chats (BETA)!"
|
||||
),
|
||||
FeatureDescription(
|
||||
icon: "tray.and.arrow.up",
|
||||
title: "App data migration",
|
||||
description: "Migrate to another device via QR code."
|
||||
),
|
||||
FeatureDescription(
|
||||
icon: "phone",
|
||||
title: "Picture-in-picture calls",
|
||||
description: "Use the app while in the call."
|
||||
),
|
||||
FeatureDescription(
|
||||
icon: "hand.raised",
|
||||
title: "Safer groups",
|
||||
description: "Admins can block a member for all."
|
||||
),
|
||||
FeatureDescription(
|
||||
icon: "character",
|
||||
title: "Hungarian interface",
|
||||
description: "Thanks to the users – [contribute via Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)!"
|
||||
),
|
||||
]
|
||||
)
|
||||
]
|
||||
|
||||
|
||||
@@ -58,7 +58,7 @@ struct ConnectDesktopView: View {
|
||||
var body: some View {
|
||||
if viaSettings {
|
||||
viewBody
|
||||
.modifier(BackButton(label: "Back") {
|
||||
.modifier(BackButton(label: "Back", disabled: Binding.constant(false)) {
|
||||
if m.activeRemoteCtrl {
|
||||
alert = .disconnectDesktop(action: .back)
|
||||
} else {
|
||||
|
||||
@@ -51,9 +51,10 @@ struct AdvancedNetworkSettings: View {
|
||||
}
|
||||
.disabled(currentNetCfg == NetCfg.proxyDefaults)
|
||||
|
||||
timeoutSettingPicker("TCP connection timeout", selection: $netCfg.tcpConnectTimeout, values: [7_500000, 10_000000, 15_000000, 20_000000, 30_000000, 45_000000], label: secondsLabel)
|
||||
timeoutSettingPicker("TCP connection timeout", selection: $netCfg.tcpConnectTimeout, values: [5_000000, 7_500000, 10_000000, 15_000000, 20_000000, 30_000000, 40_000000], label: secondsLabel)
|
||||
timeoutSettingPicker("Protocol timeout", selection: $netCfg.tcpTimeout, values: [5_000000, 7_000000, 10_000000, 15_000000, 20_000000, 30_000000], label: secondsLabel)
|
||||
timeoutSettingPicker("Protocol timeout per KB", selection: $netCfg.tcpTimeoutPerKb, values: [15_000, 30_000, 45_000, 60_000, 90_000, 120_000], label: secondsLabel)
|
||||
timeoutSettingPicker("Protocol timeout per KB", selection: $netCfg.tcpTimeoutPerKb, values: [2_500, 5_000, 10_000, 15_000, 20_000, 30_000], label: secondsLabel)
|
||||
intSettingPicker("Receiving concurrency", selection: $netCfg.rcvConcurrency, values: [1, 2, 4, 8, 12, 16, 24], label: "")
|
||||
timeoutSettingPicker("PING interval", selection: $netCfg.smpPingInterval, values: [120_000000, 300_000000, 600_000000, 1200_000000, 2400_000000, 3600_000000], label: secondsLabel)
|
||||
intSettingPicker("PING count", selection: $netCfg.smpPingCount, values: [1, 2, 3, 5, 8], label: "")
|
||||
Toggle("Enable TCP keep-alive", isOn: $enableKeepAlive)
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
//
|
||||
// AppSettings.swift
|
||||
// SimpleX (iOS)
|
||||
//
|
||||
// Created by Avently on 26.02.2024.
|
||||
// Copyright © 2024 SimpleX Chat. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import SimpleXChat
|
||||
import SwiftUI
|
||||
|
||||
extension AppSettings {
|
||||
public func importIntoApp() {
|
||||
let def = UserDefaults.standard
|
||||
if var val = networkConfig {
|
||||
// migrating from Android/desktop BUT shouldn't be here ever because it should be changed in migration stage
|
||||
if case .onionViaSocks = val.hostMode {
|
||||
val.hostMode = .publicHost
|
||||
val.requiredHostMode = true
|
||||
}
|
||||
val.socksProxy = nil
|
||||
setNetCfg(val)
|
||||
}
|
||||
if let val = privacyEncryptLocalFiles { privacyEncryptLocalFilesGroupDefault.set(val) }
|
||||
if let val = privacyAcceptImages {
|
||||
privacyAcceptImagesGroupDefault.set(val)
|
||||
def.setValue(val, forKey: DEFAULT_PRIVACY_ACCEPT_IMAGES)
|
||||
}
|
||||
if let val = privacyLinkPreviews { def.setValue(val, forKey: DEFAULT_PRIVACY_LINK_PREVIEWS) }
|
||||
if let val = privacyShowChatPreviews { def.setValue(val, forKey: DEFAULT_PRIVACY_SHOW_CHAT_PREVIEWS) }
|
||||
if let val = privacySaveLastDraft { def.setValue(val, forKey: DEFAULT_PRIVACY_SAVE_LAST_DRAFT) }
|
||||
if let val = privacyProtectScreen { def.setValue(val, forKey: DEFAULT_PRIVACY_PROTECT_SCREEN) }
|
||||
if let val = notificationMode { ChatModel.shared.notificationMode = val.toNotificationsMode() }
|
||||
if let val = notificationPreviewMode { ntfPreviewModeGroupDefault.set(val) }
|
||||
if let val = webrtcPolicyRelay { def.setValue(val, forKey: DEFAULT_WEBRTC_POLICY_RELAY) }
|
||||
if let val = webrtcICEServers { def.setValue(val, forKey: DEFAULT_WEBRTC_ICE_SERVERS) }
|
||||
if let val = confirmRemoteSessions { def.setValue(val, forKey: DEFAULT_CONFIRM_REMOTE_SESSIONS) }
|
||||
if let val = connectRemoteViaMulticast { def.setValue(val, forKey: DEFAULT_CONNECT_REMOTE_VIA_MULTICAST) }
|
||||
if let val = connectRemoteViaMulticastAuto { def.setValue(val, forKey: DEFAULT_CONNECT_REMOTE_VIA_MULTICAST_AUTO) }
|
||||
if let val = developerTools { def.setValue(val, forKey: DEFAULT_DEVELOPER_TOOLS) }
|
||||
if let val = confirmDBUpgrades { confirmDBUpgradesGroupDefault.set(val) }
|
||||
if let val = androidCallOnLockScreen { def.setValue(val.rawValue, forKey: ANDROID_DEFAULT_CALL_ON_LOCK_SCREEN) }
|
||||
if let val = iosCallKitEnabled { callKitEnabledGroupDefault.set(val) }
|
||||
if let val = iosCallKitCallsInRecents { def.setValue(val, forKey: DEFAULT_CALL_KIT_CALLS_IN_RECENTS) }
|
||||
}
|
||||
|
||||
public static var current: AppSettings {
|
||||
let def = UserDefaults.standard
|
||||
var c = AppSettings.defaults
|
||||
c.networkConfig = getNetCfg()
|
||||
c.privacyEncryptLocalFiles = privacyEncryptLocalFilesGroupDefault.get()
|
||||
c.privacyAcceptImages = privacyAcceptImagesGroupDefault.get()
|
||||
c.privacyLinkPreviews = def.bool(forKey: DEFAULT_PRIVACY_LINK_PREVIEWS)
|
||||
c.privacyShowChatPreviews = def.bool(forKey: DEFAULT_PRIVACY_SHOW_CHAT_PREVIEWS)
|
||||
c.privacySaveLastDraft = def.bool(forKey: DEFAULT_PRIVACY_SAVE_LAST_DRAFT)
|
||||
c.privacyProtectScreen = def.bool(forKey: DEFAULT_PRIVACY_PROTECT_SCREEN)
|
||||
c.notificationMode = AppSettingsNotificationMode.from(ChatModel.shared.notificationMode)
|
||||
c.notificationPreviewMode = ntfPreviewModeGroupDefault.get()
|
||||
c.webrtcPolicyRelay = def.bool(forKey: DEFAULT_WEBRTC_POLICY_RELAY)
|
||||
c.webrtcICEServers = def.stringArray(forKey: DEFAULT_WEBRTC_ICE_SERVERS)
|
||||
c.confirmRemoteSessions = def.bool(forKey: DEFAULT_CONFIRM_REMOTE_SESSIONS)
|
||||
c.connectRemoteViaMulticast = def.bool(forKey: DEFAULT_CONNECT_REMOTE_VIA_MULTICAST)
|
||||
c.connectRemoteViaMulticastAuto = def.bool(forKey: DEFAULT_CONNECT_REMOTE_VIA_MULTICAST_AUTO)
|
||||
c.developerTools = def.bool(forKey: DEFAULT_DEVELOPER_TOOLS)
|
||||
c.confirmDBUpgrades = confirmDBUpgradesGroupDefault.get()
|
||||
c.androidCallOnLockScreen = AppSettingsLockScreenCalls(rawValue: def.string(forKey: ANDROID_DEFAULT_CALL_ON_LOCK_SCREEN)!)
|
||||
c.iosCallKitEnabled = callKitEnabledGroupDefault.get()
|
||||
c.iosCallKitCallsInRecents = def.bool(forKey: DEFAULT_CALL_KIT_CALLS_IN_RECENTS)
|
||||
return c
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,7 @@ import SimpleXChat
|
||||
struct DeveloperView: View {
|
||||
@AppStorage(DEFAULT_DEVELOPER_TOOLS) private var developerTools = false
|
||||
@AppStorage(GROUP_DEFAULT_CONFIRM_DB_UPGRADES, store: groupDefaults) private var confirmDatabaseUpgrades = false
|
||||
@AppStorage(GROUP_DEFAULT_PQ_EXPERIMENTAL_ENABLED, store: groupDefaults) private var pqExperimentalEnabled = false
|
||||
@Environment(\.colorScheme) var colorScheme
|
||||
|
||||
var body: some View {
|
||||
@@ -43,27 +44,32 @@ struct DeveloperView: View {
|
||||
(developerTools ? Text("Show:") : Text("Hide:")) + Text(" ") + Text("Database IDs and Transport isolation option.")
|
||||
}
|
||||
|
||||
// Section {
|
||||
// settingsRow("arrow.up.doc") {
|
||||
// Toggle("Send videos and files via XFTP", isOn: $xftpSendEnabled)
|
||||
// .onChange(of: xftpSendEnabled) { _ in
|
||||
// do {
|
||||
// try setXFTPConfig(getXFTPCfg())
|
||||
// } catch {
|
||||
// logger.error("setXFTPConfig: cannot set XFTP config \(responseError(error))")
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// } header: {
|
||||
// Text("Experimental")
|
||||
// } footer: {
|
||||
// if xftpSendEnabled {
|
||||
// Text("v4.6.1+ is required to receive via XFTP.")
|
||||
// }
|
||||
// }
|
||||
if developerTools {
|
||||
Section {
|
||||
settingsRow("key") {
|
||||
Toggle("Post-quantum E2EE", isOn: $pqExperimentalEnabled)
|
||||
.onChange(of: pqExperimentalEnabled) {
|
||||
setPQExperimentalEnabled($0)
|
||||
}
|
||||
}
|
||||
} header: {
|
||||
Text(String("Experimental"))
|
||||
} footer: {
|
||||
Text(String("In this version applies only to new contacts."))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func setPQExperimentalEnabled(_ enable: Bool) {
|
||||
do {
|
||||
try apiSetPQEncryption(enable)
|
||||
} catch let error {
|
||||
let err = responseError(error)
|
||||
logger.error("apiSetPQEncryption \(err)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct DeveloperView_Previews: PreviewProvider {
|
||||
|
||||
@@ -24,6 +24,7 @@ private enum NetworkAlert: Identifiable {
|
||||
}
|
||||
|
||||
struct NetworkAndServers: View {
|
||||
@EnvironmentObject var m: ChatModel
|
||||
@AppStorage(DEFAULT_DEVELOPER_TOOLS) private var developerTools = false
|
||||
@State private var cfgLoaded = false
|
||||
@State private var currentNetCfg = NetCfg.defaults
|
||||
@@ -82,6 +83,14 @@ struct NetworkAndServers: View {
|
||||
Text("WebRTC ICE servers")
|
||||
}
|
||||
}
|
||||
|
||||
Section("Network connection") {
|
||||
HStack {
|
||||
Text(m.networkInfo.networkType.text)
|
||||
Spacer()
|
||||
Image(systemName: "circle.fill").foregroundColor(m.networkInfo.online ? .green : .red)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
|
||||
@@ -76,6 +76,10 @@ struct NotificationsView: View {
|
||||
Text(m.notificationPreview.label)
|
||||
}
|
||||
}
|
||||
|
||||
if let server = m.notificationServer {
|
||||
smpServers("Push server", [server])
|
||||
}
|
||||
} header: {
|
||||
Text("Push notifications")
|
||||
} footer: {
|
||||
@@ -87,6 +91,9 @@ struct NotificationsView: View {
|
||||
}
|
||||
}
|
||||
.disabled(legacyDatabase)
|
||||
.onAppear {
|
||||
(m.savedToken, m.tokenStatus, m.notificationMode, m.notificationServer) = apiGetNtfToken()
|
||||
}
|
||||
}
|
||||
|
||||
private func notificationAlert(_ alert: NotificationAlert, _ token: DeviceToken) -> Alert {
|
||||
@@ -125,6 +132,7 @@ struct NotificationsView: View {
|
||||
m.tokenStatus = .new
|
||||
notificationMode = .off
|
||||
m.notificationMode = .off
|
||||
m.notificationServer = nil
|
||||
}
|
||||
} catch let error {
|
||||
await MainActor.run {
|
||||
@@ -135,11 +143,13 @@ struct NotificationsView: View {
|
||||
}
|
||||
default:
|
||||
do {
|
||||
let status = try await apiRegisterToken(token: token, notificationMode: mode)
|
||||
let _ = try await apiRegisterToken(token: token, notificationMode: mode)
|
||||
let (_, tknStatus, ntfMode, ntfServer) = apiGetNtfToken()
|
||||
await MainActor.run {
|
||||
m.tokenStatus = status
|
||||
notificationMode = mode
|
||||
m.notificationMode = mode
|
||||
m.tokenStatus = tknStatus
|
||||
notificationMode = ntfMode
|
||||
m.notificationMode = ntfMode
|
||||
m.notificationServer = ntfServer
|
||||
}
|
||||
} catch let error {
|
||||
await MainActor.run {
|
||||
|
||||
@@ -31,7 +31,7 @@ struct ProtocolServerView: View {
|
||||
ProgressView().scaleEffect(2)
|
||||
}
|
||||
}
|
||||
.modifier(BackButton(label: "Your \(proto) servers") {
|
||||
.modifier(BackButton(label: "Your \(proto) servers", disabled: Binding.constant(false)) {
|
||||
server = serverToEdit
|
||||
dismiss()
|
||||
})
|
||||
@@ -117,6 +117,7 @@ struct ProtocolServerView: View {
|
||||
|
||||
struct BackButton: ViewModifier {
|
||||
var label: LocalizedStringKey = "Back"
|
||||
@Binding var disabled: Bool
|
||||
var action: () -> Void
|
||||
|
||||
func body(content: Content) -> some View {
|
||||
@@ -130,6 +131,7 @@ struct BackButton: ViewModifier {
|
||||
Text(label)
|
||||
}
|
||||
}
|
||||
.disabled(disabled)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -95,7 +95,7 @@ struct ProtocolServersView: View {
|
||||
.sheet(isPresented: $showScanProtoServer) {
|
||||
ScanProtocolServer(servers: $servers)
|
||||
}
|
||||
.modifier(BackButton {
|
||||
.modifier(BackButton(disabled: Binding.constant(false)) {
|
||||
if saveDisabled {
|
||||
dismiss()
|
||||
justOpened = false
|
||||
|
||||
@@ -27,7 +27,7 @@ let DEFAULT_NOTIFICATION_ALERT_SHOWN = "notificationAlertShown"
|
||||
let DEFAULT_WEBRTC_POLICY_RELAY = "webrtcPolicyRelay"
|
||||
let DEFAULT_WEBRTC_ICE_SERVERS = "webrtcICEServers"
|
||||
let DEFAULT_CALL_KIT_CALLS_IN_RECENTS = "callKitCallsInRecents"
|
||||
let DEFAULT_PRIVACY_ACCEPT_IMAGES = "privacyAcceptImages"
|
||||
let DEFAULT_PRIVACY_ACCEPT_IMAGES = "privacyAcceptImages" // unused. Use GROUP_DEFAULT_PRIVACY_ACCEPT_IMAGES instead
|
||||
let DEFAULT_PRIVACY_LINK_PREVIEWS = "privacyLinkPreviews"
|
||||
let DEFAULT_PRIVACY_SIMPLEX_LINK_MODE = "privacySimplexLinkMode"
|
||||
let DEFAULT_PRIVACY_SHOW_CHAT_PREVIEWS = "privacyShowChatPreviews"
|
||||
@@ -51,6 +51,8 @@ let DEFAULT_SHOW_HIDDEN_PROFILES_NOTICE = "showHiddenProfilesNotice"
|
||||
let DEFAULT_SHOW_MUTE_PROFILE_ALERT = "showMuteProfileAlert"
|
||||
let DEFAULT_WHATS_NEW_VERSION = "defaultWhatsNewVersion"
|
||||
let DEFAULT_ONBOARDING_STAGE = "onboardingStage"
|
||||
let DEFAULT_MIGRATION_TO_STAGE = "migrationToStage"
|
||||
let DEFAULT_MIGRATION_FROM_STAGE = "migrationFromStage"
|
||||
let DEFAULT_CUSTOM_DISAPPEARING_MESSAGE_TIME = "customDisappearingMessageTime"
|
||||
let DEFAULT_SHOW_UNREAD_AND_FAVORITES = "showUnreadAndFavorites"
|
||||
let DEFAULT_DEVICE_NAME_FOR_REMOTE_ACCESS = "deviceNameForRemoteAccess"
|
||||
@@ -58,6 +60,8 @@ let DEFAULT_CONFIRM_REMOTE_SESSIONS = "confirmRemoteSessions"
|
||||
let DEFAULT_CONNECT_REMOTE_VIA_MULTICAST = "connectRemoteViaMulticast"
|
||||
let DEFAULT_CONNECT_REMOTE_VIA_MULTICAST_AUTO = "connectRemoteViaMulticastAuto"
|
||||
|
||||
let ANDROID_DEFAULT_CALL_ON_LOCK_SCREEN = "androidCallOnLockScreen"
|
||||
|
||||
let appDefaults: [String: Any] = [
|
||||
DEFAULT_SHOW_LA_NOTICE: false,
|
||||
DEFAULT_LA_NOTICE_SHOWN: false,
|
||||
@@ -93,6 +97,7 @@ let appDefaults: [String: Any] = [
|
||||
DEFAULT_CONFIRM_REMOTE_SESSIONS: false,
|
||||
DEFAULT_CONNECT_REMOTE_VIA_MULTICAST: true,
|
||||
DEFAULT_CONNECT_REMOTE_VIA_MULTICAST_AUTO: true,
|
||||
ANDROID_DEFAULT_CALL_ON_LOCK_SCREEN: AppSettingsLockScreenCalls.show.rawValue
|
||||
]
|
||||
|
||||
// not used anymore
|
||||
@@ -148,10 +153,14 @@ struct SettingsView: View {
|
||||
@EnvironmentObject var chatModel: ChatModel
|
||||
@EnvironmentObject var sceneDelegate: SceneDelegate
|
||||
@Binding var showSettings: Bool
|
||||
@State private var showProgress: Bool = false
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
settingsView()
|
||||
if showProgress {
|
||||
progressView()
|
||||
}
|
||||
if let la = chatModel.laRequest {
|
||||
LocalAuthView(authRequest: la)
|
||||
}
|
||||
@@ -202,9 +211,17 @@ struct SettingsView: View {
|
||||
} label: {
|
||||
settingsRow("desktopcomputer") { Text("Use from desktop") }
|
||||
}
|
||||
|
||||
NavigationLink {
|
||||
MigrateFromDevice(showSettings: $showSettings, showProgressOnSettings: $showProgress)
|
||||
.navigationTitle("Migrate device")
|
||||
.navigationBarTitleDisplayMode(.large)
|
||||
} label: {
|
||||
settingsRow("tray.and.arrow.up") { Text("Migrate to another device") }
|
||||
}
|
||||
}
|
||||
.disabled(chatModel.chatRunning != true)
|
||||
|
||||
|
||||
Section("Settings") {
|
||||
NavigationLink {
|
||||
NotificationsView()
|
||||
@@ -349,6 +366,13 @@ struct SettingsView: View {
|
||||
}
|
||||
}
|
||||
|
||||
private func progressView() -> some View {
|
||||
VStack {
|
||||
ProgressView().scaleEffect(2)
|
||||
}
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity )
|
||||
}
|
||||
|
||||
private enum NotificationAlert {
|
||||
case enable
|
||||
case error(LocalizedStringKey, String)
|
||||
|
||||
@@ -47,7 +47,7 @@ struct UserAddressView: View {
|
||||
userAddressScrollView()
|
||||
} else {
|
||||
userAddressScrollView()
|
||||
.modifier(BackButton {
|
||||
.modifier(BackButton(disabled: Binding.constant(false)) {
|
||||
if savedAAS == aas {
|
||||
dismiss()
|
||||
} else {
|
||||
|
||||
@@ -276,6 +276,7 @@ struct UserProfilesView: View {
|
||||
// Deleting the last visible user while having hidden one(s)
|
||||
try await deleteUser()
|
||||
try await changeActiveUserAsync_(nil, viewPwd: nil)
|
||||
try? await stopChatAsync()
|
||||
await MainActor.run {
|
||||
onboardingStageDefault.set(.step1_SimpleXInfo)
|
||||
m.onboardingStage = .step1_SimpleXInfo
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -107,6 +107,10 @@
|
||||
<target>%@ připojen</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="%@ downloaded" xml:space="preserve">
|
||||
<source>%@ downloaded</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="%@ is connected!" xml:space="preserve">
|
||||
<source>%@ is connected!</source>
|
||||
<target>%@ je připojen!</target>
|
||||
@@ -127,6 +131,10 @@
|
||||
<target>%@ servery</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="%@ uploaded" xml:space="preserve">
|
||||
<source>%@ uploaded</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="%@ wants to connect!" xml:space="preserve">
|
||||
<source>%@ wants to connect!</source>
|
||||
<target>%@ se chce připojit!</target>
|
||||
@@ -332,6 +340,10 @@
|
||||
<target>**Nejsoukromější**: nepoužívejte server oznámení SimpleX Chat, pravidelně kontrolujte zprávy na pozadí (závisí na tom, jak často aplikaci používáte).</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="**Please note**: using the same database on two devices will break the decryption of messages from your connections, as a security protection." xml:space="preserve">
|
||||
<source>**Please note**: using the same database on two devices will break the decryption of messages from your connections, as a security protection.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="**Please note**: you will NOT be able to recover or change passphrase if you lose it." xml:space="preserve">
|
||||
<source>**Please note**: you will NOT be able to recover or change passphrase if you lose it.</source>
|
||||
<target>**Upozornění**: Pokud heslo ztratíte, NEBUDETE jej moci obnovit ani změnit.</target>
|
||||
@@ -347,6 +359,10 @@
|
||||
<target>**Upozornění**: Okamžitě doručovaná oznámení vyžadují přístupové heslo uložené v Klíčence.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="**Warning**: the archive will be removed." xml:space="preserve">
|
||||
<source>**Warning**: the archive will be removed.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="**e2e encrypted** audio call" xml:space="preserve">
|
||||
<source>**e2e encrypted** audio call</source>
|
||||
<target>**e2e šifrovaný** audio hovor</target>
|
||||
@@ -598,6 +614,10 @@
|
||||
<target>Změna adresy bude přerušena. Budou použity staré přijímací adresy.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Admins can block a member for all." xml:space="preserve">
|
||||
<source>Admins can block a member for all.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Admins can create the links to join groups." xml:space="preserve">
|
||||
<source>Admins can create the links to join groups.</source>
|
||||
<target>Správci mohou vytvářet odkazy pro připojení ke skupinám.</target>
|
||||
@@ -651,6 +671,10 @@
|
||||
<target>Všechny vaše kontakty zůstanou připojeny. Aktualizace profilu bude odeslána vašim kontaktům.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="All your contacts, conversations and files will be securely encrypted and uploaded in chunks to configured XFTP relays." xml:space="preserve">
|
||||
<source>All your contacts, conversations and files will be securely encrypted and uploaded in chunks to configured XFTP relays.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Allow" xml:space="preserve">
|
||||
<source>Allow</source>
|
||||
<target>Povolit</target>
|
||||
@@ -696,6 +720,10 @@
|
||||
<target>Povolit nevratné smazání odeslaných zpráv. (24 hodin)</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Allow to send SimpleX links." xml:space="preserve">
|
||||
<source>Allow to send SimpleX links.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Allow to send files and media." xml:space="preserve">
|
||||
<source>Allow to send files and media.</source>
|
||||
<target>Povolit odesílání souborů a médii.</target>
|
||||
@@ -774,6 +802,10 @@
|
||||
<target>Sestavení aplikace: %@</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="App data migration" xml:space="preserve">
|
||||
<source>App data migration</source>
|
||||
<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>Aplikace šifruje nové místní soubory (s výjimkou videí).</target>
|
||||
@@ -809,6 +841,18 @@
|
||||
<target>Vzhled</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Apply" xml:space="preserve">
|
||||
<source>Apply</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Archive and upload" xml:space="preserve">
|
||||
<source>Archive and upload</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Archiving database" xml:space="preserve">
|
||||
<source>Archiving database</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Attach" xml:space="preserve">
|
||||
<source>Attach</source>
|
||||
<target>Připojit</target>
|
||||
@@ -989,6 +1033,10 @@
|
||||
<target>Zrušit</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Cancel migration" xml:space="preserve">
|
||||
<source>Cancel migration</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Cannot access keychain to save database password" xml:space="preserve">
|
||||
<source>Cannot access keychain to save database password</source>
|
||||
<target>Nelze získat přístup ke klíčence pro uložení hesla databáze</target>
|
||||
@@ -999,6 +1047,10 @@
|
||||
<target>Nelze přijmout soubor</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Cellular" xml:space="preserve">
|
||||
<source>Cellular</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Change" xml:space="preserve">
|
||||
<source>Change</source>
|
||||
<target>Změnit</target>
|
||||
@@ -1089,6 +1141,10 @@
|
||||
<source>Chat is stopped. If you already used this database on another device, you should transfer it back before starting chat.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Chat migrated!" xml:space="preserve">
|
||||
<source>Chat migrated!</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Chat preferences" xml:space="preserve">
|
||||
<source>Chat preferences</source>
|
||||
<target>Předvolby chatu</target>
|
||||
@@ -1109,6 +1165,10 @@
|
||||
<target>Čínské a Španělské rozhranní</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Choose _Migrate from another device_ on the new device and scan QR code." xml:space="preserve">
|
||||
<source>Choose _Migrate from another device_ on the new device and scan QR code.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Choose file" xml:space="preserve">
|
||||
<source>Choose file</source>
|
||||
<target>Vybrat soubor</target>
|
||||
@@ -1178,6 +1238,10 @@
|
||||
<target>Potvrdit aktualizaci databáze</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Confirm network settings" xml:space="preserve">
|
||||
<source>Confirm network settings</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Confirm new passphrase…" xml:space="preserve">
|
||||
<source>Confirm new passphrase…</source>
|
||||
<target>Potvrdit novou heslovou frázi…</target>
|
||||
@@ -1188,6 +1252,14 @@
|
||||
<target>Potvrdit heslo</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Confirm that you remember database passphrase to migrate it." xml:space="preserve">
|
||||
<source>Confirm that you remember database passphrase to migrate it.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Confirm upload" xml:space="preserve">
|
||||
<source>Confirm upload</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Connect" xml:space="preserve">
|
||||
<source>Connect</source>
|
||||
<target>Připojit</target>
|
||||
@@ -1428,6 +1500,10 @@ This is your own one-time link!</source>
|
||||
<target>Vytvořeno na %@</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Creating archive link" xml:space="preserve">
|
||||
<source>Creating archive link</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Creating link…" xml:space="preserve">
|
||||
<source>Creating link…</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
@@ -1643,6 +1719,10 @@ This cannot be undone!</source>
|
||||
<target>Odstranění databáze</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Delete database from this device" xml:space="preserve">
|
||||
<source>Delete database from this device</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Delete file" xml:space="preserve">
|
||||
<source>Delete file</source>
|
||||
<target>Smazat soubor</target>
|
||||
@@ -1927,11 +2007,27 @@ This cannot be undone!</source>
|
||||
<target>Snížit a otevřít chat</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Download" xml:space="preserve">
|
||||
<source>Download</source>
|
||||
<note>chat item action</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Download failed" xml:space="preserve">
|
||||
<source>Download failed</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Download file" xml:space="preserve">
|
||||
<source>Download file</source>
|
||||
<target>Stáhnout soubor</target>
|
||||
<note>server test step</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Downloading archive" xml:space="preserve">
|
||||
<source>Downloading archive</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Downloading link details" xml:space="preserve">
|
||||
<source>Downloading link details</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Duplicate display name!" xml:space="preserve">
|
||||
<source>Duplicate display name!</source>
|
||||
<target>Duplicitní zobrazované jméno!</target>
|
||||
@@ -1986,6 +2082,10 @@ This cannot be undone!</source>
|
||||
<target>Povolit pro všechny</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Enable in direct chats (BETA)!" xml:space="preserve">
|
||||
<source>Enable in direct chats (BETA)!</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Enable instant notifications?" xml:space="preserve">
|
||||
<source>Enable instant notifications?</source>
|
||||
<target>Povolit okamžitá oznámení?</target>
|
||||
@@ -2016,6 +2116,10 @@ This cannot be undone!</source>
|
||||
<target>Povolit sebedestrukční heslo</target>
|
||||
<note>set passcode view</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Enabled for" xml:space="preserve">
|
||||
<source>Enabled for</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Encrypt" xml:space="preserve">
|
||||
<source>Encrypt</source>
|
||||
<target>Šifrovat</target>
|
||||
@@ -2097,6 +2201,10 @@ This cannot be undone!</source>
|
||||
<source>Enter group name…</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Enter passphrase" xml:space="preserve">
|
||||
<source>Enter passphrase</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Enter passphrase…" xml:space="preserve">
|
||||
<source>Enter passphrase…</source>
|
||||
<target>Zadejte přístupovou frázi…</target>
|
||||
@@ -2155,6 +2263,10 @@ This cannot be undone!</source>
|
||||
<target>Chyba přidávání člena(ů)</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Error allowing contact PQ encryption" xml:space="preserve">
|
||||
<source>Error allowing contact PQ encryption</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Error changing address" xml:space="preserve">
|
||||
<source>Error changing address</source>
|
||||
<target>Chuba změny adresy</target>
|
||||
@@ -2244,6 +2356,10 @@ This cannot be undone!</source>
|
||||
<target>Chyba mazání uživatelského profilu</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Error downloading the archive" xml:space="preserve">
|
||||
<source>Error downloading the archive</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Error enabling delivery receipts!" xml:space="preserve">
|
||||
<source>Error enabling delivery receipts!</source>
|
||||
<target>Chyba povolení potvrzení o doručení!</target>
|
||||
@@ -2318,6 +2434,10 @@ This cannot be undone!</source>
|
||||
<target>Při ukládání přístupové fráze do klíčenky došlo k chybě</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Error saving settings" xml:space="preserve">
|
||||
<source>Error saving settings</source>
|
||||
<note>when migrating</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Error saving user password" xml:space="preserve">
|
||||
<source>Error saving user password</source>
|
||||
<target>Chyba ukládání hesla uživatele</target>
|
||||
@@ -2387,6 +2507,14 @@ This cannot be undone!</source>
|
||||
<target>Chyba aktualizace soukromí uživatele</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Error uploading the archive" xml:space="preserve">
|
||||
<source>Error uploading the archive</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Error verifying passphrase:" xml:space="preserve">
|
||||
<source>Error verifying passphrase:</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Error: " xml:space="preserve">
|
||||
<source>Error: </source>
|
||||
<target>Chyba: </target>
|
||||
@@ -2436,6 +2564,10 @@ This cannot be undone!</source>
|
||||
<target>Exportovaný archiv databáze.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Exported file doesn't exist" xml:space="preserve">
|
||||
<source>Exported file doesn't exist</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Exporting database archive…" xml:space="preserve">
|
||||
<source>Exporting database archive…</source>
|
||||
<target>Exportuji archiv databáze…</target>
|
||||
@@ -2495,6 +2627,10 @@ This cannot be undone!</source>
|
||||
<target>Soubory a média jsou zakázány v této skupině.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Files and media not allowed" xml:space="preserve">
|
||||
<source>Files and media not allowed</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Files and media prohibited!" xml:space="preserve">
|
||||
<source>Files and media prohibited!</source>
|
||||
<target>Soubory a média jsou zakázány!</target>
|
||||
@@ -2505,6 +2641,14 @@ This cannot be undone!</source>
|
||||
<target>Filtrovat nepřečtené a oblíbené chaty.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Finalize migration" xml:space="preserve">
|
||||
<source>Finalize migration</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Finalize migration on another device." xml:space="preserve">
|
||||
<source>Finalize migration on another device.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Finally, we have them! 🚀" xml:space="preserve">
|
||||
<source>Finally, we have them! 🚀</source>
|
||||
<target>Konečně je máme! 🚀</target>
|
||||
@@ -2550,6 +2694,18 @@ This cannot be undone!</source>
|
||||
<target>Pro konzoli</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Forward" xml:space="preserve">
|
||||
<source>Forward</source>
|
||||
<note>chat item action</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Forwarded" xml:space="preserve">
|
||||
<source>Forwarded</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Forwarded from" xml:space="preserve">
|
||||
<source>Forwarded from</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Found desktop" xml:space="preserve">
|
||||
<source>Found desktop</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
@@ -2656,6 +2812,10 @@ This cannot be undone!</source>
|
||||
<target>Členové skupiny mohou nevratně mazat odeslané zprávy. (24 hodin)</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Group members can send SimpleX links." xml:space="preserve">
|
||||
<source>Group members can send SimpleX links.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Group members can send direct messages." xml:space="preserve">
|
||||
<source>Group members can send direct messages.</source>
|
||||
<target>Členové skupiny mohou posílat přímé zprávy.</target>
|
||||
@@ -2790,6 +2950,10 @@ This cannot be undone!</source>
|
||||
<target>Jak používat servery</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Hungarian interface" xml:space="preserve">
|
||||
<source>Hungarian interface</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="ICE servers (one per line)" xml:space="preserve">
|
||||
<source>ICE servers (one per line)</source>
|
||||
<target>Servery ICE (jeden na řádek)</target>
|
||||
@@ -2855,6 +3019,14 @@ This cannot be undone!</source>
|
||||
<target>Import databáze</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Import failed" xml:space="preserve">
|
||||
<source>Import failed</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Importing archive" xml:space="preserve">
|
||||
<source>Importing archive</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Improved message delivery" xml:space="preserve">
|
||||
<source>Improved message delivery</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
@@ -2869,6 +3041,10 @@ This cannot be undone!</source>
|
||||
<target>Vylepšená konfigurace serveru</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="In order to continue, chat should be stopped." xml:space="preserve">
|
||||
<source>In order to continue, chat should be stopped.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="In reply to" xml:space="preserve">
|
||||
<source>In reply to</source>
|
||||
<target>V odpovědi na</target>
|
||||
@@ -2976,6 +3152,10 @@ This cannot be undone!</source>
|
||||
<source>Invalid link</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Invalid migration confirmation" xml:space="preserve">
|
||||
<source>Invalid migration confirmation</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Invalid name!" xml:space="preserve">
|
||||
<source>Invalid name!</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
@@ -3331,6 +3511,10 @@ This is your link for group %@!</source>
|
||||
<target>Text zprávy</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Message too large" xml:space="preserve">
|
||||
<source>Message too large</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Messages" xml:space="preserve">
|
||||
<source>Messages</source>
|
||||
<target>Zprávy</target>
|
||||
@@ -3345,11 +3529,47 @@ This is your link for group %@!</source>
|
||||
<source>Messages from %@ will be shown!</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Messages, files and calls are protected by **end-to-end encryption** with perfect forward secrecy, repudiation and break-in recovery." xml:space="preserve">
|
||||
<source>Messages, files and calls are protected by **end-to-end encryption** with perfect forward secrecy, repudiation and break-in recovery.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Messages, files and calls are protected by **quantum resistant e2e encryption** with perfect forward secrecy, repudiation and break-in recovery." xml:space="preserve">
|
||||
<source>Messages, files and calls are protected by **quantum resistant e2e encryption** with perfect forward secrecy, repudiation and break-in recovery.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Migrate device" xml:space="preserve">
|
||||
<source>Migrate device</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Migrate from another device" xml:space="preserve">
|
||||
<source>Migrate from another device</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Migrate here" xml:space="preserve">
|
||||
<source>Migrate here</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Migrate to another device" xml:space="preserve">
|
||||
<source>Migrate to another device</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Migrate to another device via QR code." xml:space="preserve">
|
||||
<source>Migrate to another device via QR code.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Migrating" xml:space="preserve">
|
||||
<source>Migrating</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Migrating database archive…" xml:space="preserve">
|
||||
<source>Migrating database archive…</source>
|
||||
<target>Přenášení archivu databáze…</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Migration complete" xml:space="preserve">
|
||||
<source>Migration complete</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Migration error:" xml:space="preserve">
|
||||
<source>Migration error:</source>
|
||||
<target>Chyba přenášení:</target>
|
||||
@@ -3425,6 +3645,10 @@ This is your link for group %@!</source>
|
||||
<target>Síť a servery</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Network connection" xml:space="preserve">
|
||||
<source>Network connection</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Network settings" xml:space="preserve">
|
||||
<source>Network settings</source>
|
||||
<target>Nastavení sítě</target>
|
||||
@@ -3534,6 +3758,10 @@ This is your link for group %@!</source>
|
||||
<target>Žádná historie</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="No network connection" xml:space="preserve">
|
||||
<source>No network connection</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="No permission to record voice message" xml:space="preserve">
|
||||
<source>No permission to record voice message</source>
|
||||
<target>Nemáte oprávnění nahrávat hlasové zprávy</target>
|
||||
@@ -3705,6 +3933,10 @@ This is your link for group %@!</source>
|
||||
<source>Open group</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Open migration to another device" xml:space="preserve">
|
||||
<source>Open migration to another device</source>
|
||||
<note>authentication reason</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Open user profiles" xml:space="preserve">
|
||||
<source>Open user profiles</source>
|
||||
<target>Otevřít uživatelské profily</target>
|
||||
@@ -3719,14 +3951,26 @@ This is your link for group %@!</source>
|
||||
<source>Opening app…</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Or paste archive link" xml:space="preserve">
|
||||
<source>Or paste archive link</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Or scan QR code" xml:space="preserve">
|
||||
<source>Or scan QR code</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Or securely share this file link" xml:space="preserve">
|
||||
<source>Or securely share this file link</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Or show this code" xml:space="preserve">
|
||||
<source>Or show this code</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Other" xml:space="preserve">
|
||||
<source>Other</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="PING count" xml:space="preserve">
|
||||
<source>PING count</source>
|
||||
<target>Počet PING</target>
|
||||
@@ -3803,6 +4047,10 @@ This is your link for group %@!</source>
|
||||
<target>Chyba dešifrování</target>
|
||||
<note>message decrypt error item</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Picture-in-picture calls" xml:space="preserve">
|
||||
<source>Picture-in-picture calls</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</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>Prosím, požádejte kontaktní osobu, aby umožnila odesílání hlasových zpráv.</target>
|
||||
@@ -3823,6 +4071,10 @@ This is your link for group %@!</source>
|
||||
<target>Zkontrolujte prosím nastavení své i svého kontaktu.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Please confirm that network settings are correct for this device." xml:space="preserve">
|
||||
<source>Please confirm that network settings are correct for this device.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Please contact developers. Error: %@" xml:space="preserve">
|
||||
<source>Please contact developers.
|
||||
Error: %@</source>
|
||||
@@ -3878,6 +4130,10 @@ Error: %@</source>
|
||||
<target>Je možné, že otisk certifikátu v adrese serveru je nesprávný</target>
|
||||
<note>server test error</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Post-quantum E2EE" xml:space="preserve">
|
||||
<source>Post-quantum E2EE</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Preserve the last message draft, with attachments." xml:space="preserve">
|
||||
<source>Preserve the last message draft, with attachments.</source>
|
||||
<target>Zachování posledního návrhu zprávy s přílohami.</target>
|
||||
@@ -3965,6 +4221,10 @@ Error: %@</source>
|
||||
<target>Zakázat reakce na zprávy.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Prohibit sending SimpleX links." xml:space="preserve">
|
||||
<source>Prohibit sending SimpleX links.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Prohibit sending direct messages to members." xml:space="preserve">
|
||||
<source>Prohibit sending direct messages to members.</source>
|
||||
<target>Zakázat odesílání přímých zpráv členům.</target>
|
||||
@@ -4010,6 +4270,14 @@ Error: %@</source>
|
||||
<target>Nabízená oznámení</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Push server" xml:space="preserve">
|
||||
<source>Push server</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Quantum resistant encryption" xml:space="preserve">
|
||||
<source>Quantum resistant encryption</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Rate the app" xml:space="preserve">
|
||||
<source>Rate the app</source>
|
||||
<target>Ohodnoťte aplikaci</target>
|
||||
@@ -4084,6 +4352,10 @@ Error: %@</source>
|
||||
<target>Přijímací adresa bude změněna na jiný server. Změna adresy bude dokončena po připojení odesílatele.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Receiving concurrency" xml:space="preserve">
|
||||
<source>Receiving concurrency</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Receiving file will be stopped." xml:space="preserve">
|
||||
<source>Receiving file will be stopped.</source>
|
||||
<target>Příjem souboru bude zastaven.</target>
|
||||
@@ -4098,6 +4370,10 @@ Error: %@</source>
|
||||
<source>Recent history and improved [directory bot](simplex:/contact#/?v=1-4&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FeXSPwqTkKyDO3px4fLf1wx3MvPdjdLW3%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAaiv6MkMH44L2TcYrt_CsX3ZvM11WgbMEUn0hkIKTOho%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion).</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Recipient(s) can't see who this message is from." xml:space="preserve">
|
||||
<source>Recipient(s) can't see who this message is from.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Recipients see updates as you type them." xml:space="preserve">
|
||||
<source>Recipients see updates as you type them.</source>
|
||||
<target>Příjemci uvidí aktualizace během jejich psaní.</target>
|
||||
@@ -4192,10 +4468,22 @@ Error: %@</source>
|
||||
<source>Repeat connection request?</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Repeat download" xml:space="preserve">
|
||||
<source>Repeat download</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Repeat import" xml:space="preserve">
|
||||
<source>Repeat import</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Repeat join request?" xml:space="preserve">
|
||||
<source>Repeat join request?</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Repeat upload" xml:space="preserve">
|
||||
<source>Repeat upload</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Reply" xml:space="preserve">
|
||||
<source>Reply</source>
|
||||
<target>Odpověď</target>
|
||||
@@ -4295,6 +4583,10 @@ Error: %@</source>
|
||||
<target>SMP servery</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Safer groups" xml:space="preserve">
|
||||
<source>Safer groups</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Save" xml:space="preserve">
|
||||
<source>Save</source>
|
||||
<target>Uložit</target>
|
||||
@@ -4375,11 +4667,19 @@ Error: %@</source>
|
||||
<target>Uložit uvítací zprávu?</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Saved" xml:space="preserve">
|
||||
<source>Saved</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Saved WebRTC ICE servers will be removed" xml:space="preserve">
|
||||
<source>Saved WebRTC ICE servers will be removed</source>
|
||||
<target>Uložené servery WebRTC ICE budou odstraněny</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Saved from" xml:space="preserve">
|
||||
<source>Saved from</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Saved message" xml:space="preserve">
|
||||
<source>Saved message</source>
|
||||
<note>message info title</note>
|
||||
@@ -4654,6 +4954,10 @@ Error: %@</source>
|
||||
<target>Nastavit heslo</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Set passphrase" xml:space="preserve">
|
||||
<source>Set passphrase</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Set passphrase to export" xml:space="preserve">
|
||||
<source>Set passphrase to export</source>
|
||||
<target>Nastavení přístupové fráze pro export</target>
|
||||
@@ -4708,6 +5012,10 @@ Error: %@</source>
|
||||
<target>Sdílet s kontakty</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Show QR code" xml:space="preserve">
|
||||
<source>Show QR code</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Show calls in phone history" xml:space="preserve">
|
||||
<source>Show calls in phone history</source>
|
||||
<target>Ukaž hovory v historii telefonu</target>
|
||||
@@ -4786,6 +5094,14 @@ Error: %@</source>
|
||||
<trans-unit id="SimpleX links" xml:space="preserve">
|
||||
<source>SimpleX links</source>
|
||||
<target>Odkazy na SimpleX</target>
|
||||
<note>chat feature</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="SimpleX links are prohibited in this group." xml:space="preserve">
|
||||
<source>SimpleX links are prohibited in this group.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="SimpleX links not allowed" xml:space="preserve">
|
||||
<source>SimpleX links not allowed</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="SimpleX one-time invitation" xml:space="preserve">
|
||||
@@ -4847,6 +5163,10 @@ Error: %@</source>
|
||||
<target>Zastavit SimpleX</target>
|
||||
<note>authentication reason</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Stop chat" xml:space="preserve">
|
||||
<source>Stop chat</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Stop chat to enable database actions" xml:space="preserve">
|
||||
<source>Stop chat to enable database actions</source>
|
||||
<target>Zastavte chat pro povolení akcí databáze</target>
|
||||
@@ -4887,6 +5207,10 @@ Error: %@</source>
|
||||
<target>Přestat sdílet adresu?</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Stopping chat" xml:space="preserve">
|
||||
<source>Stopping chat</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Submit" xml:space="preserve">
|
||||
<source>Submit</source>
|
||||
<target>Odeslat</target>
|
||||
@@ -5129,6 +5453,14 @@ Může se to stát kvůli nějaké chybě, nebo pokud je spojení kompromitován
|
||||
<target>Tuto akci nelze vzít zpět - váš profil, kontakty, zprávy a soubory budou nenávratně ztraceny.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="This chat is protected by end-to-end encryption." xml:space="preserve">
|
||||
<source>This chat is protected by end-to-end encryption.</source>
|
||||
<note>E2EE info chat item</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="This chat is protected by quantum resistant end-to-end encryption." xml:space="preserve">
|
||||
<source>This chat is protected by quantum resistant end-to-end encryption.</source>
|
||||
<note>E2EE info chat item</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="This device name" xml:space="preserve">
|
||||
<source>This device name</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
@@ -5409,11 +5741,19 @@ Chcete-li se připojit, požádejte svůj kontakt o vytvoření dalšího odkazu
|
||||
<target>Zvýšit a otevřít chat</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Upload failed" xml:space="preserve">
|
||||
<source>Upload failed</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Upload file" xml:space="preserve">
|
||||
<source>Upload file</source>
|
||||
<target>Nahrát soubor</target>
|
||||
<note>server test step</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Uploading archive" xml:space="preserve">
|
||||
<source>Uploading archive</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Use .onion hosts" xml:space="preserve">
|
||||
<source>Use .onion hosts</source>
|
||||
<target>Použít hostitele .onion</target>
|
||||
@@ -5462,6 +5802,10 @@ Chcete-li se připojit, požádejte svůj kontakt o vytvoření dalšího odkazu
|
||||
<target>Použít server</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Use the app while in the call." xml:space="preserve">
|
||||
<source>Use the app while in the call.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="User profile" xml:space="preserve">
|
||||
<source>User profile</source>
|
||||
<target>Profil uživatele</target>
|
||||
@@ -5494,6 +5838,14 @@ Chcete-li se připojit, požádejte svůj kontakt o vytvoření dalšího odkazu
|
||||
<source>Verify connections</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Verify database passphrase" xml:space="preserve">
|
||||
<source>Verify database passphrase</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Verify passphrase" xml:space="preserve">
|
||||
<source>Verify passphrase</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Verify security code" xml:space="preserve">
|
||||
<source>Verify security code</source>
|
||||
<target>Ověření bezpečnostního kódu</target>
|
||||
@@ -5552,6 +5904,10 @@ Chcete-li se připojit, požádejte svůj kontakt o vytvoření dalšího odkazu
|
||||
<target>Hlasové zprávy jsou v této skupině zakázány.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Voice messages not allowed" xml:space="preserve">
|
||||
<source>Voice messages not allowed</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Voice messages prohibited!" xml:space="preserve">
|
||||
<source>Voice messages prohibited!</source>
|
||||
<target>Hlasové zprávy jsou zakázány!</target>
|
||||
@@ -5581,6 +5937,10 @@ Chcete-li se připojit, požádejte svůj kontakt o vytvoření dalšího odkazu
|
||||
<target>Čekám na video</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Warning: starting chat on multiple devices is not supported and will cause message delivery failures" xml:space="preserve">
|
||||
<source>Warning: starting chat on multiple devices is not supported and will cause message delivery failures</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Warning: you may lose some data!" xml:space="preserve">
|
||||
<source>Warning: you may lose some data!</source>
|
||||
<target>Upozornění: můžete ztratit nějaká data!</target>
|
||||
@@ -5601,6 +5961,10 @@ Chcete-li se připojit, požádejte svůj kontakt o vytvoření dalšího odkazu
|
||||
<target>Uvítací zpráva</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Welcome message is too long" xml:space="preserve">
|
||||
<source>Welcome message is too long</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="What's new" xml:space="preserve">
|
||||
<source>What's new</source>
|
||||
<target>Co je nového</target>
|
||||
@@ -5621,6 +5985,14 @@ Chcete-li se připojit, požádejte svůj kontakt o vytvoření dalšího odkazu
|
||||
<target>Pokud s někým sdílíte inkognito profil, bude tento profil použit pro skupiny, do kterých vás pozve.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="WiFi" xml:space="preserve">
|
||||
<source>WiFi</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Wired ethernet" xml:space="preserve">
|
||||
<source>Wired ethernet</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="With encrypted files and media." xml:space="preserve">
|
||||
<source>With encrypted files and media.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
@@ -5654,6 +6026,10 @@ Chcete-li se připojit, požádejte svůj kontakt o vytvoření dalšího odkazu
|
||||
<target>Vy</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="You **must not** use the same database on two devices." xml:space="preserve">
|
||||
<source>You **must not** use the same database on two devices.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="You accepted connection" xml:space="preserve">
|
||||
<source>You accepted connection</source>
|
||||
<target>Přijali jste spojení</target>
|
||||
@@ -5733,6 +6109,10 @@ Repeat join request?</source>
|
||||
<target>Můžete je povolit později v nastavení Soukromí & Bezpečnosti aplikace</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="You can give another try." xml:space="preserve">
|
||||
<source>You can give another try.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="You can hide or mute a user profile - swipe it to the right." xml:space="preserve">
|
||||
<source>You can hide or mute a user profile - swipe it to the right.</source>
|
||||
<target>Profil uživatele můžete skrýt nebo ztlumit - přejeďte prstem doprava.</target>
|
||||
@@ -6076,6 +6456,10 @@ Servery SimpleX nevidí váš profil.</target>
|
||||
<target>správce</target>
|
||||
<note>member role</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="admins" xml:space="preserve">
|
||||
<source>admins</source>
|
||||
<note>feature role</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="agreeing encryption for %@…" xml:space="preserve">
|
||||
<source>agreeing encryption for %@…</source>
|
||||
<target>povoluji šifrování pro %@…</target>
|
||||
@@ -6086,6 +6470,10 @@ Servery SimpleX nevidí váš profil.</target>
|
||||
<target>povoluji šifrování…</target>
|
||||
<note>chat item text</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="all members" xml:space="preserve">
|
||||
<source>all members</source>
|
||||
<note>feature role</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="always" xml:space="preserve">
|
||||
<source>always</source>
|
||||
<target>vždy</target>
|
||||
@@ -6116,7 +6504,7 @@ Servery SimpleX nevidí váš profil.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="blocked" xml:space="preserve">
|
||||
<source>blocked</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
<note>marked deleted chat item preview text</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="blocked %@" xml:space="preserve">
|
||||
<source>blocked %@</source>
|
||||
@@ -6124,7 +6512,7 @@ Servery SimpleX nevidí váš profil.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="blocked by admin" xml:space="preserve">
|
||||
<source>blocked by admin</source>
|
||||
<note>blocked chat item</note>
|
||||
<note>marked deleted chat item preview text</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="bold" xml:space="preserve">
|
||||
<source>bold</source>
|
||||
@@ -6408,6 +6796,10 @@ Servery SimpleX nevidí váš profil.</target>
|
||||
<source>event happened</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="forwarded" xml:space="preserve">
|
||||
<source>forwarded</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="group deleted" xml:space="preserve">
|
||||
<source>group deleted</source>
|
||||
<target>skupina smazána</target>
|
||||
@@ -6550,7 +6942,7 @@ Servery SimpleX nevidí váš profil.</target>
|
||||
<trans-unit id="moderated by %@" xml:space="preserve">
|
||||
<source>moderated by %@</source>
|
||||
<target>moderovaný %@</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
<note>marked deleted chat item preview text</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="months" xml:space="preserve">
|
||||
<source>months</source>
|
||||
@@ -6614,11 +7006,19 @@ Servery SimpleX nevidí váš profil.</target>
|
||||
<target>vlastník</target>
|
||||
<note>member role</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="owners" xml:space="preserve">
|
||||
<source>owners</source>
|
||||
<note>feature role</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="peer-to-peer" xml:space="preserve">
|
||||
<source>peer-to-peer</source>
|
||||
<target>peer-to-peer</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="quantum resistant e2e encryption" xml:space="preserve">
|
||||
<source>quantum resistant e2e encryption</source>
|
||||
<note>chat item text</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="received answer…" xml:space="preserve">
|
||||
<source>received answer…</source>
|
||||
<target>obdržel odpověď…</target>
|
||||
@@ -6657,6 +7057,14 @@ Servery SimpleX nevidí váš profil.</target>
|
||||
<target>odstranil vás</target>
|
||||
<note>rcv group event chat item</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="saved" xml:space="preserve">
|
||||
<source>saved</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="saved from %@" xml:space="preserve">
|
||||
<source>saved from %@</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="sec" xml:space="preserve">
|
||||
<source>sec</source>
|
||||
<target>sek</target>
|
||||
@@ -6690,6 +7098,10 @@ Servery SimpleX nevidí váš profil.</target>
|
||||
<source>set new profile picture</source>
|
||||
<note>profile update event chat item</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="standard end-to-end encryption" xml:space="preserve">
|
||||
<source>standard end-to-end encryption</source>
|
||||
<note>chat item text</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="starting…" xml:space="preserve">
|
||||
<source>starting…</source>
|
||||
<target>začíná…</target>
|
||||
@@ -6786,6 +7198,10 @@ Servery SimpleX nevidí váš profil.</target>
|
||||
<target>ano</target>
|
||||
<note>pref value</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="you" xml:space="preserve">
|
||||
<source>you</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="you are invited to group" xml:space="preserve">
|
||||
<source>you are invited to group</source>
|
||||
<target>jste pozváni do skupiny</target>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -105,6 +105,10 @@
|
||||
<source>%@ connected</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="%@ downloaded" xml:space="preserve">
|
||||
<source>%@ downloaded</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="%@ is connected!" xml:space="preserve">
|
||||
<source>%@ is connected!</source>
|
||||
<target>%@ on yhdistetty!</target>
|
||||
@@ -125,6 +129,10 @@
|
||||
<target>%@ palvelimet</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="%@ uploaded" xml:space="preserve">
|
||||
<source>%@ uploaded</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="%@ wants to connect!" xml:space="preserve">
|
||||
<source>%@ wants to connect!</source>
|
||||
<target>%@ haluaa muodostaa yhteyden!</target>
|
||||
@@ -330,6 +338,10 @@
|
||||
<target>**Yksityisin**: älä käytä SimpleX Chat -ilmoituspalvelinta, tarkista viestit ajoittain taustalla (riippuu siitä, kuinka usein käytät sovellusta).</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="**Please note**: using the same database on two devices will break the decryption of messages from your connections, as a security protection." xml:space="preserve">
|
||||
<source>**Please note**: using the same database on two devices will break the decryption of messages from your connections, as a security protection.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="**Please note**: you will NOT be able to recover or change passphrase if you lose it." xml:space="preserve">
|
||||
<source>**Please note**: you will NOT be able to recover or change passphrase if you lose it.</source>
|
||||
<target>**Huomaa**: et voi palauttaa tai muuttaa tunnuslausetta, jos kadotat sen.</target>
|
||||
@@ -345,6 +357,10 @@
|
||||
<target>**Varoitus**: Välittömät push-ilmoitukset vaativat tunnuslauseen, joka on tallennettu Keychainiin.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="**Warning**: the archive will be removed." xml:space="preserve">
|
||||
<source>**Warning**: the archive will be removed.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="**e2e encrypted** audio call" xml:space="preserve">
|
||||
<source>**e2e encrypted** audio call</source>
|
||||
<target>**e2e-salattu** äänipuhelu</target>
|
||||
@@ -593,6 +609,10 @@
|
||||
<target>Osoitteenmuutos keskeytetään. Käytetään vanhaa vastaanotto-osoitetta.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Admins can block a member for all." xml:space="preserve">
|
||||
<source>Admins can block a member for all.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Admins can create the links to join groups." xml:space="preserve">
|
||||
<source>Admins can create the links to join groups.</source>
|
||||
<target>Ylläpitäjät voivat luoda linkkejä ryhmiin liittymiseen.</target>
|
||||
@@ -646,6 +666,10 @@
|
||||
<target>Kaikki kontaktisi pysyvät yhteydessä. Profiilipäivitys lähetetään kontakteillesi.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="All your contacts, conversations and files will be securely encrypted and uploaded in chunks to configured XFTP relays." xml:space="preserve">
|
||||
<source>All your contacts, conversations and files will be securely encrypted and uploaded in chunks to configured XFTP relays.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Allow" xml:space="preserve">
|
||||
<source>Allow</source>
|
||||
<target>Salli</target>
|
||||
@@ -691,6 +715,10 @@
|
||||
<target>Salli lähetettyjen viestien peruuttamaton poistaminen. (24 tuntia)</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Allow to send SimpleX links." xml:space="preserve">
|
||||
<source>Allow to send SimpleX links.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Allow to send files and media." xml:space="preserve">
|
||||
<source>Allow to send files and media.</source>
|
||||
<target>Salli tiedostojen ja median lähettäminen.</target>
|
||||
@@ -769,6 +797,10 @@
|
||||
<target>Sovellusversio: %@</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="App data migration" xml:space="preserve">
|
||||
<source>App data migration</source>
|
||||
<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>
|
||||
@@ -803,6 +835,18 @@
|
||||
<target>Ulkonäkö</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Apply" xml:space="preserve">
|
||||
<source>Apply</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Archive and upload" xml:space="preserve">
|
||||
<source>Archive and upload</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Archiving database" xml:space="preserve">
|
||||
<source>Archiving database</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Attach" xml:space="preserve">
|
||||
<source>Attach</source>
|
||||
<target>Liitä</target>
|
||||
@@ -982,6 +1026,10 @@
|
||||
<target>Peruuta</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Cancel migration" xml:space="preserve">
|
||||
<source>Cancel migration</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Cannot access keychain to save database password" xml:space="preserve">
|
||||
<source>Cannot access keychain to save database password</source>
|
||||
<target>Ei pääsyä avainnippuun tietokannan salasanan tallentamiseksi</target>
|
||||
@@ -992,6 +1040,10 @@
|
||||
<target>Tiedostoa ei voi vastaanottaa</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Cellular" xml:space="preserve">
|
||||
<source>Cellular</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Change" xml:space="preserve">
|
||||
<source>Change</source>
|
||||
<target>Muuta</target>
|
||||
@@ -1082,6 +1134,10 @@
|
||||
<source>Chat is stopped. If you already used this database on another device, you should transfer it back before starting chat.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Chat migrated!" xml:space="preserve">
|
||||
<source>Chat migrated!</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Chat preferences" xml:space="preserve">
|
||||
<source>Chat preferences</source>
|
||||
<target>Chat-asetukset</target>
|
||||
@@ -1102,6 +1158,10 @@
|
||||
<target>Kiinalainen ja espanjalainen käyttöliittymä</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Choose _Migrate from another device_ on the new device and scan QR code." xml:space="preserve">
|
||||
<source>Choose _Migrate from another device_ on the new device and scan QR code.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Choose file" xml:space="preserve">
|
||||
<source>Choose file</source>
|
||||
<target>Valitse tiedosto</target>
|
||||
@@ -1171,6 +1231,10 @@
|
||||
<target>Vahvista tietokannan päivitykset</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Confirm network settings" xml:space="preserve">
|
||||
<source>Confirm network settings</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Confirm new passphrase…" xml:space="preserve">
|
||||
<source>Confirm new passphrase…</source>
|
||||
<target>Vahvista uusi tunnuslause…</target>
|
||||
@@ -1181,6 +1245,14 @@
|
||||
<target>Vahvista salasana</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Confirm that you remember database passphrase to migrate it." xml:space="preserve">
|
||||
<source>Confirm that you remember database passphrase to migrate it.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Confirm upload" xml:space="preserve">
|
||||
<source>Confirm upload</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Connect" xml:space="preserve">
|
||||
<source>Connect</source>
|
||||
<target>Yhdistä</target>
|
||||
@@ -1421,6 +1493,10 @@ This is your own one-time link!</source>
|
||||
<target>Luotu %@</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Creating archive link" xml:space="preserve">
|
||||
<source>Creating archive link</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Creating link…" xml:space="preserve">
|
||||
<source>Creating link…</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
@@ -1636,6 +1712,10 @@ This cannot be undone!</source>
|
||||
<target>Poista tietokanta</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Delete database from this device" xml:space="preserve">
|
||||
<source>Delete database from this device</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Delete file" xml:space="preserve">
|
||||
<source>Delete file</source>
|
||||
<target>Poista tiedosto</target>
|
||||
@@ -1920,11 +2000,27 @@ This cannot be undone!</source>
|
||||
<target>Alenna ja avaa keskustelu</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Download" xml:space="preserve">
|
||||
<source>Download</source>
|
||||
<note>chat item action</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Download failed" xml:space="preserve">
|
||||
<source>Download failed</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Download file" xml:space="preserve">
|
||||
<source>Download file</source>
|
||||
<target>Lataa tiedosto</target>
|
||||
<note>server test step</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Downloading archive" xml:space="preserve">
|
||||
<source>Downloading archive</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Downloading link details" xml:space="preserve">
|
||||
<source>Downloading link details</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Duplicate display name!" xml:space="preserve">
|
||||
<source>Duplicate display name!</source>
|
||||
<target>Päällekkäinen näyttönimi!</target>
|
||||
@@ -1979,6 +2075,10 @@ This cannot be undone!</source>
|
||||
<target>Salli kaikille</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Enable in direct chats (BETA)!" xml:space="preserve">
|
||||
<source>Enable in direct chats (BETA)!</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Enable instant notifications?" xml:space="preserve">
|
||||
<source>Enable instant notifications?</source>
|
||||
<target>Salli välittömät ilmoitukset?</target>
|
||||
@@ -2009,6 +2109,10 @@ This cannot be undone!</source>
|
||||
<target>Ota itsetuhoava pääsykoodi käyttöön</target>
|
||||
<note>set passcode view</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Enabled for" xml:space="preserve">
|
||||
<source>Enabled for</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Encrypt" xml:space="preserve">
|
||||
<source>Encrypt</source>
|
||||
<target>Salaa</target>
|
||||
@@ -2089,6 +2193,10 @@ This cannot be undone!</source>
|
||||
<source>Enter group name…</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Enter passphrase" xml:space="preserve">
|
||||
<source>Enter passphrase</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Enter passphrase…" xml:space="preserve">
|
||||
<source>Enter passphrase…</source>
|
||||
<target>Syötä tunnuslause…</target>
|
||||
@@ -2147,6 +2255,10 @@ This cannot be undone!</source>
|
||||
<target>Virhe lisättäessä jäseniä</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Error allowing contact PQ encryption" xml:space="preserve">
|
||||
<source>Error allowing contact PQ encryption</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Error changing address" xml:space="preserve">
|
||||
<source>Error changing address</source>
|
||||
<target>Virhe osoitteenvaihdossa</target>
|
||||
@@ -2235,6 +2347,10 @@ This cannot be undone!</source>
|
||||
<target>Virhe käyttäjäprofiilin poistamisessa</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Error downloading the archive" xml:space="preserve">
|
||||
<source>Error downloading the archive</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Error enabling delivery receipts!" xml:space="preserve">
|
||||
<source>Error enabling delivery receipts!</source>
|
||||
<target>Virhe toimituskuittauksien sallimisessa!</target>
|
||||
@@ -2309,6 +2425,10 @@ This cannot be undone!</source>
|
||||
<target>Virhe tunnuslauseen tallentamisessa avainnippuun</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Error saving settings" xml:space="preserve">
|
||||
<source>Error saving settings</source>
|
||||
<note>when migrating</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Error saving user password" xml:space="preserve">
|
||||
<source>Error saving user password</source>
|
||||
<target>Virhe käyttäjän salasanan tallentamisessa</target>
|
||||
@@ -2377,6 +2497,14 @@ This cannot be undone!</source>
|
||||
<target>Virhe päivitettäessä käyttäjän tietosuojaa</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Error uploading the archive" xml:space="preserve">
|
||||
<source>Error uploading the archive</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Error verifying passphrase:" xml:space="preserve">
|
||||
<source>Error verifying passphrase:</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Error: " xml:space="preserve">
|
||||
<source>Error: </source>
|
||||
<target>Virhe: </target>
|
||||
@@ -2426,6 +2554,10 @@ This cannot be undone!</source>
|
||||
<target>Viety tietokanta-arkisto.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Exported file doesn't exist" xml:space="preserve">
|
||||
<source>Exported file doesn't exist</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Exporting database archive…" xml:space="preserve">
|
||||
<source>Exporting database archive…</source>
|
||||
<target>Tietokanta-arkiston vienti…</target>
|
||||
@@ -2485,6 +2617,10 @@ This cannot be undone!</source>
|
||||
<target>Tiedostot ja media ovat tässä ryhmässä kiellettyjä.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Files and media not allowed" xml:space="preserve">
|
||||
<source>Files and media not allowed</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Files and media prohibited!" xml:space="preserve">
|
||||
<source>Files and media prohibited!</source>
|
||||
<target>Tiedostot ja media kielletty!</target>
|
||||
@@ -2495,6 +2631,14 @@ This cannot be undone!</source>
|
||||
<target>Suodata lukemattomia- ja suosikkikeskusteluja.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Finalize migration" xml:space="preserve">
|
||||
<source>Finalize migration</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Finalize migration on another device." xml:space="preserve">
|
||||
<source>Finalize migration on another device.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Finally, we have them! 🚀" xml:space="preserve">
|
||||
<source>Finally, we have them! 🚀</source>
|
||||
<target>Vihdoinkin meillä! 🚀</target>
|
||||
@@ -2540,6 +2684,18 @@ This cannot be undone!</source>
|
||||
<target>Konsoliin</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Forward" xml:space="preserve">
|
||||
<source>Forward</source>
|
||||
<note>chat item action</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Forwarded" xml:space="preserve">
|
||||
<source>Forwarded</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Forwarded from" xml:space="preserve">
|
||||
<source>Forwarded from</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Found desktop" xml:space="preserve">
|
||||
<source>Found desktop</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
@@ -2646,6 +2802,10 @@ This cannot be undone!</source>
|
||||
<target>Ryhmän jäsenet voivat poistaa lähetetyt viestit peruuttamattomasti. (24 tuntia)</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Group members can send SimpleX links." xml:space="preserve">
|
||||
<source>Group members can send SimpleX links.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Group members can send direct messages." xml:space="preserve">
|
||||
<source>Group members can send direct messages.</source>
|
||||
<target>Ryhmän jäsenet voivat lähettää suoraviestejä.</target>
|
||||
@@ -2780,6 +2940,10 @@ This cannot be undone!</source>
|
||||
<target>Miten käytät palvelimiasi</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Hungarian interface" xml:space="preserve">
|
||||
<source>Hungarian interface</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="ICE servers (one per line)" xml:space="preserve">
|
||||
<source>ICE servers (one per line)</source>
|
||||
<target>ICE-palvelimet (yksi per rivi)</target>
|
||||
@@ -2845,6 +3009,14 @@ This cannot be undone!</source>
|
||||
<target>Tuo tietokanta</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Import failed" xml:space="preserve">
|
||||
<source>Import failed</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Importing archive" xml:space="preserve">
|
||||
<source>Importing archive</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Improved message delivery" xml:space="preserve">
|
||||
<source>Improved message delivery</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
@@ -2859,6 +3031,10 @@ This cannot be undone!</source>
|
||||
<target>Parannettu palvelimen kokoonpano</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="In order to continue, chat should be stopped." xml:space="preserve">
|
||||
<source>In order to continue, chat should be stopped.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="In reply to" xml:space="preserve">
|
||||
<source>In reply to</source>
|
||||
<target>Vastauksena</target>
|
||||
@@ -2966,6 +3142,10 @@ This cannot be undone!</source>
|
||||
<source>Invalid link</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Invalid migration confirmation" xml:space="preserve">
|
||||
<source>Invalid migration confirmation</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Invalid name!" xml:space="preserve">
|
||||
<source>Invalid name!</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
@@ -3321,6 +3501,10 @@ This is your link for group %@!</source>
|
||||
<target>Viestin teksti</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Message too large" xml:space="preserve">
|
||||
<source>Message too large</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Messages" xml:space="preserve">
|
||||
<source>Messages</source>
|
||||
<target>Viestit</target>
|
||||
@@ -3335,11 +3519,47 @@ This is your link for group %@!</source>
|
||||
<source>Messages from %@ will be shown!</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Messages, files and calls are protected by **end-to-end encryption** with perfect forward secrecy, repudiation and break-in recovery." xml:space="preserve">
|
||||
<source>Messages, files and calls are protected by **end-to-end encryption** with perfect forward secrecy, repudiation and break-in recovery.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Messages, files and calls are protected by **quantum resistant e2e encryption** with perfect forward secrecy, repudiation and break-in recovery." xml:space="preserve">
|
||||
<source>Messages, files and calls are protected by **quantum resistant e2e encryption** with perfect forward secrecy, repudiation and break-in recovery.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Migrate device" xml:space="preserve">
|
||||
<source>Migrate device</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Migrate from another device" xml:space="preserve">
|
||||
<source>Migrate from another device</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Migrate here" xml:space="preserve">
|
||||
<source>Migrate here</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Migrate to another device" xml:space="preserve">
|
||||
<source>Migrate to another device</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Migrate to another device via QR code." xml:space="preserve">
|
||||
<source>Migrate to another device via QR code.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Migrating" xml:space="preserve">
|
||||
<source>Migrating</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Migrating database archive…" xml:space="preserve">
|
||||
<source>Migrating database archive…</source>
|
||||
<target>Siirretään tietokannan arkistoa…</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Migration complete" xml:space="preserve">
|
||||
<source>Migration complete</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Migration error:" xml:space="preserve">
|
||||
<source>Migration error:</source>
|
||||
<target>Siirtovirhe:</target>
|
||||
@@ -3415,6 +3635,10 @@ This is your link for group %@!</source>
|
||||
<target>Verkko ja palvelimet</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Network connection" xml:space="preserve">
|
||||
<source>Network connection</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Network settings" xml:space="preserve">
|
||||
<source>Network settings</source>
|
||||
<target>Verkkoasetukset</target>
|
||||
@@ -3523,6 +3747,10 @@ This is your link for group %@!</source>
|
||||
<target>Ei historiaa</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="No network connection" xml:space="preserve">
|
||||
<source>No network connection</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="No permission to record voice message" xml:space="preserve">
|
||||
<source>No permission to record voice message</source>
|
||||
<target>Ei lupaa ääniviestin tallentamiseen</target>
|
||||
@@ -3693,6 +3921,10 @@ This is your link for group %@!</source>
|
||||
<source>Open group</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Open migration to another device" xml:space="preserve">
|
||||
<source>Open migration to another device</source>
|
||||
<note>authentication reason</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Open user profiles" xml:space="preserve">
|
||||
<source>Open user profiles</source>
|
||||
<target>Avaa käyttäjäprofiilit</target>
|
||||
@@ -3707,14 +3939,26 @@ This is your link for group %@!</source>
|
||||
<source>Opening app…</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Or paste archive link" xml:space="preserve">
|
||||
<source>Or paste archive link</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Or scan QR code" xml:space="preserve">
|
||||
<source>Or scan QR code</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Or securely share this file link" xml:space="preserve">
|
||||
<source>Or securely share this file link</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Or show this code" xml:space="preserve">
|
||||
<source>Or show this code</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Other" xml:space="preserve">
|
||||
<source>Other</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="PING count" xml:space="preserve">
|
||||
<source>PING count</source>
|
||||
<target>PING-määrä</target>
|
||||
@@ -3791,6 +4035,10 @@ This is your link for group %@!</source>
|
||||
<target>Pysyvä salauksen purkuvirhe</target>
|
||||
<note>message decrypt error item</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Picture-in-picture calls" xml:space="preserve">
|
||||
<source>Picture-in-picture calls</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</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>Pyydä kontaktiasi sallimaan ääniviestien lähettäminen.</target>
|
||||
@@ -3811,6 +4059,10 @@ This is your link for group %@!</source>
|
||||
<target>Tarkista omasi ja kontaktin asetukset.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Please confirm that network settings are correct for this device." xml:space="preserve">
|
||||
<source>Please confirm that network settings are correct for this device.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Please contact developers. Error: %@" xml:space="preserve">
|
||||
<source>Please contact developers.
|
||||
Error: %@</source>
|
||||
@@ -3866,6 +4118,10 @@ Error: %@</source>
|
||||
<target>Palvelimen osoitteen varmenteen sormenjälki on mahdollisesti virheellinen</target>
|
||||
<note>server test error</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Post-quantum E2EE" xml:space="preserve">
|
||||
<source>Post-quantum E2EE</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Preserve the last message draft, with attachments." xml:space="preserve">
|
||||
<source>Preserve the last message draft, with attachments.</source>
|
||||
<target>Säilytä viimeinen viestiluonnos liitteineen.</target>
|
||||
@@ -3953,6 +4209,10 @@ Error: %@</source>
|
||||
<target>Estä viestireaktiot.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Prohibit sending SimpleX links." xml:space="preserve">
|
||||
<source>Prohibit sending SimpleX links.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Prohibit sending direct messages to members." xml:space="preserve">
|
||||
<source>Prohibit sending direct messages to members.</source>
|
||||
<target>Estä suorien viestien lähettäminen jäsenille.</target>
|
||||
@@ -3998,6 +4258,14 @@ Error: %@</source>
|
||||
<target>Push-ilmoitukset</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Push server" xml:space="preserve">
|
||||
<source>Push server</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Quantum resistant encryption" xml:space="preserve">
|
||||
<source>Quantum resistant encryption</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Rate the app" xml:space="preserve">
|
||||
<source>Rate the app</source>
|
||||
<target>Arvioi sovellus</target>
|
||||
@@ -4072,6 +4340,10 @@ Error: %@</source>
|
||||
<target>Vastaanotto-osoite vaihdetaan toiseen palvelimeen. Osoitteenmuutos tehdään sen jälkeen, kun lähettäjä tulee verkkoon.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Receiving concurrency" xml:space="preserve">
|
||||
<source>Receiving concurrency</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Receiving file will be stopped." xml:space="preserve">
|
||||
<source>Receiving file will be stopped.</source>
|
||||
<target>Tiedoston vastaanotto pysäytetään.</target>
|
||||
@@ -4086,6 +4358,10 @@ Error: %@</source>
|
||||
<source>Recent history and improved [directory bot](simplex:/contact#/?v=1-4&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FeXSPwqTkKyDO3px4fLf1wx3MvPdjdLW3%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAaiv6MkMH44L2TcYrt_CsX3ZvM11WgbMEUn0hkIKTOho%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion).</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Recipient(s) can't see who this message is from." xml:space="preserve">
|
||||
<source>Recipient(s) can't see who this message is from.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Recipients see updates as you type them." xml:space="preserve">
|
||||
<source>Recipients see updates as you type them.</source>
|
||||
<target>Vastaanottajat näkevät päivitykset, kun kirjoitat niitä.</target>
|
||||
@@ -4180,10 +4456,22 @@ Error: %@</source>
|
||||
<source>Repeat connection request?</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Repeat download" xml:space="preserve">
|
||||
<source>Repeat download</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Repeat import" xml:space="preserve">
|
||||
<source>Repeat import</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Repeat join request?" xml:space="preserve">
|
||||
<source>Repeat join request?</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Repeat upload" xml:space="preserve">
|
||||
<source>Repeat upload</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Reply" xml:space="preserve">
|
||||
<source>Reply</source>
|
||||
<target>Vastaa</target>
|
||||
@@ -4283,6 +4571,10 @@ Error: %@</source>
|
||||
<target>SMP-palvelimet</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Safer groups" xml:space="preserve">
|
||||
<source>Safer groups</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Save" xml:space="preserve">
|
||||
<source>Save</source>
|
||||
<target>Tallenna</target>
|
||||
@@ -4363,11 +4655,19 @@ Error: %@</source>
|
||||
<target>Tallenna tervetuloviesti?</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Saved" xml:space="preserve">
|
||||
<source>Saved</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Saved WebRTC ICE servers will be removed" xml:space="preserve">
|
||||
<source>Saved WebRTC ICE servers will be removed</source>
|
||||
<target>Tallennetut WebRTC ICE -palvelimet poistetaan</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Saved from" xml:space="preserve">
|
||||
<source>Saved from</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Saved message" xml:space="preserve">
|
||||
<source>Saved message</source>
|
||||
<note>message info title</note>
|
||||
@@ -4641,6 +4941,10 @@ Error: %@</source>
|
||||
<target>Aseta pääsykoodi</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Set passphrase" xml:space="preserve">
|
||||
<source>Set passphrase</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Set passphrase to export" xml:space="preserve">
|
||||
<source>Set passphrase to export</source>
|
||||
<target>Aseta tunnuslause vientiä varten</target>
|
||||
@@ -4695,6 +4999,10 @@ Error: %@</source>
|
||||
<target>Jaa kontaktien kanssa</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Show QR code" xml:space="preserve">
|
||||
<source>Show QR code</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Show calls in phone history" xml:space="preserve">
|
||||
<source>Show calls in phone history</source>
|
||||
<target>Näytä puhelut puhelinhistoriassa</target>
|
||||
@@ -4773,6 +5081,14 @@ Error: %@</source>
|
||||
<trans-unit id="SimpleX links" xml:space="preserve">
|
||||
<source>SimpleX links</source>
|
||||
<target>SimpleX-linkit</target>
|
||||
<note>chat feature</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="SimpleX links are prohibited in this group." xml:space="preserve">
|
||||
<source>SimpleX links are prohibited in this group.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="SimpleX links not allowed" xml:space="preserve">
|
||||
<source>SimpleX links not allowed</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="SimpleX one-time invitation" xml:space="preserve">
|
||||
@@ -4833,6 +5149,10 @@ Error: %@</source>
|
||||
<target>Lopeta SimpleX</target>
|
||||
<note>authentication reason</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Stop chat" xml:space="preserve">
|
||||
<source>Stop chat</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Stop chat to enable database actions" xml:space="preserve">
|
||||
<source>Stop chat to enable database actions</source>
|
||||
<target>Pysäytä keskustelu tietokantatoimien mahdollistamiseksi</target>
|
||||
@@ -4873,6 +5193,10 @@ Error: %@</source>
|
||||
<target>Lopeta osoitteen jakaminen?</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Stopping chat" xml:space="preserve">
|
||||
<source>Stopping chat</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Submit" xml:space="preserve">
|
||||
<source>Submit</source>
|
||||
<target>Lähetä</target>
|
||||
@@ -5115,6 +5439,14 @@ Tämä voi johtua jostain virheestä tai siitä, että yhteys on vaarantunut.</t
|
||||
<target>Tätä toimintoa ei voi kumota - profiilisi, kontaktisi, viestisi ja tiedostosi poistuvat peruuttamattomasti.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="This chat is protected by end-to-end encryption." xml:space="preserve">
|
||||
<source>This chat is protected by end-to-end encryption.</source>
|
||||
<note>E2EE info chat item</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="This chat is protected by quantum resistant end-to-end encryption." xml:space="preserve">
|
||||
<source>This chat is protected by quantum resistant end-to-end encryption.</source>
|
||||
<note>E2EE info chat item</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="This device name" xml:space="preserve">
|
||||
<source>This device name</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
@@ -5394,11 +5726,19 @@ Jos haluat muodostaa yhteyden, pyydä kontaktiasi luomaan toinen yhteyslinkki ja
|
||||
<target>Päivitä ja avaa keskustelu</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Upload failed" xml:space="preserve">
|
||||
<source>Upload failed</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Upload file" xml:space="preserve">
|
||||
<source>Upload file</source>
|
||||
<target>Lataa tiedosto</target>
|
||||
<note>server test step</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Uploading archive" xml:space="preserve">
|
||||
<source>Uploading archive</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Use .onion hosts" xml:space="preserve">
|
||||
<source>Use .onion hosts</source>
|
||||
<target>Käytä .onion-isäntiä</target>
|
||||
@@ -5447,6 +5787,10 @@ Jos haluat muodostaa yhteyden, pyydä kontaktiasi luomaan toinen yhteyslinkki ja
|
||||
<target>Käytä palvelinta</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Use the app while in the call." xml:space="preserve">
|
||||
<source>Use the app while in the call.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="User profile" xml:space="preserve">
|
||||
<source>User profile</source>
|
||||
<target>Käyttäjäprofiili</target>
|
||||
@@ -5479,6 +5823,14 @@ Jos haluat muodostaa yhteyden, pyydä kontaktiasi luomaan toinen yhteyslinkki ja
|
||||
<source>Verify connections</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Verify database passphrase" xml:space="preserve">
|
||||
<source>Verify database passphrase</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Verify passphrase" xml:space="preserve">
|
||||
<source>Verify passphrase</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Verify security code" xml:space="preserve">
|
||||
<source>Verify security code</source>
|
||||
<target>Tarkista turvakoodi</target>
|
||||
@@ -5537,6 +5889,10 @@ Jos haluat muodostaa yhteyden, pyydä kontaktiasi luomaan toinen yhteyslinkki ja
|
||||
<target>Ääniviestit ovat kiellettyjä tässä ryhmässä.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Voice messages not allowed" xml:space="preserve">
|
||||
<source>Voice messages not allowed</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Voice messages prohibited!" xml:space="preserve">
|
||||
<source>Voice messages prohibited!</source>
|
||||
<target>Ääniviestit kielletty!</target>
|
||||
@@ -5566,6 +5922,10 @@ Jos haluat muodostaa yhteyden, pyydä kontaktiasi luomaan toinen yhteyslinkki ja
|
||||
<target>Odottaa videota</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Warning: starting chat on multiple devices is not supported and will cause message delivery failures" xml:space="preserve">
|
||||
<source>Warning: starting chat on multiple devices is not supported and will cause message delivery failures</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Warning: you may lose some data!" xml:space="preserve">
|
||||
<source>Warning: you may lose some data!</source>
|
||||
<target>Varoitus: saatat menettää joitain tietoja!</target>
|
||||
@@ -5586,6 +5946,10 @@ Jos haluat muodostaa yhteyden, pyydä kontaktiasi luomaan toinen yhteyslinkki ja
|
||||
<target>Tervetuloviesti</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Welcome message is too long" xml:space="preserve">
|
||||
<source>Welcome message is too long</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="What's new" xml:space="preserve">
|
||||
<source>What's new</source>
|
||||
<target>Uusimmat</target>
|
||||
@@ -5606,6 +5970,14 @@ Jos haluat muodostaa yhteyden, pyydä kontaktiasi luomaan toinen yhteyslinkki ja
|
||||
<target>Kun jaat inkognitoprofiilin jonkun kanssa, tätä profiilia käytetään ryhmissä, joihin tämä sinut kutsuu.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="WiFi" xml:space="preserve">
|
||||
<source>WiFi</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Wired ethernet" xml:space="preserve">
|
||||
<source>Wired ethernet</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="With encrypted files and media." xml:space="preserve">
|
||||
<source>With encrypted files and media.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
@@ -5639,6 +6011,10 @@ Jos haluat muodostaa yhteyden, pyydä kontaktiasi luomaan toinen yhteyslinkki ja
|
||||
<target>Sinä</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="You **must not** use the same database on two devices." xml:space="preserve">
|
||||
<source>You **must not** use the same database on two devices.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="You accepted connection" xml:space="preserve">
|
||||
<source>You accepted connection</source>
|
||||
<target>Hyväksyit yhteyden</target>
|
||||
@@ -5718,6 +6094,10 @@ Repeat join request?</source>
|
||||
<target>Voit ottaa ne käyttöön myöhemmin sovelluksen Yksityisyys & Turvallisuus -asetuksista.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="You can give another try." xml:space="preserve">
|
||||
<source>You can give another try.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="You can hide or mute a user profile - swipe it to the right." xml:space="preserve">
|
||||
<source>You can hide or mute a user profile - swipe it to the right.</source>
|
||||
<target>Voit piilottaa tai mykistää käyttäjäprofiilin pyyhkäisemällä sitä oikealle.</target>
|
||||
@@ -6061,6 +6441,10 @@ SimpleX-palvelimet eivät näe profiiliasi.</target>
|
||||
<target>ylläpitäjä</target>
|
||||
<note>member role</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="admins" xml:space="preserve">
|
||||
<source>admins</source>
|
||||
<note>feature role</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="agreeing encryption for %@…" xml:space="preserve">
|
||||
<source>agreeing encryption for %@…</source>
|
||||
<target>salauksesta sovitaan %@:lle…</target>
|
||||
@@ -6071,6 +6455,10 @@ SimpleX-palvelimet eivät näe profiiliasi.</target>
|
||||
<target>hyväksyy salausta…</target>
|
||||
<note>chat item text</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="all members" xml:space="preserve">
|
||||
<source>all members</source>
|
||||
<note>feature role</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="always" xml:space="preserve">
|
||||
<source>always</source>
|
||||
<target>aina</target>
|
||||
@@ -6101,7 +6489,7 @@ SimpleX-palvelimet eivät näe profiiliasi.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="blocked" xml:space="preserve">
|
||||
<source>blocked</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
<note>marked deleted chat item preview text</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="blocked %@" xml:space="preserve">
|
||||
<source>blocked %@</source>
|
||||
@@ -6109,7 +6497,7 @@ SimpleX-palvelimet eivät näe profiiliasi.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="blocked by admin" xml:space="preserve">
|
||||
<source>blocked by admin</source>
|
||||
<note>blocked chat item</note>
|
||||
<note>marked deleted chat item preview text</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="bold" xml:space="preserve">
|
||||
<source>bold</source>
|
||||
@@ -6393,6 +6781,10 @@ SimpleX-palvelimet eivät näe profiiliasi.</target>
|
||||
<target>tapahtuma tapahtui</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="forwarded" xml:space="preserve">
|
||||
<source>forwarded</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="group deleted" xml:space="preserve">
|
||||
<source>group deleted</source>
|
||||
<target>ryhmä poistettu</target>
|
||||
@@ -6535,7 +6927,7 @@ SimpleX-palvelimet eivät näe profiiliasi.</target>
|
||||
<trans-unit id="moderated by %@" xml:space="preserve">
|
||||
<source>moderated by %@</source>
|
||||
<target>%@ moderoi</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
<note>marked deleted chat item preview text</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="months" xml:space="preserve">
|
||||
<source>months</source>
|
||||
@@ -6599,11 +6991,19 @@ SimpleX-palvelimet eivät näe profiiliasi.</target>
|
||||
<target>omistaja</target>
|
||||
<note>member role</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="owners" xml:space="preserve">
|
||||
<source>owners</source>
|
||||
<note>feature role</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="peer-to-peer" xml:space="preserve">
|
||||
<source>peer-to-peer</source>
|
||||
<target>vertais</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="quantum resistant e2e encryption" xml:space="preserve">
|
||||
<source>quantum resistant e2e encryption</source>
|
||||
<note>chat item text</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="received answer…" xml:space="preserve">
|
||||
<source>received answer…</source>
|
||||
<target>vastaus saatu…</target>
|
||||
@@ -6642,6 +7042,14 @@ SimpleX-palvelimet eivät näe profiiliasi.</target>
|
||||
<target>poisti sinut</target>
|
||||
<note>rcv group event chat item</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="saved" xml:space="preserve">
|
||||
<source>saved</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="saved from %@" xml:space="preserve">
|
||||
<source>saved from %@</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="sec" xml:space="preserve">
|
||||
<source>sec</source>
|
||||
<target>sek</target>
|
||||
@@ -6674,6 +7082,10 @@ SimpleX-palvelimet eivät näe profiiliasi.</target>
|
||||
<source>set new profile picture</source>
|
||||
<note>profile update event chat item</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="standard end-to-end encryption" xml:space="preserve">
|
||||
<source>standard end-to-end encryption</source>
|
||||
<note>chat item text</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="starting…" xml:space="preserve">
|
||||
<source>starting…</source>
|
||||
<target>alkaa…</target>
|
||||
@@ -6770,6 +7182,10 @@ SimpleX-palvelimet eivät näe profiiliasi.</target>
|
||||
<target>kyllä</target>
|
||||
<note>pref value</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="you" xml:space="preserve">
|
||||
<source>you</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="you are invited to group" xml:space="preserve">
|
||||
<source>you are invited to group</source>
|
||||
<target>sinut on kutsuttu ryhmään</target>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -5311,6 +5311,11 @@ SimpleX servers cannot see your profile.</source>
|
||||
<target state="translated">ללא היסטוריה</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="%@ and %@" xml:space="preserve" approved="no">
|
||||
<source>%@ and %@</source>
|
||||
<target state="translated">%@ ו-%@</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
<file original="en.lproj/SimpleX--iOS--InfoPlist.strings" source-language="en" target-language="he" datatype="plaintext">
|
||||
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"colors" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"locale" : "hu"
|
||||
}
|
||||
],
|
||||
"properties" : {
|
||||
"localizable" : true
|
||||
},
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
+23
@@ -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"
|
||||
}
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
+6
@@ -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!";
|
||||
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
/* 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 - Local Network Usage Description */
|
||||
"NSLocalNetworkUsageDescription" = "SimpleX uses local network access to allow using user chat profile via desktop app on the same network.";
|
||||
/* Privacy - Microphone Usage Description */
|
||||
"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";
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"developmentRegion" : "en",
|
||||
"project" : "SimpleX.xcodeproj",
|
||||
"targetLocale" : "hu",
|
||||
"toolInfo" : {
|
||||
"toolBuildNumber" : "15A240d",
|
||||
"toolID" : "com.apple.dt.xcode",
|
||||
"toolName" : "Xcode",
|
||||
"toolVersion" : "15.0"
|
||||
},
|
||||
"version" : "1.0"
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -89,6 +89,7 @@
|
||||
</trans-unit>
|
||||
<trans-unit id="%@ and %@" xml:space="preserve">
|
||||
<source>%@ and %@</source>
|
||||
<target>%@ と %@</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="%@ and %@ connected" xml:space="preserve">
|
||||
@@ -106,6 +107,10 @@
|
||||
<target>%@ 接続中</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="%@ downloaded" xml:space="preserve">
|
||||
<source>%@ downloaded</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="%@ is connected!" xml:space="preserve">
|
||||
<source>%@ is connected!</source>
|
||||
<target>%@ 接続中!</target>
|
||||
@@ -126,6 +131,10 @@
|
||||
<target>%@ サーバー</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="%@ uploaded" xml:space="preserve">
|
||||
<source>%@ uploaded</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="%@ wants to connect!" xml:space="preserve">
|
||||
<source>%@ wants to connect!</source>
|
||||
<target>%@ が接続を希望しています!</target>
|
||||
@@ -333,6 +342,10 @@
|
||||
<target>**最もプライベート**: SimpleX Chat 通知サーバーを使用せず、バックグラウンドで定期的にメッセージをチェックします (アプリの使用頻度によって異なります)。</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="**Please note**: using the same database on two devices will break the decryption of messages from your connections, as a security protection." xml:space="preserve">
|
||||
<source>**Please note**: using the same database on two devices will break the decryption of messages from your connections, as a security protection.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="**Please note**: you will NOT be able to recover or change passphrase if you lose it." xml:space="preserve">
|
||||
<source>**Please note**: you will NOT be able to recover or change passphrase if you lose it.</source>
|
||||
<target>**注意**: パスフレーズを紛失すると、パスフレーズを復元または変更できなくなります。</target>
|
||||
@@ -348,6 +361,10 @@
|
||||
<target>**警告**: 即時の プッシュ通知には、キーチェーンに保存されたパスフレーズが必要です。</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="**Warning**: the archive will be removed." xml:space="preserve">
|
||||
<source>**Warning**: the archive will be removed.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="**e2e encrypted** audio call" xml:space="preserve">
|
||||
<source>**e2e encrypted** audio call</source>
|
||||
<target>**e2e 暗号化**音声通話</target>
|
||||
@@ -596,6 +613,10 @@
|
||||
<target>アドレス変更は中止されます。古い受信アドレスが使用されます。</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Admins can block a member for all." xml:space="preserve">
|
||||
<source>Admins can block a member for all.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Admins can create the links to join groups." xml:space="preserve">
|
||||
<source>Admins can create the links to join groups.</source>
|
||||
<target>管理者はグループの参加リンクを生成できます。</target>
|
||||
@@ -649,6 +670,10 @@
|
||||
<target>すべての連絡先は維持されます。連絡先に更新されたプロフィールを送信します。</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="All your contacts, conversations and files will be securely encrypted and uploaded in chunks to configured XFTP relays." xml:space="preserve">
|
||||
<source>All your contacts, conversations and files will be securely encrypted and uploaded in chunks to configured XFTP relays.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Allow" xml:space="preserve">
|
||||
<source>Allow</source>
|
||||
<target>許可</target>
|
||||
@@ -694,6 +719,10 @@
|
||||
<target>送信済みメッセージの永久削除を許可する。</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Allow to send SimpleX links." xml:space="preserve">
|
||||
<source>Allow to send SimpleX links.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Allow to send files and media." xml:space="preserve">
|
||||
<source>Allow to send files and media.</source>
|
||||
<target>ファイルやメディアの送信を許可する。</target>
|
||||
@@ -772,6 +801,10 @@
|
||||
<target>アプリのビルド: %@</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="App data migration" xml:space="preserve">
|
||||
<source>App data migration</source>
|
||||
<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>
|
||||
@@ -807,6 +840,18 @@
|
||||
<target>見た目</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Apply" xml:space="preserve">
|
||||
<source>Apply</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Archive and upload" xml:space="preserve">
|
||||
<source>Archive and upload</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Archiving database" xml:space="preserve">
|
||||
<source>Archiving database</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Attach" xml:space="preserve">
|
||||
<source>Attach</source>
|
||||
<target>添付する</target>
|
||||
@@ -987,6 +1032,10 @@
|
||||
<target>中止</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Cancel migration" xml:space="preserve">
|
||||
<source>Cancel migration</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Cannot access keychain to save database password" xml:space="preserve">
|
||||
<source>Cannot access keychain to save database password</source>
|
||||
<target>データベースのパスワードを保存するためのキーチェーンにアクセスできません</target>
|
||||
@@ -997,6 +1046,10 @@
|
||||
<target>ファイル受信ができません</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Cellular" xml:space="preserve">
|
||||
<source>Cellular</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Change" xml:space="preserve">
|
||||
<source>Change</source>
|
||||
<target>変更</target>
|
||||
@@ -1087,6 +1140,10 @@
|
||||
<source>Chat is stopped. If you already used this database on another device, you should transfer it back before starting chat.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Chat migrated!" xml:space="preserve">
|
||||
<source>Chat migrated!</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Chat preferences" xml:space="preserve">
|
||||
<source>Chat preferences</source>
|
||||
<target>チャット設定</target>
|
||||
@@ -1107,6 +1164,10 @@
|
||||
<target>中国語とスペイン語UI</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Choose _Migrate from another device_ on the new device and scan QR code." xml:space="preserve">
|
||||
<source>Choose _Migrate from another device_ on the new device and scan QR code.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Choose file" xml:space="preserve">
|
||||
<source>Choose file</source>
|
||||
<target>ファイルを選択</target>
|
||||
@@ -1176,6 +1237,10 @@
|
||||
<target>データベースのアップグレードを確認</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Confirm network settings" xml:space="preserve">
|
||||
<source>Confirm network settings</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Confirm new passphrase…" xml:space="preserve">
|
||||
<source>Confirm new passphrase…</source>
|
||||
<target>新しいパスフレーズを確認…</target>
|
||||
@@ -1186,6 +1251,14 @@
|
||||
<target>パスワードを確認</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Confirm that you remember database passphrase to migrate it." xml:space="preserve">
|
||||
<source>Confirm that you remember database passphrase to migrate it.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Confirm upload" xml:space="preserve">
|
||||
<source>Confirm upload</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Connect" xml:space="preserve">
|
||||
<source>Connect</source>
|
||||
<target>接続</target>
|
||||
@@ -1426,6 +1499,10 @@ This is your own one-time link!</source>
|
||||
<target>%@ によって作成されました</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Creating archive link" xml:space="preserve">
|
||||
<source>Creating archive link</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Creating link…" xml:space="preserve">
|
||||
<source>Creating link…</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
@@ -1641,6 +1718,10 @@ This cannot be undone!</source>
|
||||
<target>データベースを削除</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Delete database from this device" xml:space="preserve">
|
||||
<source>Delete database from this device</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Delete file" xml:space="preserve">
|
||||
<source>Delete file</source>
|
||||
<target>ファイルの削除</target>
|
||||
@@ -1925,11 +2006,27 @@ This cannot be undone!</source>
|
||||
<target>ダウングレードしてチャットを開く</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Download" xml:space="preserve">
|
||||
<source>Download</source>
|
||||
<note>chat item action</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Download failed" xml:space="preserve">
|
||||
<source>Download failed</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Download file" xml:space="preserve">
|
||||
<source>Download file</source>
|
||||
<target>ファイルをダウンロード</target>
|
||||
<note>server test step</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Downloading archive" xml:space="preserve">
|
||||
<source>Downloading archive</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Downloading link details" xml:space="preserve">
|
||||
<source>Downloading link details</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Duplicate display name!" xml:space="preserve">
|
||||
<source>Duplicate display name!</source>
|
||||
<target>表示の名前が重複してます!</target>
|
||||
@@ -1984,6 +2081,10 @@ This cannot be undone!</source>
|
||||
<target>すべて有効</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Enable in direct chats (BETA)!" xml:space="preserve">
|
||||
<source>Enable in direct chats (BETA)!</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Enable instant notifications?" xml:space="preserve">
|
||||
<source>Enable instant notifications?</source>
|
||||
<target>即時通知を有効にしますか?</target>
|
||||
@@ -2014,6 +2115,10 @@ This cannot be undone!</source>
|
||||
<target>自己破壊パスコードを有効にする</target>
|
||||
<note>set passcode view</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Enabled for" xml:space="preserve">
|
||||
<source>Enabled for</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Encrypt" xml:space="preserve">
|
||||
<source>Encrypt</source>
|
||||
<target>暗号化する</target>
|
||||
@@ -2095,6 +2200,10 @@ This cannot be undone!</source>
|
||||
<source>Enter group name…</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Enter passphrase" xml:space="preserve">
|
||||
<source>Enter passphrase</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Enter passphrase…" xml:space="preserve">
|
||||
<source>Enter passphrase…</source>
|
||||
<target>暗証フレーズを入力…</target>
|
||||
@@ -2153,6 +2262,10 @@ This cannot be undone!</source>
|
||||
<target>メンバー追加にエラー発生</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Error allowing contact PQ encryption" xml:space="preserve">
|
||||
<source>Error allowing contact PQ encryption</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Error changing address" xml:space="preserve">
|
||||
<source>Error changing address</source>
|
||||
<target>アドレス変更にエラー発生</target>
|
||||
@@ -2242,6 +2355,10 @@ This cannot be undone!</source>
|
||||
<target>ユーザのプロフィール削除にエラー発生</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Error downloading the archive" xml:space="preserve">
|
||||
<source>Error downloading the archive</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Error enabling delivery receipts!" xml:space="preserve">
|
||||
<source>Error enabling delivery receipts!</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
@@ -2315,6 +2432,10 @@ This cannot be undone!</source>
|
||||
<target>キーチェーンにパスフレーズを保存にエラー発生</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Error saving settings" xml:space="preserve">
|
||||
<source>Error saving settings</source>
|
||||
<note>when migrating</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Error saving user password" xml:space="preserve">
|
||||
<source>Error saving user password</source>
|
||||
<target>ユーザーパスワード保存エラー</target>
|
||||
@@ -2383,6 +2504,14 @@ This cannot be undone!</source>
|
||||
<target>ユーザープライバシーの更新のエラー</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Error uploading the archive" xml:space="preserve">
|
||||
<source>Error uploading the archive</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Error verifying passphrase:" xml:space="preserve">
|
||||
<source>Error verifying passphrase:</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Error: " xml:space="preserve">
|
||||
<source>Error: </source>
|
||||
<target>エラー : </target>
|
||||
@@ -2432,6 +2561,10 @@ This cannot be undone!</source>
|
||||
<target>データベースのアーカイブをエクスポートします。</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Exported file doesn't exist" xml:space="preserve">
|
||||
<source>Exported file doesn't exist</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Exporting database archive…" xml:space="preserve">
|
||||
<source>Exporting database archive…</source>
|
||||
<target>データベース アーカイブをエクスポートしています…</target>
|
||||
@@ -2491,6 +2624,10 @@ This cannot be undone!</source>
|
||||
<target>このグループでは、ファイルとメディアは禁止されています。</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Files and media not allowed" xml:space="preserve">
|
||||
<source>Files and media not allowed</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Files and media prohibited!" xml:space="preserve">
|
||||
<source>Files and media prohibited!</source>
|
||||
<target>ファイルとメディアは禁止されています!</target>
|
||||
@@ -2501,6 +2638,14 @@ This cannot be undone!</source>
|
||||
<target>未読とお気に入りをフィルターします。</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Finalize migration" xml:space="preserve">
|
||||
<source>Finalize migration</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Finalize migration on another device." xml:space="preserve">
|
||||
<source>Finalize migration on another device.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Finally, we have them! 🚀" xml:space="preserve">
|
||||
<source>Finally, we have them! 🚀</source>
|
||||
<target>ついに、私たちはそれらを手に入れました! 🚀</target>
|
||||
@@ -2546,6 +2691,18 @@ This cannot be undone!</source>
|
||||
<target>コンソール</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Forward" xml:space="preserve">
|
||||
<source>Forward</source>
|
||||
<note>chat item action</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Forwarded" xml:space="preserve">
|
||||
<source>Forwarded</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Forwarded from" xml:space="preserve">
|
||||
<source>Forwarded from</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Found desktop" xml:space="preserve">
|
||||
<source>Found desktop</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
@@ -2652,6 +2809,10 @@ This cannot be undone!</source>
|
||||
<target>グループのメンバーがメッセージを完全削除することができます。</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Group members can send SimpleX links." xml:space="preserve">
|
||||
<source>Group members can send SimpleX links.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Group members can send direct messages." xml:space="preserve">
|
||||
<source>Group members can send direct messages.</source>
|
||||
<target>グループのメンバーがダイレクトメッセージを送信できます。</target>
|
||||
@@ -2786,6 +2947,10 @@ This cannot be undone!</source>
|
||||
<target>自分のサーバの使い方</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Hungarian interface" xml:space="preserve">
|
||||
<source>Hungarian interface</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="ICE servers (one per line)" xml:space="preserve">
|
||||
<source>ICE servers (one per line)</source>
|
||||
<target>ICEサーバ (1行に1サーバ)</target>
|
||||
@@ -2851,6 +3016,14 @@ This cannot be undone!</source>
|
||||
<target>データベースを読み込む</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Import failed" xml:space="preserve">
|
||||
<source>Import failed</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Importing archive" xml:space="preserve">
|
||||
<source>Importing archive</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Improved message delivery" xml:space="preserve">
|
||||
<source>Improved message delivery</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
@@ -2865,6 +3038,10 @@ This cannot be undone!</source>
|
||||
<target>サーバ設定の向上</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="In order to continue, chat should be stopped." xml:space="preserve">
|
||||
<source>In order to continue, chat should be stopped.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="In reply to" xml:space="preserve">
|
||||
<source>In reply to</source>
|
||||
<target>返信先</target>
|
||||
@@ -2972,6 +3149,10 @@ This cannot be undone!</source>
|
||||
<source>Invalid link</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Invalid migration confirmation" xml:space="preserve">
|
||||
<source>Invalid migration confirmation</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Invalid name!" xml:space="preserve">
|
||||
<source>Invalid name!</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
@@ -3326,6 +3507,10 @@ This is your link for group %@!</source>
|
||||
<target>メッセージ内容</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Message too large" xml:space="preserve">
|
||||
<source>Message too large</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Messages" xml:space="preserve">
|
||||
<source>Messages</source>
|
||||
<target>メッセージ</target>
|
||||
@@ -3340,11 +3525,47 @@ This is your link for group %@!</source>
|
||||
<source>Messages from %@ will be shown!</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Messages, files and calls are protected by **end-to-end encryption** with perfect forward secrecy, repudiation and break-in recovery." xml:space="preserve">
|
||||
<source>Messages, files and calls are protected by **end-to-end encryption** with perfect forward secrecy, repudiation and break-in recovery.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Messages, files and calls are protected by **quantum resistant e2e encryption** with perfect forward secrecy, repudiation and break-in recovery." xml:space="preserve">
|
||||
<source>Messages, files and calls are protected by **quantum resistant e2e encryption** with perfect forward secrecy, repudiation and break-in recovery.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Migrate device" xml:space="preserve">
|
||||
<source>Migrate device</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Migrate from another device" xml:space="preserve">
|
||||
<source>Migrate from another device</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Migrate here" xml:space="preserve">
|
||||
<source>Migrate here</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Migrate to another device" xml:space="preserve">
|
||||
<source>Migrate to another device</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Migrate to another device via QR code." xml:space="preserve">
|
||||
<source>Migrate to another device via QR code.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Migrating" xml:space="preserve">
|
||||
<source>Migrating</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Migrating database archive…" xml:space="preserve">
|
||||
<source>Migrating database archive…</source>
|
||||
<target>データベースのアーカイブを移行しています…</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Migration complete" xml:space="preserve">
|
||||
<source>Migration complete</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Migration error:" xml:space="preserve">
|
||||
<source>Migration error:</source>
|
||||
<target>移行エラー:</target>
|
||||
@@ -3420,6 +3641,10 @@ This is your link for group %@!</source>
|
||||
<target>ネットワークとサーバ</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Network connection" xml:space="preserve">
|
||||
<source>Network connection</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Network settings" xml:space="preserve">
|
||||
<source>Network settings</source>
|
||||
<target>ネットワーク設定</target>
|
||||
@@ -3529,6 +3754,10 @@ This is your link for group %@!</source>
|
||||
<target>履歴はありません</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="No network connection" xml:space="preserve">
|
||||
<source>No network connection</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="No permission to record voice message" xml:space="preserve">
|
||||
<source>No permission to record voice message</source>
|
||||
<target>音声メッセージを録音する権限がありません</target>
|
||||
@@ -3700,6 +3929,10 @@ This is your link for group %@!</source>
|
||||
<source>Open group</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Open migration to another device" xml:space="preserve">
|
||||
<source>Open migration to another device</source>
|
||||
<note>authentication reason</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Open user profiles" xml:space="preserve">
|
||||
<source>Open user profiles</source>
|
||||
<target>ユーザープロフィールを開く</target>
|
||||
@@ -3714,14 +3947,26 @@ This is your link for group %@!</source>
|
||||
<source>Opening app…</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Or paste archive link" xml:space="preserve">
|
||||
<source>Or paste archive link</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Or scan QR code" xml:space="preserve">
|
||||
<source>Or scan QR code</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Or securely share this file link" xml:space="preserve">
|
||||
<source>Or securely share this file link</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Or show this code" xml:space="preserve">
|
||||
<source>Or show this code</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Other" xml:space="preserve">
|
||||
<source>Other</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="PING count" xml:space="preserve">
|
||||
<source>PING count</source>
|
||||
<target>PING回数</target>
|
||||
@@ -3798,6 +4043,10 @@ This is your link for group %@!</source>
|
||||
<target>永続的な復号化エラー</target>
|
||||
<note>message decrypt error item</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Picture-in-picture calls" xml:space="preserve">
|
||||
<source>Picture-in-picture calls</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</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>音声メッセージを有効にするように連絡相手に要求してください。</target>
|
||||
@@ -3818,6 +4067,10 @@ This is your link for group %@!</source>
|
||||
<target>あなたと連絡先の設定を確認してください。</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Please confirm that network settings are correct for this device." xml:space="preserve">
|
||||
<source>Please confirm that network settings are correct for this device.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Please contact developers. Error: %@" xml:space="preserve">
|
||||
<source>Please contact developers.
|
||||
Error: %@</source>
|
||||
@@ -3873,6 +4126,10 @@ Error: %@</source>
|
||||
<target>サーバアドレスの証明証IDが正しくないかもしれません</target>
|
||||
<note>server test error</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Post-quantum E2EE" xml:space="preserve">
|
||||
<source>Post-quantum E2EE</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Preserve the last message draft, with attachments." xml:space="preserve">
|
||||
<source>Preserve the last message draft, with attachments.</source>
|
||||
<target>添付を含めて、下書きを保存する。</target>
|
||||
@@ -3960,6 +4217,10 @@ Error: %@</source>
|
||||
<target>メッセージへのリアクションは禁止されています。</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Prohibit sending SimpleX links." xml:space="preserve">
|
||||
<source>Prohibit sending SimpleX links.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Prohibit sending direct messages to members." xml:space="preserve">
|
||||
<source>Prohibit sending direct messages to members.</source>
|
||||
<target>メンバー間のダイレクトメッセージを使用禁止にする。</target>
|
||||
@@ -4005,6 +4266,14 @@ Error: %@</source>
|
||||
<target>プッシュ通知</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Push server" xml:space="preserve">
|
||||
<source>Push server</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Quantum resistant encryption" xml:space="preserve">
|
||||
<source>Quantum resistant encryption</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Rate the app" xml:space="preserve">
|
||||
<source>Rate the app</source>
|
||||
<target>アプリを評価</target>
|
||||
@@ -4078,6 +4347,10 @@ Error: %@</source>
|
||||
<target>開発中の機能です!相手のクライアントが4.2でなければ機能しません。アドレス変更が完了すると、会話にメッセージが出ます。連絡相手 (またはグループのメンバー) からメッセージを受信できないかをご確認ください。</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Receiving concurrency" xml:space="preserve">
|
||||
<source>Receiving concurrency</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Receiving file will be stopped." xml:space="preserve">
|
||||
<source>Receiving file will be stopped.</source>
|
||||
<target>ファイルの受信を停止します。</target>
|
||||
@@ -4092,6 +4365,10 @@ Error: %@</source>
|
||||
<source>Recent history and improved [directory bot](simplex:/contact#/?v=1-4&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FeXSPwqTkKyDO3px4fLf1wx3MvPdjdLW3%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAaiv6MkMH44L2TcYrt_CsX3ZvM11WgbMEUn0hkIKTOho%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion).</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Recipient(s) can't see who this message is from." xml:space="preserve">
|
||||
<source>Recipient(s) can't see who this message is from.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Recipients see updates as you type them." xml:space="preserve">
|
||||
<source>Recipients see updates as you type them.</source>
|
||||
<target>受信者には、入力時に更新内容が表示されます。</target>
|
||||
@@ -4186,10 +4463,22 @@ Error: %@</source>
|
||||
<source>Repeat connection request?</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Repeat download" xml:space="preserve">
|
||||
<source>Repeat download</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Repeat import" xml:space="preserve">
|
||||
<source>Repeat import</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Repeat join request?" xml:space="preserve">
|
||||
<source>Repeat join request?</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Repeat upload" xml:space="preserve">
|
||||
<source>Repeat upload</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Reply" xml:space="preserve">
|
||||
<source>Reply</source>
|
||||
<target>返信</target>
|
||||
@@ -4289,6 +4578,10 @@ Error: %@</source>
|
||||
<target>SMPサーバ</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Safer groups" xml:space="preserve">
|
||||
<source>Safer groups</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Save" xml:space="preserve">
|
||||
<source>Save</source>
|
||||
<target>保存</target>
|
||||
@@ -4369,11 +4662,19 @@ Error: %@</source>
|
||||
<target>ウェルカムメッセージを保存しますか?</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Saved" xml:space="preserve">
|
||||
<source>Saved</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Saved WebRTC ICE servers will be removed" xml:space="preserve">
|
||||
<source>Saved WebRTC ICE servers will be removed</source>
|
||||
<target>保存されたWebRTC ICEサーバは削除されます</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Saved from" xml:space="preserve">
|
||||
<source>Saved from</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Saved message" xml:space="preserve">
|
||||
<source>Saved message</source>
|
||||
<note>message info title</note>
|
||||
@@ -4640,6 +4941,10 @@ Error: %@</source>
|
||||
<target>パスコードを設定する</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Set passphrase" xml:space="preserve">
|
||||
<source>Set passphrase</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Set passphrase to export" xml:space="preserve">
|
||||
<source>Set passphrase to export</source>
|
||||
<target>暗証フレーズを設定してからエクスポート</target>
|
||||
@@ -4694,6 +4999,10 @@ Error: %@</source>
|
||||
<target>連絡先と共有する</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Show QR code" xml:space="preserve">
|
||||
<source>Show QR code</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Show calls in phone history" xml:space="preserve">
|
||||
<source>Show calls in phone history</source>
|
||||
<target>通話履歴を表示</target>
|
||||
@@ -4772,6 +5081,14 @@ Error: %@</source>
|
||||
<trans-unit id="SimpleX links" xml:space="preserve">
|
||||
<source>SimpleX links</source>
|
||||
<target>SimpleXリンク</target>
|
||||
<note>chat feature</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="SimpleX links are prohibited in this group." xml:space="preserve">
|
||||
<source>SimpleX links are prohibited in this group.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="SimpleX links not allowed" xml:space="preserve">
|
||||
<source>SimpleX links not allowed</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="SimpleX one-time invitation" xml:space="preserve">
|
||||
@@ -4833,6 +5150,10 @@ Error: %@</source>
|
||||
<target>SimpleX を停止</target>
|
||||
<note>authentication reason</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Stop chat" xml:space="preserve">
|
||||
<source>Stop chat</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Stop chat to enable database actions" xml:space="preserve">
|
||||
<source>Stop chat to enable database actions</source>
|
||||
<target>チャットを停止してデータベースアクションを有効にします</target>
|
||||
@@ -4873,6 +5194,10 @@ Error: %@</source>
|
||||
<target>アドレスの共有を停止しますか?</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Stopping chat" xml:space="preserve">
|
||||
<source>Stopping chat</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Submit" xml:space="preserve">
|
||||
<source>Submit</source>
|
||||
<target>送信</target>
|
||||
@@ -5115,6 +5440,14 @@ It can happen because of some bug or when the connection is compromised.</source
|
||||
<target>あなたのプロフィール、連絡先、メッセージ、ファイルが完全削除されます (※元に戻せません※)。</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="This chat is protected by end-to-end encryption." xml:space="preserve">
|
||||
<source>This chat is protected by end-to-end encryption.</source>
|
||||
<note>E2EE info chat item</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="This chat is protected by quantum resistant end-to-end encryption." xml:space="preserve">
|
||||
<source>This chat is protected by quantum resistant end-to-end encryption.</source>
|
||||
<note>E2EE info chat item</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="This device name" xml:space="preserve">
|
||||
<source>This device name</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
@@ -5393,11 +5726,19 @@ To connect, please ask your contact to create another connection link and check
|
||||
<target>アップグレードしてチャットを開く</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Upload failed" xml:space="preserve">
|
||||
<source>Upload failed</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Upload file" xml:space="preserve">
|
||||
<source>Upload file</source>
|
||||
<target>ファイルをアップロードする</target>
|
||||
<note>server test step</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Uploading archive" xml:space="preserve">
|
||||
<source>Uploading archive</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Use .onion hosts" xml:space="preserve">
|
||||
<source>Use .onion hosts</source>
|
||||
<target>.onionホストを使う</target>
|
||||
@@ -5446,6 +5787,10 @@ To connect, please ask your contact to create another connection link and check
|
||||
<target>サーバを使う</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Use the app while in the call." xml:space="preserve">
|
||||
<source>Use the app while in the call.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="User profile" xml:space="preserve">
|
||||
<source>User profile</source>
|
||||
<target>ユーザープロフィール</target>
|
||||
@@ -5478,6 +5823,14 @@ To connect, please ask your contact to create another connection link and check
|
||||
<source>Verify connections</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Verify database passphrase" xml:space="preserve">
|
||||
<source>Verify database passphrase</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Verify passphrase" xml:space="preserve">
|
||||
<source>Verify passphrase</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Verify security code" xml:space="preserve">
|
||||
<source>Verify security code</source>
|
||||
<target>セキュリティコードを確認</target>
|
||||
@@ -5536,6 +5889,10 @@ To connect, please ask your contact to create another connection link and check
|
||||
<target>このグループでは音声メッセージが使用禁止です。</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Voice messages not allowed" xml:space="preserve">
|
||||
<source>Voice messages not allowed</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Voice messages prohibited!" xml:space="preserve">
|
||||
<source>Voice messages prohibited!</source>
|
||||
<target>音声メッセージは使用禁止です!</target>
|
||||
@@ -5565,6 +5922,10 @@ To connect, please ask your contact to create another connection link and check
|
||||
<target>ビデオ待機中</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Warning: starting chat on multiple devices is not supported and will cause message delivery failures" xml:space="preserve">
|
||||
<source>Warning: starting chat on multiple devices is not supported and will cause message delivery failures</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Warning: you may lose some data!" xml:space="preserve">
|
||||
<source>Warning: you may lose some data!</source>
|
||||
<target>警告: 一部のデータが失われる可能性があります!</target>
|
||||
@@ -5585,6 +5946,10 @@ To connect, please ask your contact to create another connection link and check
|
||||
<target>ウェルカムメッセージ</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Welcome message is too long" xml:space="preserve">
|
||||
<source>Welcome message is too long</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="What's new" xml:space="preserve">
|
||||
<source>What's new</source>
|
||||
<target>新着情報</target>
|
||||
@@ -5605,6 +5970,14 @@ To connect, please ask your contact to create another connection link and check
|
||||
<target>連絡相手にシークレットモードのプロフィールを共有すると、その連絡相手に招待されたグループでも同じプロフィールが使われます。</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="WiFi" xml:space="preserve">
|
||||
<source>WiFi</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Wired ethernet" xml:space="preserve">
|
||||
<source>Wired ethernet</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="With encrypted files and media." xml:space="preserve">
|
||||
<source>With encrypted files and media.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
@@ -5638,6 +6011,10 @@ To connect, please ask your contact to create another connection link and check
|
||||
<target>あなた</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="You **must not** use the same database on two devices." xml:space="preserve">
|
||||
<source>You **must not** use the same database on two devices.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="You accepted connection" xml:space="preserve">
|
||||
<source>You accepted connection</source>
|
||||
<target>接続を承認しました</target>
|
||||
@@ -5717,6 +6094,10 @@ Repeat join request?</source>
|
||||
<target>あとでアプリのプライバシーとセキュリティの設定から有効にすることができます。</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="You can give another try." xml:space="preserve">
|
||||
<source>You can give another try.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="You can hide or mute a user profile - swipe it to the right." xml:space="preserve">
|
||||
<source>You can hide or mute a user profile - swipe it to the right.</source>
|
||||
<target>ユーザープロファイルを右にスワイプすると、非表示またはミュートにすることができます。</target>
|
||||
@@ -6060,6 +6441,10 @@ SimpleX サーバーはあなたのプロファイルを参照できません。
|
||||
<target>管理者</target>
|
||||
<note>member role</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="admins" xml:space="preserve">
|
||||
<source>admins</source>
|
||||
<note>feature role</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="agreeing encryption for %@…" xml:space="preserve">
|
||||
<source>agreeing encryption for %@…</source>
|
||||
<target>%@の暗号化に同意しています…</target>
|
||||
@@ -6070,6 +6455,10 @@ SimpleX サーバーはあなたのプロファイルを参照できません。
|
||||
<target>暗号化に同意しています…</target>
|
||||
<note>chat item text</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="all members" xml:space="preserve">
|
||||
<source>all members</source>
|
||||
<note>feature role</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="always" xml:space="preserve">
|
||||
<source>always</source>
|
||||
<target>常に</target>
|
||||
@@ -6100,7 +6489,7 @@ SimpleX サーバーはあなたのプロファイルを参照できません。
|
||||
</trans-unit>
|
||||
<trans-unit id="blocked" xml:space="preserve">
|
||||
<source>blocked</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
<note>marked deleted chat item preview text</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="blocked %@" xml:space="preserve">
|
||||
<source>blocked %@</source>
|
||||
@@ -6108,7 +6497,7 @@ SimpleX サーバーはあなたのプロファイルを参照できません。
|
||||
</trans-unit>
|
||||
<trans-unit id="blocked by admin" xml:space="preserve">
|
||||
<source>blocked by admin</source>
|
||||
<note>blocked chat item</note>
|
||||
<note>marked deleted chat item preview text</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="bold" xml:space="preserve">
|
||||
<source>bold</source>
|
||||
@@ -6392,6 +6781,10 @@ SimpleX サーバーはあなたのプロファイルを参照できません。
|
||||
<target>イベント発生</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="forwarded" xml:space="preserve">
|
||||
<source>forwarded</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="group deleted" xml:space="preserve">
|
||||
<source>group deleted</source>
|
||||
<target>グループ削除済み</target>
|
||||
@@ -6534,7 +6927,7 @@ SimpleX サーバーはあなたのプロファイルを参照できません。
|
||||
<trans-unit id="moderated by %@" xml:space="preserve">
|
||||
<source>moderated by %@</source>
|
||||
<target>%@ によってモデレートされた</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
<note>marked deleted chat item preview text</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="months" xml:space="preserve">
|
||||
<source>months</source>
|
||||
@@ -6598,11 +6991,19 @@ SimpleX サーバーはあなたのプロファイルを参照できません。
|
||||
<target>オーナー</target>
|
||||
<note>member role</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="owners" xml:space="preserve">
|
||||
<source>owners</source>
|
||||
<note>feature role</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="peer-to-peer" xml:space="preserve">
|
||||
<source>peer-to-peer</source>
|
||||
<target>P2P</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="quantum resistant e2e encryption" xml:space="preserve">
|
||||
<source>quantum resistant e2e encryption</source>
|
||||
<note>chat item text</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="received answer…" xml:space="preserve">
|
||||
<source>received answer…</source>
|
||||
<target>回答を受け取りました…</target>
|
||||
@@ -6641,6 +7042,14 @@ SimpleX サーバーはあなたのプロファイルを参照できません。
|
||||
<target>あなたを除名しました</target>
|
||||
<note>rcv group event chat item</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="saved" xml:space="preserve">
|
||||
<source>saved</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="saved from %@" xml:space="preserve">
|
||||
<source>saved from %@</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="sec" xml:space="preserve">
|
||||
<source>sec</source>
|
||||
<target>秒</target>
|
||||
@@ -6673,6 +7082,10 @@ SimpleX サーバーはあなたのプロファイルを参照できません。
|
||||
<source>set new profile picture</source>
|
||||
<note>profile update event chat item</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="standard end-to-end encryption" xml:space="preserve">
|
||||
<source>standard end-to-end encryption</source>
|
||||
<note>chat item text</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="starting…" xml:space="preserve">
|
||||
<source>starting…</source>
|
||||
<target>接続中…</target>
|
||||
@@ -6769,6 +7182,10 @@ SimpleX サーバーはあなたのプロファイルを参照できません。
|
||||
<target>はい</target>
|
||||
<note>pref value</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="you" xml:space="preserve">
|
||||
<source>you</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="you are invited to group" xml:space="preserve">
|
||||
<source>you are invited to group</source>
|
||||
<target>グループ招待が届きました</target>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -69,7 +69,7 @@
|
||||
</trans-unit>
|
||||
<trans-unit id="%@ is not verified" xml:space="preserve" approved="no">
|
||||
<source>%@ is not verified</source>
|
||||
<target state="translated">%@ não foi verificado</target>
|
||||
<target state="translated">%@ não foi verificado(a)</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="%@ is verified" xml:space="preserve" approved="no">
|
||||
@@ -84,12 +84,12 @@
|
||||
</trans-unit>
|
||||
<trans-unit id="%d days" xml:space="preserve" approved="no">
|
||||
<source>%d days</source>
|
||||
<target state="translated">%d dia(s)</target>
|
||||
<target state="translated">%d dias</target>
|
||||
<note>message ttl</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="%d hours" xml:space="preserve" approved="no">
|
||||
<source>%d hours</source>
|
||||
<target state="translated">%d hora(s)</target>
|
||||
<target state="translated">%d horas</target>
|
||||
<note>message ttl</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="%d min" xml:space="preserve" approved="no">
|
||||
@@ -99,7 +99,7 @@
|
||||
</trans-unit>
|
||||
<trans-unit id="%d months" xml:space="preserve" approved="no">
|
||||
<source>%d months</source>
|
||||
<target state="translated">%d mês(es)</target>
|
||||
<target state="translated">%d meses</target>
|
||||
<note>message ttl</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="%d sec" xml:space="preserve" approved="no">
|
||||
@@ -109,7 +109,7 @@
|
||||
</trans-unit>
|
||||
<trans-unit id="%d skipped message(s)" xml:space="preserve" approved="no">
|
||||
<source>%d skipped message(s)</source>
|
||||
<target state="translated">%d mensagem(ns) ignorada(s)</target>
|
||||
<target state="translated">%d mensagem(ns) omitida(s)</target>
|
||||
<note>integrity error chat item</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="%lld" xml:space="preserve" approved="no">
|
||||
@@ -5189,6 +5189,51 @@ Isso pode acontecer por causa de algum bug ou quando a conexão está comprometi
|
||||
<target state="translated">Desaparecerá: %@</target>
|
||||
<note>copied message info</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="# %@" xml:space="preserve" approved="no">
|
||||
<source># %@</source>
|
||||
<target state="translated"># %@</target>
|
||||
<note>copied message info title, # <title></note>
|
||||
</trans-unit>
|
||||
<trans-unit id="## History" xml:space="preserve" approved="no">
|
||||
<source>## History</source>
|
||||
<target state="translated">## Histórico</target>
|
||||
<note>copied message info</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="## In reply to" xml:space="preserve" approved="no">
|
||||
<source>## In reply to</source>
|
||||
<target state="translated">## Em resposta a</target>
|
||||
<note>copied message info</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="%@ connected" xml:space="preserve" approved="no">
|
||||
<source>%@ connected</source>
|
||||
<target state="translated">%@ conectado(a)</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="%@, %@ and %lld members" xml:space="preserve" approved="no">
|
||||
<source>%@, %@ and %lld members</source>
|
||||
<target state="translated">%@, %@ e %lld membros</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="%@ and %@ connected" xml:space="preserve" approved="no">
|
||||
<source>%@ and %@ connected</source>
|
||||
<target state="translated">%@ e %@ conectados</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="%@, %@ and %lld other members connected" xml:space="preserve" approved="no">
|
||||
<source>%@, %@ and %lld other members connected</source>
|
||||
<target state="translated">%@, %@ e %lld e outros membros se conectaram</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="%@ and %@" xml:space="preserve" approved="no">
|
||||
<source>%@ and %@</source>
|
||||
<target state="translated">%@ e %@</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="%@ at %@:" xml:space="preserve" approved="no">
|
||||
<source>%1$@ at %2$@:</source>
|
||||
<target state="translated">%1$@ em %2$@:</target>
|
||||
<note>copied message info, <sender> at <time></note>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
<file original="en.lproj/SimpleX--iOS--InfoPlist.strings" source-language="en" target-language="pt-BR" datatype="plaintext">
|
||||
|
||||
@@ -15,32 +15,39 @@
|
||||
Available in v5.1</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id=" " xml:space="preserve">
|
||||
<trans-unit id=" " xml:space="preserve" approved="no">
|
||||
<source> </source>
|
||||
<target state="translated"> </target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id=" " xml:space="preserve">
|
||||
<trans-unit id=" " xml:space="preserve" approved="no">
|
||||
<source> </source>
|
||||
<target state="translated"> </target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id=" " xml:space="preserve">
|
||||
<trans-unit id=" " xml:space="preserve" approved="no">
|
||||
<source> </source>
|
||||
<target state="translated"> </target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id=" (" xml:space="preserve">
|
||||
<trans-unit id=" (" xml:space="preserve" approved="no">
|
||||
<source> (</source>
|
||||
<target state="translated"> (</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id=" (can be copied)" xml:space="preserve">
|
||||
<trans-unit id=" (can be copied)" xml:space="preserve" approved="no">
|
||||
<source> (can be copied)</source>
|
||||
<target state="translated"> .(pode ser copiado)</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="!1 colored!" xml:space="preserve">
|
||||
<trans-unit id="!1 colored!" xml:space="preserve" approved="no">
|
||||
<source>!1 colored!</source>
|
||||
<target state="translated">!1 colorido!</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="#secret#" xml:space="preserve">
|
||||
<trans-unit id="#secret#" xml:space="preserve" approved="no">
|
||||
<source>#secret#</source>
|
||||
<target state="translated">#secreto#</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="%@" xml:space="preserve">
|
||||
@@ -55,48 +62,59 @@ Available in v5.1</source>
|
||||
<source>%@ / %@</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="%@ is connected!" xml:space="preserve">
|
||||
<trans-unit id="%@ is connected!" xml:space="preserve" approved="no">
|
||||
<source>%@ is connected!</source>
|
||||
<target state="translated">%@ está conectado!</target>
|
||||
<note>notification title</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="%@ is not verified" xml:space="preserve">
|
||||
<trans-unit id="%@ is not verified" xml:space="preserve" approved="no">
|
||||
<source>%@ is not verified</source>
|
||||
<target state="translated">%@ não foi verificado</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="%@ is verified" xml:space="preserve">
|
||||
<trans-unit id="%@ is verified" xml:space="preserve" approved="no">
|
||||
<source>%@ is verified</source>
|
||||
<target state="translated">%@ foi verificado</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="%@ servers" xml:space="preserve">
|
||||
<trans-unit id="%@ servers" xml:space="preserve" approved="no">
|
||||
<source>%@ servers</source>
|
||||
<target state="translated">%@ servidores</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="%@ wants to connect!" xml:space="preserve">
|
||||
<trans-unit id="%@ wants to connect!" xml:space="preserve" approved="no">
|
||||
<source>%@ wants to connect!</source>
|
||||
<target state="translated">%@ quer se conectar!</target>
|
||||
<note>notification title</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="%d days" xml:space="preserve">
|
||||
<trans-unit id="%d days" xml:space="preserve" approved="no">
|
||||
<source>%d days</source>
|
||||
<target state="translated">%d dias</target>
|
||||
<note>message ttl</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="%d hours" xml:space="preserve">
|
||||
<trans-unit id="%d hours" xml:space="preserve" approved="no">
|
||||
<source>%d hours</source>
|
||||
<target state="translated">%d horas</target>
|
||||
<note>message ttl</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="%d min" xml:space="preserve">
|
||||
<trans-unit id="%d min" xml:space="preserve" approved="no">
|
||||
<source>%d min</source>
|
||||
<target state="translated">%d minuto</target>
|
||||
<note>message ttl</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="%d months" xml:space="preserve">
|
||||
<trans-unit id="%d months" xml:space="preserve" approved="no">
|
||||
<source>%d months</source>
|
||||
<target state="translated">%d meses</target>
|
||||
<note>message ttl</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="%d sec" xml:space="preserve">
|
||||
<trans-unit id="%d sec" xml:space="preserve" approved="no">
|
||||
<source>%d sec</source>
|
||||
<target state="translated">%d segundo</target>
|
||||
<note>message ttl</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="%d skipped message(s)" xml:space="preserve">
|
||||
<trans-unit id="%d skipped message(s)" xml:space="preserve" approved="no">
|
||||
<source>%d skipped message(s)</source>
|
||||
<target state="translated">%d mensagem(s) ignorada(s)</target>
|
||||
<note>integrity error chat item</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="%lld" xml:space="preserve">
|
||||
@@ -107,28 +125,34 @@ Available in v5.1</source>
|
||||
<source>%lld %@</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="%lld contact(s) selected" xml:space="preserve">
|
||||
<trans-unit id="%lld contact(s) selected" xml:space="preserve" approved="no">
|
||||
<source>%lld contact(s) selected</source>
|
||||
<target state="translated">%lld contato(s) selecionado(s)</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="%lld file(s) with total size of %@" xml:space="preserve">
|
||||
<trans-unit id="%lld file(s) with total size of %@" xml:space="preserve" approved="no">
|
||||
<source>%lld file(s) with total size of %@</source>
|
||||
<target state="translated">%lld arquivo(s) com tamanho total de %@</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="%lld members" xml:space="preserve">
|
||||
<trans-unit id="%lld members" xml:space="preserve" approved="no">
|
||||
<source>%lld members</source>
|
||||
<target state="translated">%lld membros</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="%lld minutes" xml:space="preserve">
|
||||
<trans-unit id="%lld minutes" xml:space="preserve" approved="no">
|
||||
<source>%lld minutes</source>
|
||||
<target state="translated">%lld minutos</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="%lld second(s)" xml:space="preserve">
|
||||
<trans-unit id="%lld second(s)" xml:space="preserve" approved="no">
|
||||
<source>%lld second(s)</source>
|
||||
<target state="translated">%lld segundo(s)</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="%lld seconds" xml:space="preserve">
|
||||
<trans-unit id="%lld seconds" xml:space="preserve" approved="no">
|
||||
<source>%lld seconds</source>
|
||||
<target state="translated">%lld segundos</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="%lldd" xml:space="preserve">
|
||||
@@ -159,12 +183,14 @@ Available in v5.1</source>
|
||||
<source>%lldw</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="%u messages failed to decrypt." xml:space="preserve">
|
||||
<trans-unit id="%u messages failed to decrypt." xml:space="preserve" approved="no">
|
||||
<source>%u messages failed to decrypt.</source>
|
||||
<target state="translated">%u mensagens não foram descriptografadas.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="%u messages skipped." xml:space="preserve">
|
||||
<trans-unit id="%u messages skipped." xml:space="preserve" approved="no">
|
||||
<source>%u messages skipped.</source>
|
||||
<target state="translated">%u mensagens ignoradas.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="(" xml:space="preserve">
|
||||
@@ -175,48 +201,56 @@ Available in v5.1</source>
|
||||
<source>)</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="**Add new contact**: to create your one-time QR Code for your contact." xml:space="preserve">
|
||||
<trans-unit id="**Add new contact**: to create your one-time QR Code for your contact." xml:space="preserve" approved="no">
|
||||
<source>**Add new contact**: to create your one-time QR Code or link for your contact.</source>
|
||||
<target state="translated">**Adicionar novo contato**: para criar seu QR Code único ou link para seu contato.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</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>
|
||||
<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">
|
||||
<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" approved="no">
|
||||
<source>**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.</source>
|
||||
<target state="translated">**Mais privado**: verifique novas mensagens a cada 20 minutos. O token do dispositivo é compartilhado com o servidor SimpleX Chat, mas não com quantos contatos ou mensagens você possui.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="**Most private**: do not use SimpleX Chat notifications server, check messages periodically in the background (depends on how often you use the app)." xml:space="preserve">
|
||||
<trans-unit id="**Most private**: do not use SimpleX Chat notifications server, check messages periodically in the background (depends on how often you use the app)." xml:space="preserve" approved="no">
|
||||
<source>**Most private**: do not use SimpleX Chat notifications server, check messages periodically in the background (depends on how often you use the app).</source>
|
||||
<target state="translated">**Totalmente privado**: não use o servidor de notificações do SimpleX Chat, verifique as mensagens periodicamente em segundo plano (depende da frequência com que você usa o aplicativo).</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="**Paste received link** or open it in the browser and tap **Open in mobile app**." xml:space="preserve">
|
||||
<source>**Paste received link** or open it in the browser and tap **Open in mobile app**.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="**Please note**: you will NOT be able to recover or change passphrase if you lose it." xml:space="preserve">
|
||||
<trans-unit id="**Please note**: you will NOT be able to recover or change passphrase if you lose it." xml:space="preserve" approved="no">
|
||||
<source>**Please note**: you will NOT be able to recover or change passphrase if you lose it.</source>
|
||||
<target state="translated">**Atenção**: Você NÃO poderá recuperar ou alterar a senha caso a perca.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="**Recommended**: device token and notifications are sent to SimpleX Chat notification server, but not the message content, size or who it is from." xml:space="preserve">
|
||||
<trans-unit id="**Recommended**: device token and notifications are sent to SimpleX Chat notification server, but not the message content, size or who it is from." xml:space="preserve" approved="no">
|
||||
<source>**Recommended**: device token and notifications are sent to SimpleX Chat notification server, but not the message content, size or who it is from.</source>
|
||||
<target state="translated">**Recomendado**: O token do dispositivo e as notificações são enviados ao servidor de notificação do SimpleX Chat, mas não o conteúdo, o tamanho da mensagem ou de quem ela é.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</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>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="**Warning**: Instant push notifications require passphrase saved in Keychain." xml:space="preserve">
|
||||
<trans-unit id="**Warning**: Instant push notifications require passphrase saved in Keychain." xml:space="preserve" approved="no">
|
||||
<source>**Warning**: Instant push notifications require passphrase saved in Keychain.</source>
|
||||
<target state="translated">**Aviso**: As notificações push instantâneas exigem uma senha salva nas Chaves.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="**e2e encrypted** audio call" xml:space="preserve">
|
||||
<trans-unit id="**e2e encrypted** audio call" xml:space="preserve" approved="no">
|
||||
<source>**e2e encrypted** audio call</source>
|
||||
<target state="translated">** Criptografado e2e** chamada de áudio</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="**e2e encrypted** video call" xml:space="preserve">
|
||||
<trans-unit id="**e2e encrypted** video call" xml:space="preserve" approved="no">
|
||||
<source>**e2e encrypted** video call</source>
|
||||
<target state="translated">**Criptografado e2e** chamada de vídeo</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="*bold*" xml:space="preserve">
|
||||
@@ -353,144 +387,175 @@ Available in v5.1</source>
|
||||
<source>All group members will remain connected.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="All messages will be deleted - this cannot be undone! The messages will be deleted ONLY for you." xml:space="preserve">
|
||||
<trans-unit id="All messages will be deleted - this cannot be undone! The messages will be deleted ONLY for you." xml:space="preserve" approved="no">
|
||||
<source>All messages will be deleted - this cannot be undone! The messages will be deleted ONLY for you.</source>
|
||||
<target state="translated">Todas as mensagens serão apagadas – isso não pode ser desfeito! As mensagens serão apagadas SOMENTE para você.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="All your contacts will remain connected" xml:space="preserve">
|
||||
<source>All your contacts will remain connected</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Allow" xml:space="preserve">
|
||||
<trans-unit id="Allow" xml:space="preserve" approved="no">
|
||||
<source>Allow</source>
|
||||
<target state="translated">Permitir</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Allow calls only if your contact allows them." xml:space="preserve">
|
||||
<trans-unit id="Allow calls only if your contact allows them." xml:space="preserve" approved="no">
|
||||
<source>Allow calls only if your contact allows them.</source>
|
||||
<target state="translated">Permita chamadas somente se seu contato permitir.</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">
|
||||
<trans-unit id="Allow disappearing messages only if your contact allows it to you." xml:space="preserve" approved="no">
|
||||
<source>Allow disappearing messages only if your contact allows it to you.</source>
|
||||
<target state="translated">Permita o desaparecimento de mensagens somente se o seu contato permitir.</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>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Allow sending direct messages to members." xml:space="preserve">
|
||||
<trans-unit id="Allow sending direct messages to members." xml:space="preserve" approved="no">
|
||||
<source>Allow sending direct messages to members.</source>
|
||||
<target state="translated">Permitir o envio de mensagens diretas aos membros.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Allow sending disappearing messages." xml:space="preserve">
|
||||
<trans-unit id="Allow sending disappearing messages." xml:space="preserve" approved="no">
|
||||
<source>Allow sending disappearing messages.</source>
|
||||
<target state="translated">Permitir o envio de mensagens que desaparecem.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Allow to irreversibly delete sent messages." xml:space="preserve">
|
||||
<source>Allow to irreversibly delete sent messages.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Allow to send voice messages." xml:space="preserve">
|
||||
<trans-unit id="Allow to send voice messages." xml:space="preserve" approved="no">
|
||||
<source>Allow to send voice messages.</source>
|
||||
<target state="translated">Permitir enviar mensagens de voz.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Allow voice messages only if your contact allows them." xml:space="preserve">
|
||||
<trans-unit id="Allow voice messages only if your contact allows them." xml:space="preserve" approved="no">
|
||||
<source>Allow voice messages only if your contact allows them.</source>
|
||||
<target state="translated">Permita mensagens de voz apenas se o seu contato permitir.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Allow voice messages?" xml:space="preserve">
|
||||
<trans-unit id="Allow voice messages?" xml:space="preserve" approved="no">
|
||||
<source>Allow voice messages?</source>
|
||||
<target state="translated">Permitir mensagens de voz?</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Allow your contacts to call you." xml:space="preserve">
|
||||
<trans-unit id="Allow your contacts to call you." xml:space="preserve" approved="no">
|
||||
<source>Allow your contacts to call you.</source>
|
||||
<target state="translated">Permita que seus contatos liguem para você.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Allow your contacts to irreversibly delete sent messages." xml:space="preserve">
|
||||
<source>Allow your contacts to irreversibly delete sent messages.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Allow your contacts to send disappearing messages." xml:space="preserve">
|
||||
<trans-unit id="Allow your contacts to send disappearing messages." xml:space="preserve" approved="no">
|
||||
<source>Allow your contacts to send disappearing messages.</source>
|
||||
<target state="translated">Permita que seus contatos enviem mensagens que desaparecem.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Allow your contacts to send voice messages." xml:space="preserve">
|
||||
<trans-unit id="Allow your contacts to send voice messages." xml:space="preserve" approved="no">
|
||||
<source>Allow your contacts to send voice messages.</source>
|
||||
<target state="translated">Permita que seus contatos enviem mensagens de voz.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Already connected?" xml:space="preserve">
|
||||
<trans-unit id="Already connected?" xml:space="preserve" approved="no">
|
||||
<source>Already connected?</source>
|
||||
<target state="translated">Já conectado?</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Always use relay" xml:space="preserve">
|
||||
<trans-unit id="Always use relay" xml:space="preserve" approved="no">
|
||||
<source>Always use relay</source>
|
||||
<target state="translated">Sempre usar retransmissão</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Answer call" xml:space="preserve">
|
||||
<trans-unit id="Answer call" xml:space="preserve" approved="no">
|
||||
<source>Answer call</source>
|
||||
<target state="translated">Atender chamada</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="App build: %@" xml:space="preserve">
|
||||
<trans-unit id="App build: %@" xml:space="preserve" approved="no">
|
||||
<source>App build: %@</source>
|
||||
<target state="translated">Versão do Aplicativo: %@</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="App icon" xml:space="preserve">
|
||||
<trans-unit id="App icon" xml:space="preserve" approved="no">
|
||||
<source>App icon</source>
|
||||
<target state="translated">Ícone do Aplicativo</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="App passcode" xml:space="preserve">
|
||||
<trans-unit id="App passcode" xml:space="preserve" approved="no">
|
||||
<source>App passcode</source>
|
||||
<target state="translated">Senha do Aplicativo</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="App version" xml:space="preserve">
|
||||
<trans-unit id="App version" xml:space="preserve" approved="no">
|
||||
<source>App version</source>
|
||||
<target state="translated">Versão do Aplicativo</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="App version: v%@" xml:space="preserve">
|
||||
<trans-unit id="App version: v%@" xml:space="preserve" approved="no">
|
||||
<source>App version: v%@</source>
|
||||
<target state="translated">Versão do Aplicativo: v%@</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Appearance" xml:space="preserve">
|
||||
<trans-unit id="Appearance" xml:space="preserve" approved="no">
|
||||
<source>Appearance</source>
|
||||
<target state="translated">Aparência</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Attach" xml:space="preserve">
|
||||
<trans-unit id="Attach" xml:space="preserve" approved="no">
|
||||
<source>Attach</source>
|
||||
<target state="translated">Anexar</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Audio & video calls" xml:space="preserve">
|
||||
<trans-unit id="Audio & video calls" xml:space="preserve" approved="no">
|
||||
<source>Audio & video calls</source>
|
||||
<target state="translated">Chamadas de áudio e vídeo</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Audio and video calls" xml:space="preserve">
|
||||
<trans-unit id="Audio and video calls" xml:space="preserve" approved="no">
|
||||
<source>Audio and video calls</source>
|
||||
<target state="translated">Chamadas de áudio e vídeo</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Audio/video calls" xml:space="preserve">
|
||||
<trans-unit id="Audio/video calls" xml:space="preserve" approved="no">
|
||||
<source>Audio/video calls</source>
|
||||
<target state="translated">Chamadas de áudio/vídeo</target>
|
||||
<note>chat feature</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Audio/video calls are prohibited." xml:space="preserve">
|
||||
<trans-unit id="Audio/video calls are prohibited." xml:space="preserve" approved="no">
|
||||
<source>Audio/video calls are prohibited.</source>
|
||||
<target state="translated">Chamadas de áudio/vídeo são proibidas.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Authentication cancelled" xml:space="preserve">
|
||||
<trans-unit id="Authentication cancelled" xml:space="preserve" approved="no">
|
||||
<source>Authentication cancelled</source>
|
||||
<target state="translated">Autenticação cancelada</target>
|
||||
<note>PIN entry</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Authentication failed" xml:space="preserve">
|
||||
<trans-unit id="Authentication failed" xml:space="preserve" approved="no">
|
||||
<source>Authentication failed</source>
|
||||
<target state="translated">Falha na autenticação</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Authentication is required before the call is connected, but you may miss calls." xml:space="preserve">
|
||||
<trans-unit id="Authentication is required before the call is connected, but you may miss calls." xml:space="preserve" approved="no">
|
||||
<source>Authentication is required before the call is connected, but you may miss calls.</source>
|
||||
<target state="translated">A autenticação é necessária antes que a chamada seja conectada, mas você pode perder chamadas.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Authentication unavailable" xml:space="preserve">
|
||||
<trans-unit id="Authentication unavailable" xml:space="preserve" approved="no">
|
||||
<source>Authentication unavailable</source>
|
||||
<target state="translated">Autenticação indisponível</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Auto-accept contact requests" xml:space="preserve">
|
||||
<trans-unit id="Auto-accept contact requests" xml:space="preserve" approved="no">
|
||||
<source>Auto-accept contact requests</source>
|
||||
<target state="translated">Aceitar solicitações de contato automaticamente</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Auto-accept images" xml:space="preserve">
|
||||
@@ -4162,6 +4227,226 @@ SimpleX servers cannot see your profile.</source>
|
||||
<source>\~strike~</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="## In reply to" xml:space="preserve" approved="no">
|
||||
<source>## In reply to</source>
|
||||
<target state="translated">## Em resposta a</target>
|
||||
<note>copied message info</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="%@ uploaded" xml:space="preserve" approved="no">
|
||||
<source>%@ uploaded</source>
|
||||
<target state="translated">%@ enviado</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="%d weeks" xml:space="preserve" approved="no">
|
||||
<source>%d weeks</source>
|
||||
<target state="translated">%d semanas</target>
|
||||
<note>time interval</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="%lld messages blocked by admin" xml:space="preserve" approved="no">
|
||||
<source>%lld messages blocked by admin</source>
|
||||
<target state="translated">%lld mensagens bloqueadas pelo administrador</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="%lld new interface languages" xml:space="preserve" approved="no">
|
||||
<source>%lld new interface languages</source>
|
||||
<target state="translated">%lld novas interface de idiomas</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="**Add contact**: to create a new invitation link, or connect via a link you received." xml:space="preserve" approved="no">
|
||||
<source>**Add contact**: to create a new invitation link, or connect via a link you received.</source>
|
||||
<target state="translated">**Adicionar contato**: para criar um novo link de convite ou conectar-se por meio de um link que você recebeu.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="**Create group**: to create a new group." xml:space="preserve" approved="no">
|
||||
<source>**Create group**: to create a new group.</source>
|
||||
<target state="translated">**Criar grupo**: para criar um novo grupo.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="**Please note**: using the same database on two devices will break the decryption of messages from your connections, as a security protection." xml:space="preserve" approved="no">
|
||||
<source>**Please note**: using the same database on two devices will break the decryption of messages from your connections, as a security protection.</source>
|
||||
<target state="translated">**Observação**: usar o mesmo banco de dados em dois dispositivos interromperá a descriptografia de mensagens de suas conexões, como proteção de segurança.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="## History" xml:space="preserve" approved="no">
|
||||
<source>## History</source>
|
||||
<target state="translated">## Histórico</target>
|
||||
<note>copied message info</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="**Warning**: the archive will be removed." xml:space="preserve" approved="no">
|
||||
<source>**Warning**: the archive will be removed.</source>
|
||||
<target state="translated">**Atenção**: O arquivo será removido.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="%@ and %@" xml:space="preserve" approved="no">
|
||||
<source>%@ and %@</source>
|
||||
<target state="translated">%@ e %@</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="%@ and %@ connected" xml:space="preserve" approved="no">
|
||||
<source>%@ and %@ connected</source>
|
||||
<target state="translated">%@ e %@ conectado</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="%@ at %@:" xml:space="preserve" approved="no">
|
||||
<source>%1$@ at %2$@:</source>
|
||||
<target state="translated">%1$@ em %2$@:</target>
|
||||
<note>copied message info, <sender> at <time></note>
|
||||
</trans-unit>
|
||||
<trans-unit id="%@ connected" xml:space="preserve" approved="no">
|
||||
<source>%@ connected</source>
|
||||
<target state="translated">%@ conectado</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="%@, %@ and %lld members" xml:space="preserve" approved="no">
|
||||
<source>%@, %@ and %lld members</source>
|
||||
<target state="translated">%@, %@ e %lld membros</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="%@, %@ and %lld other members connected" xml:space="preserve" approved="no">
|
||||
<source>%@, %@ and %lld other members connected</source>
|
||||
<target state="translated">%@, %@ e %lld outros membros conectados</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="%lld group events" xml:space="preserve" approved="no">
|
||||
<source>%lld group events</source>
|
||||
<target state="translated">%lld eventos de grupo</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="%lld messages marked deleted" xml:space="preserve" approved="no">
|
||||
<source>%lld messages marked deleted</source>
|
||||
<target state="translated">%lld mensagens marcadas como excluídas</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="%lld messages blocked" xml:space="preserve" approved="no">
|
||||
<source>%lld messages blocked</source>
|
||||
<target state="translated">%lld mensagens bloqueadas</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="%lld messages moderated by %@" xml:space="preserve" approved="no">
|
||||
<source>%lld messages moderated by %@</source>
|
||||
<target state="translated">%lld mensagens moderadas por %@</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="(this device v%@)" xml:space="preserve" approved="no">
|
||||
<source>(this device v%@)</source>
|
||||
<target state="translated">(este dispositivo v%@)</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="All data is erased when it is entered." xml:space="preserve" approved="no">
|
||||
<source>All data is erased when it is entered.</source>
|
||||
<target state="translated">Todos os dados são apagados quando são inseridos.</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. (24 hours)" xml:space="preserve" approved="no">
|
||||
<source>Allow irreversible message deletion only if your contact allows it to you. (24 hours)</source>
|
||||
<target state="translated">Permita a exclusão irreversível de mensagens somente se o seu contato permitir. (24 horas)</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Apply" xml:space="preserve" approved="no">
|
||||
<source>Apply</source>
|
||||
<target state="translated">Aplicar</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Archive and upload" xml:space="preserve" approved="no">
|
||||
<source>Archive and upload</source>
|
||||
<target state="translated">Arquivar e fazer envio</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="All new messages from %@ will be hidden!" xml:space="preserve" approved="no">
|
||||
<source>All new messages from %@ will be hidden!</source>
|
||||
<target state="translated">Todas as novas mensagens de %@ ficarão ocultas!</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Already connecting!" xml:space="preserve" approved="no">
|
||||
<source>Already connecting!</source>
|
||||
<target state="translated">Já conectando!</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="App encrypts new local files (except videos)." xml:space="preserve" approved="no">
|
||||
<source>App encrypts new local files (except videos).</source>
|
||||
<target state="translated">O aplicativo criptografa novos arquivos locais (exceto vídeos).</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="App passcode is replaced with self-destruct passcode." xml:space="preserve" approved="no">
|
||||
<source>App passcode is replaced with self-destruct passcode.</source>
|
||||
<target state="translated">A senha do Aplicativo é substituída pela senha de autodestruição.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="All your contacts will remain connected." xml:space="preserve" approved="no">
|
||||
<source>All your contacts will remain connected.</source>
|
||||
<target state="translated">Todos os seus contatos permanecerão conectados.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="All your contacts will remain connected. Profile update will be sent to your contacts." xml:space="preserve" approved="no">
|
||||
<source>All your contacts will remain connected. Profile update will be sent to your contacts.</source>
|
||||
<target state="translated">Todos os seus contatos permanecerão conectados. A atualização do perfil será enviada para seus contatos.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Allow to send files and media." xml:space="preserve" approved="no">
|
||||
<source>Allow to send files and media.</source>
|
||||
<target state="translated">Permitir o envio de arquivos e mídia.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="App data migration" xml:space="preserve" approved="no">
|
||||
<source>App data migration</source>
|
||||
<target state="translated">Migração de dados de aplicativos</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Archiving database" xml:space="preserve" approved="no">
|
||||
<source>Archiving database</source>
|
||||
<target state="translated">Arquivando banco de dados</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="All messages will be deleted - this cannot be undone!" xml:space="preserve" approved="no">
|
||||
<source>All messages will be deleted - this cannot be undone!</source>
|
||||
<target state="translated">Todas as mensagens serão apagadas – isso não pode ser desfeito!</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="All your contacts, conversations and files will be securely encrypted and uploaded in chunks to configured XFTP relays." xml:space="preserve" approved="no">
|
||||
<source>All your contacts, conversations and files will be securely encrypted and uploaded in chunks to configured XFTP relays.</source>
|
||||
<target state="translated">Todos os seus contatos, conversas e arquivos serão criptografados com segurança e enviados em partes para retransmissões XFTP configuradas.</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" approved="no">
|
||||
<source>Allow message reactions only if your contact allows them.</source>
|
||||
<target state="translated">Permita reações às mensagens somente se o seu contato permitir.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Allow message reactions." xml:space="preserve" approved="no">
|
||||
<source>Allow message reactions.</source>
|
||||
<target state="translated">Permitir reações às mensagens.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Allow your contacts to irreversibly delete sent messages. (24 hours)" xml:space="preserve" approved="no">
|
||||
<source>Allow your contacts to irreversibly delete sent messages. (24 hours)</source>
|
||||
<target state="translated">Permita que seus contatos apaguem irreversivelmente as mensagens enviadas. (24 horas)</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Already joining the group!" xml:space="preserve" approved="no">
|
||||
<source>Already joining the group!</source>
|
||||
<target state="translated">Entrando no grupo!</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="An empty chat profile with the provided name is created, and the app opens as usual." xml:space="preserve" approved="no">
|
||||
<source>An empty chat profile with the provided name is created, and the app opens as usual.</source>
|
||||
<target state="translated">Um perfil de conversa vazio com o nome fornecido é criado e o aplicativo abre normalmente.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Auto-accept" xml:space="preserve" approved="no">
|
||||
<source>Auto-accept</source>
|
||||
<target state="translated">Aceitar automaticamente</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Allow your contacts adding message reactions." xml:space="preserve" approved="no">
|
||||
<source>Allow your contacts adding message reactions.</source>
|
||||
<target state="translated">Permita que seus contatos adicionem reações às mensagens.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Allow to irreversibly delete sent messages. (24 hours)" xml:space="preserve" approved="no">
|
||||
<source>Allow to irreversibly delete sent messages. (24 hours)</source>
|
||||
<target state="translated">Permitir apagar irreversivelmente as mensagens enviadas. (24 horas)</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
<file original="en.lproj/SimpleX--iOS--InfoPlist.strings" source-language="en" target-language="pt" datatype="plaintext">
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -101,6 +101,10 @@
|
||||
<source>%@ connected</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="%@ downloaded" xml:space="preserve">
|
||||
<source>%@ downloaded</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="%@ is connected!" xml:space="preserve">
|
||||
<source>%@ is connected!</source>
|
||||
<target>%@ เชื่อมต่อสำเร็จ!</target>
|
||||
@@ -121,6 +125,10 @@
|
||||
<target>%@ เซิร์ฟเวอร์</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="%@ uploaded" xml:space="preserve">
|
||||
<source>%@ uploaded</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="%@ wants to connect!" xml:space="preserve">
|
||||
<source>%@ wants to connect!</source>
|
||||
<target>%@ อยากเชื่อมต่อ!</target>
|
||||
@@ -324,6 +332,10 @@
|
||||
<target>**ส่วนตัวที่สุด**: ไม่ใช้เซิร์ฟเวอร์การแจ้งเตือนของ SimpleX Chat ตรวจสอบข้อความเป็นระยะในพื้นหลัง (ขึ้นอยู่กับความถี่ที่คุณใช้แอป)</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="**Please note**: using the same database on two devices will break the decryption of messages from your connections, as a security protection." xml:space="preserve">
|
||||
<source>**Please note**: using the same database on two devices will break the decryption of messages from your connections, as a security protection.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="**Please note**: you will NOT be able to recover or change passphrase if you lose it." xml:space="preserve">
|
||||
<source>**Please note**: you will NOT be able to recover or change passphrase if you lose it.</source>
|
||||
<target>**โปรดทราบ**: คุณจะไม่สามารถกู้คืนหรือเปลี่ยนรหัสผ่านได้หากคุณทำรหัสผ่านหาย</target>
|
||||
@@ -339,6 +351,10 @@
|
||||
<target>**คำเตือน**: การแจ้งเตือนแบบพุชทันทีจำเป็นต้องบันทึกรหัสผ่านไว้ใน Keychain</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="**Warning**: the archive will be removed." xml:space="preserve">
|
||||
<source>**Warning**: the archive will be removed.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="**e2e encrypted** audio call" xml:space="preserve">
|
||||
<source>**e2e encrypted** audio call</source>
|
||||
<target>การโทรเสียงแบบ **encrypted จากต้นจนจบ**</target>
|
||||
@@ -585,6 +601,10 @@
|
||||
<target>การเปลี่ยนแปลงที่อยู่จะถูกยกเลิก จะใช้ที่อยู่เก่าของผู้รับ</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Admins can block a member for all." xml:space="preserve">
|
||||
<source>Admins can block a member for all.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Admins can create the links to join groups." xml:space="preserve">
|
||||
<source>Admins can create the links to join groups.</source>
|
||||
<target>ผู้ดูแลระบบสามารถสร้างลิงก์เพื่อเข้าร่วมกลุ่มต่างๆได้</target>
|
||||
@@ -638,6 +658,10 @@
|
||||
<target>ผู้ติดต่อทั้งหมดของคุณจะยังคงเชื่อมต่ออยู่. ผู้ติดต่อทั้งหมดของคุณจะยังคงเชื่อมต่ออยู่ การอัปเดตโปรไฟล์จะถูกส่งไปยังผู้ติดต่อของคุณ.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="All your contacts, conversations and files will be securely encrypted and uploaded in chunks to configured XFTP relays." xml:space="preserve">
|
||||
<source>All your contacts, conversations and files will be securely encrypted and uploaded in chunks to configured XFTP relays.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Allow" xml:space="preserve">
|
||||
<source>Allow</source>
|
||||
<target>อนุญาต</target>
|
||||
@@ -683,6 +707,10 @@
|
||||
<target>อนุญาตให้ลบข้อความที่ส่งไปแล้วอย่างถาวร</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Allow to send SimpleX links." xml:space="preserve">
|
||||
<source>Allow to send SimpleX links.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Allow to send files and media." xml:space="preserve">
|
||||
<source>Allow to send files and media.</source>
|
||||
<target>อนุญาตให้ส่งไฟล์และสื่อ</target>
|
||||
@@ -761,6 +789,10 @@
|
||||
<target>รุ่นแอป: %@</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="App data migration" xml:space="preserve">
|
||||
<source>App data migration</source>
|
||||
<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>
|
||||
@@ -795,6 +827,18 @@
|
||||
<target>รูปร่างลักษณะ</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Apply" xml:space="preserve">
|
||||
<source>Apply</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Archive and upload" xml:space="preserve">
|
||||
<source>Archive and upload</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Archiving database" xml:space="preserve">
|
||||
<source>Archiving database</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Attach" xml:space="preserve">
|
||||
<source>Attach</source>
|
||||
<target>แนบ</target>
|
||||
@@ -974,6 +1018,10 @@
|
||||
<target>ยกเลิก</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Cancel migration" xml:space="preserve">
|
||||
<source>Cancel migration</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Cannot access keychain to save database password" xml:space="preserve">
|
||||
<source>Cannot access keychain to save database password</source>
|
||||
<target>ไม่สามารถเข้าถึง keychain เพื่อบันทึกรหัสผ่านฐานข้อมูล</target>
|
||||
@@ -984,6 +1032,10 @@
|
||||
<target>ไม่สามารถรับไฟล์ได้</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Cellular" xml:space="preserve">
|
||||
<source>Cellular</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Change" xml:space="preserve">
|
||||
<source>Change</source>
|
||||
<target>เปลี่ยน</target>
|
||||
@@ -1074,6 +1126,10 @@
|
||||
<source>Chat is stopped. If you already used this database on another device, you should transfer it back before starting chat.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Chat migrated!" xml:space="preserve">
|
||||
<source>Chat migrated!</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Chat preferences" xml:space="preserve">
|
||||
<source>Chat preferences</source>
|
||||
<target>ค่ากําหนดในการแชท</target>
|
||||
@@ -1094,6 +1150,10 @@
|
||||
<target>อินเทอร์เฟซภาษาจีนและสเปน</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Choose _Migrate from another device_ on the new device and scan QR code." xml:space="preserve">
|
||||
<source>Choose _Migrate from another device_ on the new device and scan QR code.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Choose file" xml:space="preserve">
|
||||
<source>Choose file</source>
|
||||
<target>เลือกไฟล์</target>
|
||||
@@ -1163,6 +1223,10 @@
|
||||
<target>ยืนยันการอัพเกรดฐานข้อมูล</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Confirm network settings" xml:space="preserve">
|
||||
<source>Confirm network settings</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Confirm new passphrase…" xml:space="preserve">
|
||||
<source>Confirm new passphrase…</source>
|
||||
<target>ยืนยันรหัสผ่านใหม่…</target>
|
||||
@@ -1173,6 +1237,14 @@
|
||||
<target>ยืนยันรหัสผ่าน</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Confirm that you remember database passphrase to migrate it." xml:space="preserve">
|
||||
<source>Confirm that you remember database passphrase to migrate it.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Confirm upload" xml:space="preserve">
|
||||
<source>Confirm upload</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Connect" xml:space="preserve">
|
||||
<source>Connect</source>
|
||||
<target>เชื่อมต่อ</target>
|
||||
@@ -1410,6 +1482,10 @@ This is your own one-time link!</source>
|
||||
<target>สร้างเมื่อ %@</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Creating archive link" xml:space="preserve">
|
||||
<source>Creating archive link</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Creating link…" xml:space="preserve">
|
||||
<source>Creating link…</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
@@ -1625,6 +1701,10 @@ This cannot be undone!</source>
|
||||
<target>ลบฐานข้อมูล</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Delete database from this device" xml:space="preserve">
|
||||
<source>Delete database from this device</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Delete file" xml:space="preserve">
|
||||
<source>Delete file</source>
|
||||
<target>ลบไฟล์</target>
|
||||
@@ -1907,11 +1987,27 @@ This cannot be undone!</source>
|
||||
<target>ปรับลดรุ่นและเปิดแชท</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Download" xml:space="preserve">
|
||||
<source>Download</source>
|
||||
<note>chat item action</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Download failed" xml:space="preserve">
|
||||
<source>Download failed</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Download file" xml:space="preserve">
|
||||
<source>Download file</source>
|
||||
<target>ดาวน์โหลดไฟล์</target>
|
||||
<note>server test step</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Downloading archive" xml:space="preserve">
|
||||
<source>Downloading archive</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Downloading link details" xml:space="preserve">
|
||||
<source>Downloading link details</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Duplicate display name!" xml:space="preserve">
|
||||
<source>Duplicate display name!</source>
|
||||
<target>ชื่อที่แสดงซ้ำ!</target>
|
||||
@@ -1966,6 +2062,10 @@ This cannot be undone!</source>
|
||||
<target>เปิดใช้งานสําหรับทุกคน</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Enable in direct chats (BETA)!" xml:space="preserve">
|
||||
<source>Enable in direct chats (BETA)!</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Enable instant notifications?" xml:space="preserve">
|
||||
<source>Enable instant notifications?</source>
|
||||
<target>เปิดใช้งานการแจ้งเตือนทันที?</target>
|
||||
@@ -1996,6 +2096,10 @@ This cannot be undone!</source>
|
||||
<target>เปิดใช้งานรหัสผ่านแบบทําลายตัวเอง</target>
|
||||
<note>set passcode view</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Enabled for" xml:space="preserve">
|
||||
<source>Enabled for</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Encrypt" xml:space="preserve">
|
||||
<source>Encrypt</source>
|
||||
<target>Encrypt</target>
|
||||
@@ -2075,6 +2179,10 @@ This cannot be undone!</source>
|
||||
<source>Enter group name…</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Enter passphrase" xml:space="preserve">
|
||||
<source>Enter passphrase</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Enter passphrase…" xml:space="preserve">
|
||||
<source>Enter passphrase…</source>
|
||||
<target>ใส่รหัสผ่าน</target>
|
||||
@@ -2133,6 +2241,10 @@ This cannot be undone!</source>
|
||||
<target>เกิดข้อผิดพลาดในการเพิ่มสมาชิก</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Error allowing contact PQ encryption" xml:space="preserve">
|
||||
<source>Error allowing contact PQ encryption</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Error changing address" xml:space="preserve">
|
||||
<source>Error changing address</source>
|
||||
<target>เกิดข้อผิดพลาดในการเปลี่ยนที่อยู่</target>
|
||||
@@ -2220,6 +2332,10 @@ This cannot be undone!</source>
|
||||
<target>เกิดข้อผิดพลาดในการลบโปรไฟล์ผู้ใช้</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Error downloading the archive" xml:space="preserve">
|
||||
<source>Error downloading the archive</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Error enabling delivery receipts!" xml:space="preserve">
|
||||
<source>Error enabling delivery receipts!</source>
|
||||
<target>เกิดข้อผิดพลาดในการเปิดใช้ใบเสร็จการจัดส่ง!</target>
|
||||
@@ -2294,6 +2410,10 @@ This cannot be undone!</source>
|
||||
<target>เกิดข้อผิดพลาดในการบันทึกรหัสผ่านไปยัง keychain</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Error saving settings" xml:space="preserve">
|
||||
<source>Error saving settings</source>
|
||||
<note>when migrating</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Error saving user password" xml:space="preserve">
|
||||
<source>Error saving user password</source>
|
||||
<target>เกิดข้อผิดพลาดในการบันทึกรหัสผ่านผู้ใช้</target>
|
||||
@@ -2362,6 +2482,14 @@ This cannot be undone!</source>
|
||||
<target>เกิดข้อผิดพลาดในการอัปเดตข้อมูลส่วนตัวของผู้ใช้</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Error uploading the archive" xml:space="preserve">
|
||||
<source>Error uploading the archive</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Error verifying passphrase:" xml:space="preserve">
|
||||
<source>Error verifying passphrase:</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Error: " xml:space="preserve">
|
||||
<source>Error: </source>
|
||||
<target>ผิดพลาด: </target>
|
||||
@@ -2411,6 +2539,10 @@ This cannot be undone!</source>
|
||||
<target>ที่เก็บถาวรฐานข้อมูลที่ส่งออก</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Exported file doesn't exist" xml:space="preserve">
|
||||
<source>Exported file doesn't exist</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Exporting database archive…" xml:space="preserve">
|
||||
<source>Exporting database archive…</source>
|
||||
<target>กำลังส่งออกที่เก็บถาวรฐานข้อมูล…</target>
|
||||
@@ -2470,6 +2602,10 @@ This cannot be undone!</source>
|
||||
<target>ไฟล์และสื่อเป็นสิ่งต้องห้ามในกลุ่มนี้</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Files and media not allowed" xml:space="preserve">
|
||||
<source>Files and media not allowed</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Files and media prohibited!" xml:space="preserve">
|
||||
<source>Files and media prohibited!</source>
|
||||
<target>ไฟล์และสื่อต้องห้าม!</target>
|
||||
@@ -2480,6 +2616,14 @@ This cannot be undone!</source>
|
||||
<target>กรองแชทที่ยังไม่อ่านและแชทโปรด</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Finalize migration" xml:space="preserve">
|
||||
<source>Finalize migration</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Finalize migration on another device." xml:space="preserve">
|
||||
<source>Finalize migration on another device.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Finally, we have them! 🚀" xml:space="preserve">
|
||||
<source>Finally, we have them! 🚀</source>
|
||||
<target>ในที่สุดเราก็มีแล้ว! 🚀</target>
|
||||
@@ -2525,6 +2669,18 @@ This cannot be undone!</source>
|
||||
<target>สำหรับคอนโซล</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Forward" xml:space="preserve">
|
||||
<source>Forward</source>
|
||||
<note>chat item action</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Forwarded" xml:space="preserve">
|
||||
<source>Forwarded</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Forwarded from" xml:space="preserve">
|
||||
<source>Forwarded from</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Found desktop" xml:space="preserve">
|
||||
<source>Found desktop</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
@@ -2631,6 +2787,10 @@ This cannot be undone!</source>
|
||||
<target>สมาชิกกลุ่มสามารถลบข้อความที่ส่งแล้วอย่างถาวร</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Group members can send SimpleX links." xml:space="preserve">
|
||||
<source>Group members can send SimpleX links.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Group members can send direct messages." xml:space="preserve">
|
||||
<source>Group members can send direct messages.</source>
|
||||
<target>สมาชิกกลุ่มสามารถส่งข้อความโดยตรงได้</target>
|
||||
@@ -2765,6 +2925,10 @@ This cannot be undone!</source>
|
||||
<target>วิธีใช้เซิร์ฟเวอร์ของคุณ</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Hungarian interface" xml:space="preserve">
|
||||
<source>Hungarian interface</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="ICE servers (one per line)" xml:space="preserve">
|
||||
<source>ICE servers (one per line)</source>
|
||||
<target>เซิร์ฟเวอร์ ICE (หนึ่งเครื่องต่อสาย)</target>
|
||||
@@ -2830,6 +2994,14 @@ This cannot be undone!</source>
|
||||
<target>นำเข้าฐานข้อมูล</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Import failed" xml:space="preserve">
|
||||
<source>Import failed</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Importing archive" xml:space="preserve">
|
||||
<source>Importing archive</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Improved message delivery" xml:space="preserve">
|
||||
<source>Improved message delivery</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
@@ -2844,6 +3016,10 @@ This cannot be undone!</source>
|
||||
<target>ปรับปรุงการกําหนดค่าเซิร์ฟเวอร์แล้ว</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="In order to continue, chat should be stopped." xml:space="preserve">
|
||||
<source>In order to continue, chat should be stopped.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="In reply to" xml:space="preserve">
|
||||
<source>In reply to</source>
|
||||
<target>ในการตอบกลับถึง</target>
|
||||
@@ -2950,6 +3126,10 @@ This cannot be undone!</source>
|
||||
<source>Invalid link</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Invalid migration confirmation" xml:space="preserve">
|
||||
<source>Invalid migration confirmation</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Invalid name!" xml:space="preserve">
|
||||
<source>Invalid name!</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
@@ -3304,6 +3484,10 @@ This is your link for group %@!</source>
|
||||
<target>ข้อความ</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Message too large" xml:space="preserve">
|
||||
<source>Message too large</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Messages" xml:space="preserve">
|
||||
<source>Messages</source>
|
||||
<target>ข้อความ</target>
|
||||
@@ -3318,11 +3502,47 @@ This is your link for group %@!</source>
|
||||
<source>Messages from %@ will be shown!</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Messages, files and calls are protected by **end-to-end encryption** with perfect forward secrecy, repudiation and break-in recovery." xml:space="preserve">
|
||||
<source>Messages, files and calls are protected by **end-to-end encryption** with perfect forward secrecy, repudiation and break-in recovery.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Messages, files and calls are protected by **quantum resistant e2e encryption** with perfect forward secrecy, repudiation and break-in recovery." xml:space="preserve">
|
||||
<source>Messages, files and calls are protected by **quantum resistant e2e encryption** with perfect forward secrecy, repudiation and break-in recovery.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Migrate device" xml:space="preserve">
|
||||
<source>Migrate device</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Migrate from another device" xml:space="preserve">
|
||||
<source>Migrate from another device</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Migrate here" xml:space="preserve">
|
||||
<source>Migrate here</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Migrate to another device" xml:space="preserve">
|
||||
<source>Migrate to another device</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Migrate to another device via QR code." xml:space="preserve">
|
||||
<source>Migrate to another device via QR code.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Migrating" xml:space="preserve">
|
||||
<source>Migrating</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Migrating database archive…" xml:space="preserve">
|
||||
<source>Migrating database archive…</source>
|
||||
<target>กำลังย้ายข้อมูลที่เก็บถาวรของฐานข้อมูล…</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Migration complete" xml:space="preserve">
|
||||
<source>Migration complete</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Migration error:" xml:space="preserve">
|
||||
<source>Migration error:</source>
|
||||
<target>ข้อผิดพลาดในการย้ายข้อมูล:</target>
|
||||
@@ -3397,6 +3617,10 @@ This is your link for group %@!</source>
|
||||
<target>เครือข่ายและเซิร์ฟเวอร์</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Network connection" xml:space="preserve">
|
||||
<source>Network connection</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Network settings" xml:space="preserve">
|
||||
<source>Network settings</source>
|
||||
<target>การตั้งค่าเครือข่าย</target>
|
||||
@@ -3504,6 +3728,10 @@ This is your link for group %@!</source>
|
||||
<target>ไม่มีประวัติ</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="No network connection" xml:space="preserve">
|
||||
<source>No network connection</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="No permission to record voice message" xml:space="preserve">
|
||||
<source>No permission to record voice message</source>
|
||||
<target>ไม่อนุญาตให้บันทึกข้อความเสียง</target>
|
||||
@@ -3674,6 +3902,10 @@ This is your link for group %@!</source>
|
||||
<source>Open group</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Open migration to another device" xml:space="preserve">
|
||||
<source>Open migration to another device</source>
|
||||
<note>authentication reason</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Open user profiles" xml:space="preserve">
|
||||
<source>Open user profiles</source>
|
||||
<target>เปิดโปรไฟล์ผู้ใช้</target>
|
||||
@@ -3688,14 +3920,26 @@ This is your link for group %@!</source>
|
||||
<source>Opening app…</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Or paste archive link" xml:space="preserve">
|
||||
<source>Or paste archive link</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Or scan QR code" xml:space="preserve">
|
||||
<source>Or scan QR code</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Or securely share this file link" xml:space="preserve">
|
||||
<source>Or securely share this file link</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Or show this code" xml:space="preserve">
|
||||
<source>Or show this code</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Other" xml:space="preserve">
|
||||
<source>Other</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="PING count" xml:space="preserve">
|
||||
<source>PING count</source>
|
||||
<target>จํานวน PING</target>
|
||||
@@ -3772,6 +4016,10 @@ This is your link for group %@!</source>
|
||||
<target>ข้อผิดพลาดในการถอดรหัสอย่างถาวร</target>
|
||||
<note>message decrypt error item</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Picture-in-picture calls" xml:space="preserve">
|
||||
<source>Picture-in-picture calls</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</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>โปรดขอให้ผู้ติดต่อของคุณเปิดใช้งานการส่งข้อความเสียง</target>
|
||||
@@ -3792,6 +4040,10 @@ This is your link for group %@!</source>
|
||||
<target>โปรดตรวจสอบความต้องการของคุณและการตั้งค่าผู้ติดต่อ</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Please confirm that network settings are correct for this device." xml:space="preserve">
|
||||
<source>Please confirm that network settings are correct for this device.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Please contact developers. Error: %@" xml:space="preserve">
|
||||
<source>Please contact developers.
|
||||
Error: %@</source>
|
||||
@@ -3847,6 +4099,10 @@ Error: %@</source>
|
||||
<target>อาจเป็นไปได้ว่าลายนิ้วมือของ certificate ในที่อยู่เซิร์ฟเวอร์ไม่ถูกต้อง</target>
|
||||
<note>server test error</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Post-quantum E2EE" xml:space="preserve">
|
||||
<source>Post-quantum E2EE</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Preserve the last message draft, with attachments." xml:space="preserve">
|
||||
<source>Preserve the last message draft, with attachments.</source>
|
||||
<target>เก็บข้อความที่ร่างไว้ล่าสุดพร้อมไฟล์แนบ</target>
|
||||
@@ -3934,6 +4190,10 @@ Error: %@</source>
|
||||
<target>ห้ามแสดงปฏิกิริยาต่อข้อความ</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Prohibit sending SimpleX links." xml:space="preserve">
|
||||
<source>Prohibit sending SimpleX links.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Prohibit sending direct messages to members." xml:space="preserve">
|
||||
<source>Prohibit sending direct messages to members.</source>
|
||||
<target>ห้ามส่งข้อความโดยตรงถึงสมาชิก</target>
|
||||
@@ -3979,6 +4239,14 @@ Error: %@</source>
|
||||
<target>การแจ้งเตือนแบบทันที</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Push server" xml:space="preserve">
|
||||
<source>Push server</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Quantum resistant encryption" xml:space="preserve">
|
||||
<source>Quantum resistant encryption</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Rate the app" xml:space="preserve">
|
||||
<source>Rate the app</source>
|
||||
<target>ให้คะแนนแอป</target>
|
||||
@@ -4052,6 +4320,10 @@ Error: %@</source>
|
||||
<target>ที่อยู่ผู้รับจะถูกเปลี่ยนเป็นเซิร์ฟเวอร์อื่น การเปลี่ยนแปลงที่อยู่จะเสร็จสมบูรณ์หลังจากที่ผู้ส่งออนไลน์</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Receiving concurrency" xml:space="preserve">
|
||||
<source>Receiving concurrency</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Receiving file will be stopped." xml:space="preserve">
|
||||
<source>Receiving file will be stopped.</source>
|
||||
<target>การรับไฟล์จะหยุดลง</target>
|
||||
@@ -4066,6 +4338,10 @@ Error: %@</source>
|
||||
<source>Recent history and improved [directory bot](simplex:/contact#/?v=1-4&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FeXSPwqTkKyDO3px4fLf1wx3MvPdjdLW3%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAaiv6MkMH44L2TcYrt_CsX3ZvM11WgbMEUn0hkIKTOho%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion).</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Recipient(s) can't see who this message is from." xml:space="preserve">
|
||||
<source>Recipient(s) can't see who this message is from.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Recipients see updates as you type them." xml:space="preserve">
|
||||
<source>Recipients see updates as you type them.</source>
|
||||
<target>ผู้รับจะเห็นการอัปเดตเมื่อคุณพิมพ์</target>
|
||||
@@ -4159,10 +4435,22 @@ Error: %@</source>
|
||||
<source>Repeat connection request?</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Repeat download" xml:space="preserve">
|
||||
<source>Repeat download</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Repeat import" xml:space="preserve">
|
||||
<source>Repeat import</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Repeat join request?" xml:space="preserve">
|
||||
<source>Repeat join request?</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Repeat upload" xml:space="preserve">
|
||||
<source>Repeat upload</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Reply" xml:space="preserve">
|
||||
<source>Reply</source>
|
||||
<target>ตอบ</target>
|
||||
@@ -4262,6 +4550,10 @@ Error: %@</source>
|
||||
<target>เซิร์ฟเวอร์ SMP</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Safer groups" xml:space="preserve">
|
||||
<source>Safer groups</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Save" xml:space="preserve">
|
||||
<source>Save</source>
|
||||
<target>บันทึก</target>
|
||||
@@ -4342,11 +4634,19 @@ Error: %@</source>
|
||||
<target>บันทึกข้อความต้อนรับ?</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Saved" xml:space="preserve">
|
||||
<source>Saved</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Saved WebRTC ICE servers will be removed" xml:space="preserve">
|
||||
<source>Saved WebRTC ICE servers will be removed</source>
|
||||
<target>เซิร์ฟเวอร์ WebRTC ICE ที่บันทึกไว้จะถูกลบออก</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Saved from" xml:space="preserve">
|
||||
<source>Saved from</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Saved message" xml:space="preserve">
|
||||
<source>Saved message</source>
|
||||
<note>message info title</note>
|
||||
@@ -4618,6 +4918,10 @@ Error: %@</source>
|
||||
<target>ตั้งรหัสผ่าน</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Set passphrase" xml:space="preserve">
|
||||
<source>Set passphrase</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Set passphrase to export" xml:space="preserve">
|
||||
<source>Set passphrase to export</source>
|
||||
<target>ตั้งรหัสผ่านเพื่อส่งออก</target>
|
||||
@@ -4672,6 +4976,10 @@ Error: %@</source>
|
||||
<target>แชร์กับผู้ติดต่อ</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Show QR code" xml:space="preserve">
|
||||
<source>Show QR code</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Show calls in phone history" xml:space="preserve">
|
||||
<source>Show calls in phone history</source>
|
||||
<target>แสดงการโทรในประวัติการโทร</target>
|
||||
@@ -4749,6 +5057,14 @@ Error: %@</source>
|
||||
<trans-unit id="SimpleX links" xml:space="preserve">
|
||||
<source>SimpleX links</source>
|
||||
<target>ลิงก์ SimpleX</target>
|
||||
<note>chat feature</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="SimpleX links are prohibited in this group." xml:space="preserve">
|
||||
<source>SimpleX links are prohibited in this group.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="SimpleX links not allowed" xml:space="preserve">
|
||||
<source>SimpleX links not allowed</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="SimpleX one-time invitation" xml:space="preserve">
|
||||
@@ -4808,6 +5124,10 @@ Error: %@</source>
|
||||
<target>หยุด SimpleX</target>
|
||||
<note>authentication reason</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Stop chat" xml:space="preserve">
|
||||
<source>Stop chat</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Stop chat to enable database actions" xml:space="preserve">
|
||||
<source>Stop chat to enable database actions</source>
|
||||
<target>หยุดการแชทเพื่อเปิดใช้งานการดำเนินการกับฐานข้อมูล</target>
|
||||
@@ -4848,6 +5168,10 @@ Error: %@</source>
|
||||
<target>หยุดแชร์ที่อยู่ไหม?</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Stopping chat" xml:space="preserve">
|
||||
<source>Stopping chat</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Submit" xml:space="preserve">
|
||||
<source>Submit</source>
|
||||
<target>ส่ง</target>
|
||||
@@ -5090,6 +5414,14 @@ It can happen because of some bug or when the connection is compromised.</source
|
||||
<target>การดำเนินการนี้ไม่สามารถยกเลิกได้ - โปรไฟล์ ผู้ติดต่อ ข้อความ และไฟล์ของคุณจะสูญหายไปอย่างถาวร</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="This chat is protected by end-to-end encryption." xml:space="preserve">
|
||||
<source>This chat is protected by end-to-end encryption.</source>
|
||||
<note>E2EE info chat item</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="This chat is protected by quantum resistant end-to-end encryption." xml:space="preserve">
|
||||
<source>This chat is protected by quantum resistant end-to-end encryption.</source>
|
||||
<note>E2EE info chat item</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="This device name" xml:space="preserve">
|
||||
<source>This device name</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
@@ -5368,11 +5700,19 @@ To connect, please ask your contact to create another connection link and check
|
||||
<target>อัปเกรดและเปิดการแชท</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Upload failed" xml:space="preserve">
|
||||
<source>Upload failed</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Upload file" xml:space="preserve">
|
||||
<source>Upload file</source>
|
||||
<target>อัปโหลดไฟล์</target>
|
||||
<note>server test step</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Uploading archive" xml:space="preserve">
|
||||
<source>Uploading archive</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Use .onion hosts" xml:space="preserve">
|
||||
<source>Use .onion hosts</source>
|
||||
<target>ใช้โฮสต์ .onion</target>
|
||||
@@ -5419,6 +5759,10 @@ To connect, please ask your contact to create another connection link and check
|
||||
<target>ใช้เซิร์ฟเวอร์</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Use the app while in the call." xml:space="preserve">
|
||||
<source>Use the app while in the call.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="User profile" xml:space="preserve">
|
||||
<source>User profile</source>
|
||||
<target>โปรไฟล์ผู้ใช้</target>
|
||||
@@ -5451,6 +5795,14 @@ To connect, please ask your contact to create another connection link and check
|
||||
<source>Verify connections</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Verify database passphrase" xml:space="preserve">
|
||||
<source>Verify database passphrase</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Verify passphrase" xml:space="preserve">
|
||||
<source>Verify passphrase</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Verify security code" xml:space="preserve">
|
||||
<source>Verify security code</source>
|
||||
<target>ตรวจสอบรหัสความปลอดภัย</target>
|
||||
@@ -5509,6 +5861,10 @@ To connect, please ask your contact to create another connection link and check
|
||||
<target>ข้อความเสียงเป็นสิ่งต้องห้ามในกลุ่มนี้</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Voice messages not allowed" xml:space="preserve">
|
||||
<source>Voice messages not allowed</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Voice messages prohibited!" xml:space="preserve">
|
||||
<source>Voice messages prohibited!</source>
|
||||
<target>ห้ามข้อความเสียง!</target>
|
||||
@@ -5538,6 +5894,10 @@ To connect, please ask your contact to create another connection link and check
|
||||
<target>กําลังรอวิดีโอ</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Warning: starting chat on multiple devices is not supported and will cause message delivery failures" xml:space="preserve">
|
||||
<source>Warning: starting chat on multiple devices is not supported and will cause message delivery failures</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Warning: you may lose some data!" xml:space="preserve">
|
||||
<source>Warning: you may lose some data!</source>
|
||||
<target>คำเตือน: คุณอาจสูญเสียข้อมูลบางส่วน!</target>
|
||||
@@ -5558,6 +5918,10 @@ To connect, please ask your contact to create another connection link and check
|
||||
<target>ข้อความต้อนรับ</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Welcome message is too long" xml:space="preserve">
|
||||
<source>Welcome message is too long</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="What's new" xml:space="preserve">
|
||||
<source>What's new</source>
|
||||
<target>มีอะไรใหม่</target>
|
||||
@@ -5578,6 +5942,14 @@ To connect, please ask your contact to create another connection link and check
|
||||
<target>เมื่อคุณแชร์โปรไฟล์ที่ไม่ระบุตัวตนกับใครสักคน โปรไฟล์นี้จะใช้สำหรับกลุ่มที่พวกเขาเชิญคุณ</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="WiFi" xml:space="preserve">
|
||||
<source>WiFi</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Wired ethernet" xml:space="preserve">
|
||||
<source>Wired ethernet</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="With encrypted files and media." xml:space="preserve">
|
||||
<source>With encrypted files and media.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
@@ -5611,6 +5983,10 @@ To connect, please ask your contact to create another connection link and check
|
||||
<target>คุณ</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="You **must not** use the same database on two devices." xml:space="preserve">
|
||||
<source>You **must not** use the same database on two devices.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="You accepted connection" xml:space="preserve">
|
||||
<source>You accepted connection</source>
|
||||
<target>คุณยอมรับการเชื่อมต่อ</target>
|
||||
@@ -5690,6 +6066,10 @@ Repeat join request?</source>
|
||||
<target>คุณสามารถเปิดใช้งานได้ในภายหลังผ่านการตั้งค่าความเป็นส่วนตัวและความปลอดภัยของแอป</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="You can give another try." xml:space="preserve">
|
||||
<source>You can give another try.</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="You can hide or mute a user profile - swipe it to the right." xml:space="preserve">
|
||||
<source>You can hide or mute a user profile - swipe it to the right.</source>
|
||||
<target>คุณสามารถซ่อนหรือปิดเสียงโปรไฟล์ผู้ใช้ - ปัดไปทางขวา</target>
|
||||
@@ -6031,6 +6411,10 @@ SimpleX servers cannot see your profile.</source>
|
||||
<target>ผู้ดูแลระบบ</target>
|
||||
<note>member role</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="admins" xml:space="preserve">
|
||||
<source>admins</source>
|
||||
<note>feature role</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="agreeing encryption for %@…" xml:space="preserve">
|
||||
<source>agreeing encryption for %@…</source>
|
||||
<target>ยอมรับ encryption สำหรับ %@…</target>
|
||||
@@ -6041,6 +6425,10 @@ SimpleX servers cannot see your profile.</source>
|
||||
<target>เห็นด้วยกับการ encryption…</target>
|
||||
<note>chat item text</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="all members" xml:space="preserve">
|
||||
<source>all members</source>
|
||||
<note>feature role</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="always" xml:space="preserve">
|
||||
<source>always</source>
|
||||
<target>เสมอ</target>
|
||||
@@ -6071,7 +6459,7 @@ SimpleX servers cannot see your profile.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="blocked" xml:space="preserve">
|
||||
<source>blocked</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
<note>marked deleted chat item preview text</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="blocked %@" xml:space="preserve">
|
||||
<source>blocked %@</source>
|
||||
@@ -6079,7 +6467,7 @@ SimpleX servers cannot see your profile.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="blocked by admin" xml:space="preserve">
|
||||
<source>blocked by admin</source>
|
||||
<note>blocked chat item</note>
|
||||
<note>marked deleted chat item preview text</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="bold" xml:space="preserve">
|
||||
<source>bold</source>
|
||||
@@ -6361,6 +6749,10 @@ SimpleX servers cannot see your profile.</source>
|
||||
<source>event happened</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="forwarded" xml:space="preserve">
|
||||
<source>forwarded</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="group deleted" xml:space="preserve">
|
||||
<source>group deleted</source>
|
||||
<target>ลบกลุ่มแล้ว</target>
|
||||
@@ -6503,7 +6895,7 @@ SimpleX servers cannot see your profile.</source>
|
||||
<trans-unit id="moderated by %@" xml:space="preserve">
|
||||
<source>moderated by %@</source>
|
||||
<target>กลั่นกรองโดย %@</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
<note>marked deleted chat item preview text</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="months" xml:space="preserve">
|
||||
<source>months</source>
|
||||
@@ -6567,11 +6959,19 @@ SimpleX servers cannot see your profile.</source>
|
||||
<target>เจ้าของ</target>
|
||||
<note>member role</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="owners" xml:space="preserve">
|
||||
<source>owners</source>
|
||||
<note>feature role</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="peer-to-peer" xml:space="preserve">
|
||||
<source>peer-to-peer</source>
|
||||
<target>เพื่อนต่อเพื่อน</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="quantum resistant e2e encryption" xml:space="preserve">
|
||||
<source>quantum resistant e2e encryption</source>
|
||||
<note>chat item text</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="received answer…" xml:space="preserve">
|
||||
<source>received answer…</source>
|
||||
<target>ได้รับคำตอบ…</target>
|
||||
@@ -6610,6 +7010,14 @@ SimpleX servers cannot see your profile.</source>
|
||||
<target>ลบคุณออกแล้ว</target>
|
||||
<note>rcv group event chat item</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="saved" xml:space="preserve">
|
||||
<source>saved</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="saved from %@" xml:space="preserve">
|
||||
<source>saved from %@</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="sec" xml:space="preserve">
|
||||
<source>sec</source>
|
||||
<target>วินาที</target>
|
||||
@@ -6642,6 +7050,10 @@ SimpleX servers cannot see your profile.</source>
|
||||
<source>set new profile picture</source>
|
||||
<note>profile update event chat item</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="standard end-to-end encryption" xml:space="preserve">
|
||||
<source>standard end-to-end encryption</source>
|
||||
<note>chat item text</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="starting…" xml:space="preserve">
|
||||
<source>starting…</source>
|
||||
<target>กำลังเริ่มต้น…</target>
|
||||
@@ -6738,6 +7150,10 @@ SimpleX servers cannot see your profile.</source>
|
||||
<target>ใช่</target>
|
||||
<note>pref value</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="you" xml:space="preserve">
|
||||
<source>you</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="you are invited to group" xml:space="preserve">
|
||||
<source>you are invited to group</source>
|
||||
<target>คุณได้รับเชิญให้เข้าร่วมกลุ่ม</target>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,64 +0,0 @@
|
||||
//
|
||||
// ConcurrentQueue.swift
|
||||
// SimpleX NSE
|
||||
//
|
||||
// Created by Evgeny on 08/12/2023.
|
||||
// Copyright © 2023 SimpleX Chat. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
struct DequeueElement<T> {
|
||||
var elementId: UUID?
|
||||
var task: Task<T?, Never>
|
||||
}
|
||||
|
||||
class ConcurrentQueue<T> {
|
||||
private var queue: [T] = []
|
||||
private var queueLock = DispatchQueue(label: "chat.simplex.app.SimpleX-NSE.concurrent-queue.lock.\(UUID())")
|
||||
private var continuations = [(elementId: UUID, continuation: CheckedContinuation<T?, Never>)]()
|
||||
|
||||
func enqueue(_ el: T) {
|
||||
resumeContinuation(el) { self.queue.append(el) }
|
||||
}
|
||||
|
||||
func frontEnqueue(_ el: T) {
|
||||
resumeContinuation(el) { self.queue.insert(el, at: 0) }
|
||||
}
|
||||
|
||||
private func resumeContinuation(_ el: T, add: @escaping () -> Void) {
|
||||
queueLock.sync {
|
||||
if let (_, cont) = continuations.first {
|
||||
continuations.remove(at: 0)
|
||||
cont.resume(returning: el)
|
||||
} else {
|
||||
add()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func dequeue() -> DequeueElement<T> {
|
||||
queueLock.sync {
|
||||
if queue.isEmpty {
|
||||
let elementId = UUID()
|
||||
let task = Task {
|
||||
await withCheckedContinuation { cont in
|
||||
continuations.append((elementId, cont))
|
||||
}
|
||||
}
|
||||
return DequeueElement(elementId: elementId, task: task)
|
||||
} else {
|
||||
let el = queue.remove(at: 0)
|
||||
return DequeueElement(task: Task { el })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func cancelDequeue(_ elementId: UUID) {
|
||||
queueLock.sync {
|
||||
let cancelled = continuations.filter { $0.elementId == elementId }
|
||||
continuations.removeAll { $0.elementId == elementId }
|
||||
cancelled.forEach { $0.continuation.resume(returning: nil) }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -22,110 +22,6 @@ let nseSuspendSchedule: SuspendSchedule = (2, 4)
|
||||
|
||||
let fastNSESuspendSchedule: SuspendSchedule = (1, 1)
|
||||
|
||||
typealias NtfStream = ConcurrentQueue<NSENotification>
|
||||
|
||||
// Notifications are delivered via concurrent queues, as they are all received from chat controller in a single loop that
|
||||
// writes to ConcurrentQueue and when notification is processed, the instance of Notification service extension reads from the queue.
|
||||
// One queue per connection (entity) is used.
|
||||
// The concurrent queues allow read cancellation, to ensure that notifications are not lost in case the current thread completes
|
||||
// before expected notification is read (multiple notifications can be expected, because one notification can be delivered for several messages).
|
||||
actor PendingNtfs {
|
||||
static let shared = PendingNtfs()
|
||||
private var ntfStreams: [String: NtfStream] = [:]
|
||||
|
||||
func createStream(_ id: String) async {
|
||||
logger.debug("NotificationService PendingNtfs.createStream: \(id)")
|
||||
if ntfStreams[id] == nil {
|
||||
ntfStreams[id] = ConcurrentQueue()
|
||||
logger.debug("NotificationService PendingNtfs.createStream: created ConcurrentQueue")
|
||||
}
|
||||
}
|
||||
|
||||
func readStream(_ id: String, for nse: NotificationService, ntfInfo: NtfMessages) async {
|
||||
logger.debug("NotificationService PendingNtfs.readStream: \(id) \(ntfInfo.ntfMessages.count)")
|
||||
if !ntfInfo.user.showNotifications {
|
||||
nse.setBestAttemptNtf(.empty)
|
||||
}
|
||||
if let s = ntfStreams[id] {
|
||||
logger.debug("NotificationService PendingNtfs.readStream: has stream")
|
||||
var expected = Set(ntfInfo.ntfMessages.map { $0.msgId })
|
||||
logger.debug("NotificationService PendingNtfs.readStream: expecting: \(expected)")
|
||||
var readCancelled = false
|
||||
var dequeued: DequeueElement<NSENotification>?
|
||||
nse.cancelRead = {
|
||||
readCancelled = true
|
||||
if let elementId = dequeued?.elementId {
|
||||
s.cancelDequeue(elementId)
|
||||
}
|
||||
}
|
||||
while !readCancelled {
|
||||
dequeued = s.dequeue()
|
||||
if let ntf = await dequeued?.task.value {
|
||||
if readCancelled {
|
||||
logger.debug("NotificationService PendingNtfs.readStream: read cancelled, put ntf to queue front")
|
||||
s.frontEnqueue(ntf)
|
||||
break
|
||||
} else if case let .msgInfo(info) = ntf {
|
||||
let found = expected.remove(info.msgId)
|
||||
if found != nil {
|
||||
logger.debug("NotificationService PendingNtfs.readStream: msgInfo, last: \(expected.isEmpty)")
|
||||
if expected.isEmpty { break }
|
||||
} else if let msgTs = ntfInfo.msgTs, info.msgTs > msgTs {
|
||||
logger.debug("NotificationService PendingNtfs.readStream: unexpected msgInfo")
|
||||
s.frontEnqueue(ntf)
|
||||
break
|
||||
}
|
||||
} else if ntfInfo.user.showNotifications {
|
||||
logger.debug("NotificationService PendingNtfs.readStream: setting best attempt")
|
||||
nse.setBestAttemptNtf(ntf)
|
||||
if ntf.isCallInvitation { break }
|
||||
}
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
nse.cancelRead = nil
|
||||
logger.debug("NotificationService PendingNtfs.readStream: exiting")
|
||||
}
|
||||
}
|
||||
|
||||
func writeStream(_ id: String, _ ntf: NSENotification) async {
|
||||
logger.debug("NotificationService PendingNtfs.writeStream: \(id)")
|
||||
if let s = ntfStreams[id] {
|
||||
logger.debug("NotificationService PendingNtfs.writeStream: writing ntf")
|
||||
s.enqueue(ntf)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The current implementation assumes concurrent notification delivery and uses semaphores
|
||||
// to process only one notification per connection (entity) at a time.
|
||||
class NtfStreamSemaphores {
|
||||
static let shared = NtfStreamSemaphores()
|
||||
private static let queue = DispatchQueue(label: "chat.simplex.app.SimpleX-NSE.notification-semaphores.lock")
|
||||
private var semaphores: [String: DispatchSemaphore] = [:]
|
||||
|
||||
func waitForStream(_ id: String) {
|
||||
streamSemaphore(id, value: 0)?.wait()
|
||||
}
|
||||
|
||||
func signalStreamReady(_ id: String) {
|
||||
streamSemaphore(id, value: 1)?.signal()
|
||||
}
|
||||
|
||||
// this function returns nil if semaphore is just created, so passed value shoud be coordinated with the desired end value of the semaphore
|
||||
private func streamSemaphore(_ id: String, value: Int) -> DispatchSemaphore? {
|
||||
NtfStreamSemaphores.queue.sync {
|
||||
if let s = semaphores[id] {
|
||||
return s
|
||||
} else {
|
||||
semaphores[id] = DispatchSemaphore(value: value)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum NSENotification {
|
||||
case nse(UNMutableNotificationContent)
|
||||
case callkit(RcvCallInvitation)
|
||||
@@ -149,7 +45,7 @@ class NSEThreads {
|
||||
static let shared = NSEThreads()
|
||||
private static let queue = DispatchQueue(label: "chat.simplex.app.SimpleX-NSE.notification-threads.lock")
|
||||
private var allThreads: Set<UUID> = []
|
||||
private var activeThreads: Set<UUID> = []
|
||||
private var activeThreads: [(UUID, NotificationService)] = []
|
||||
|
||||
func newThread() -> UUID {
|
||||
NSEThreads.queue.sync {
|
||||
@@ -158,19 +54,42 @@ class NSEThreads {
|
||||
}
|
||||
}
|
||||
|
||||
func startThread(_ t: UUID) {
|
||||
func startThread(_ t: UUID, _ service: NotificationService) {
|
||||
NSEThreads.queue.sync {
|
||||
if allThreads.contains(t) {
|
||||
_ = activeThreads.insert(t)
|
||||
activeThreads.append((t, service))
|
||||
} else {
|
||||
logger.warning("NotificationService startThread: thread \(t) was removed before it started")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func processNotification(_ id: ChatId, _ ntf: NSENotification) async -> Void {
|
||||
var waitTime: Int64 = 5_000_000000
|
||||
while waitTime > 0 {
|
||||
if let (_, nse) = rcvEntityThread(id),
|
||||
nse.shouldProcessNtf && nse.processReceivedNtf(ntf) {
|
||||
break
|
||||
} else {
|
||||
try? await Task.sleep(nanoseconds: 10_000000)
|
||||
waitTime -= 10_000000
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func rcvEntityThread(_ id: ChatId) -> (UUID, NotificationService)? {
|
||||
NSEThreads.queue.sync {
|
||||
activeThreads.first(where: { (_, nse) in nse.receiveEntityId == id })
|
||||
}
|
||||
}
|
||||
|
||||
func endThread(_ t: UUID) -> Bool {
|
||||
NSEThreads.queue.sync {
|
||||
let tActive = activeThreads.remove(t)
|
||||
let tActive: UUID? = if let index = activeThreads.firstIndex(where: { $0.0 == t }) {
|
||||
activeThreads.remove(at: index).0
|
||||
} else {
|
||||
nil
|
||||
}
|
||||
let t = allThreads.remove(t)
|
||||
if tActive != nil && activeThreads.isEmpty {
|
||||
return true
|
||||
@@ -198,8 +117,11 @@ class NotificationService: UNNotificationServiceExtension {
|
||||
// thread is added to allThreads here - if thread did not start chat,
|
||||
// chat does not need to be suspended but NSE state still needs to be set to "suspended".
|
||||
var threadId: UUID? = NSEThreads.shared.newThread()
|
||||
var notificationInfo: NtfMessages?
|
||||
var receiveEntityId: String?
|
||||
var cancelRead: (() -> Void)?
|
||||
var expectedMessages: Set<String> = []
|
||||
// return true if the message is taken - it prevents sending it to another NotificationService instance for processing
|
||||
var shouldProcessNtf = false
|
||||
var appSubscriber: AppSubscriber?
|
||||
var returnedSuspension = false
|
||||
|
||||
@@ -265,7 +187,7 @@ class NotificationService: UNNotificationServiceExtension {
|
||||
// check it here again
|
||||
appStateGroupDefault.get().inactive {
|
||||
// thread is added to activeThreads tracking set here - if thread started chat it needs to be suspended
|
||||
if let t = threadId { NSEThreads.shared.startThread(t) }
|
||||
if let t = threadId { NSEThreads.shared.startThread(t, self) }
|
||||
let dbStatus = startChat()
|
||||
if case .ok = dbStatus,
|
||||
let ntfInfo = apiGetNtfMessage(nonce: nonce, encNtfInfo: encNtfInfo) {
|
||||
@@ -276,17 +198,11 @@ class NotificationService: UNNotificationServiceExtension {
|
||||
? .nse(createConnectionEventNtf(ntfInfo.user, connEntity))
|
||||
: .empty
|
||||
)
|
||||
if let id = connEntity.id {
|
||||
if let id = connEntity.id, ntfInfo.msgTs != nil {
|
||||
notificationInfo = ntfInfo
|
||||
receiveEntityId = id
|
||||
NtfStreamSemaphores.shared.waitForStream(id)
|
||||
if receiveEntityId != nil {
|
||||
Task {
|
||||
logger.debug("NotificationService: receiveNtfMessages: in Task, connEntity id \(id)")
|
||||
await PendingNtfs.shared.createStream(id)
|
||||
await PendingNtfs.shared.readStream(id, for: self, ntfInfo: ntfInfo)
|
||||
deliverBestAttemptNtf()
|
||||
}
|
||||
}
|
||||
expectedMessages = Set(ntfInfo.ntfMessages.map { $0.msgId })
|
||||
shouldProcessNtf = true
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -302,6 +218,38 @@ class NotificationService: UNNotificationServiceExtension {
|
||||
deliverBestAttemptNtf(urgent: true)
|
||||
}
|
||||
|
||||
func processReceivedNtf(_ ntf: NSENotification) -> Bool {
|
||||
guard let ntfInfo = notificationInfo, let msgTs = ntfInfo.msgTs else { return false }
|
||||
if !ntfInfo.user.showNotifications {
|
||||
self.setBestAttemptNtf(.empty)
|
||||
}
|
||||
if case let .msgInfo(info) = ntf {
|
||||
let found = expectedMessages.remove(info.msgId)
|
||||
if found != nil {
|
||||
logger.debug("NotificationService processNtf: msgInfo, last: \(self.expectedMessages.isEmpty)")
|
||||
if expectedMessages.isEmpty {
|
||||
self.deliverBestAttemptNtf()
|
||||
}
|
||||
return true
|
||||
} else if info.msgTs > msgTs {
|
||||
logger.debug("NotificationService processNtf: unexpected msgInfo, let other instance to process it, stopping this one")
|
||||
self.deliverBestAttemptNtf()
|
||||
return false
|
||||
} else {
|
||||
logger.debug("NotificationService processNtf: unknown message, let other instance to process it")
|
||||
return false
|
||||
}
|
||||
} else if ntfInfo.user.showNotifications {
|
||||
logger.debug("NotificationService processNtf: setting best attempt")
|
||||
self.setBestAttemptNtf(ntf)
|
||||
if ntf.isCallInvitation {
|
||||
self.deliverBestAttemptNtf()
|
||||
}
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func setBadgeCount() {
|
||||
badgeCount = ntfBadgeCountGroupDefault.get() + 1
|
||||
ntfBadgeCountGroupDefault.set(badgeCount)
|
||||
@@ -323,14 +271,9 @@ class NotificationService: UNNotificationServiceExtension {
|
||||
|
||||
private func deliverBestAttemptNtf(urgent: Bool = false) {
|
||||
logger.debug("NotificationService.deliverBestAttemptNtf")
|
||||
if let cancel = cancelRead {
|
||||
cancelRead = nil
|
||||
cancel()
|
||||
}
|
||||
if let id = receiveEntityId {
|
||||
receiveEntityId = nil
|
||||
NtfStreamSemaphores.shared.signalStreamReady(id)
|
||||
}
|
||||
// stop processing other messages
|
||||
shouldProcessNtf = false
|
||||
|
||||
let suspend: Bool
|
||||
if let t = threadId {
|
||||
threadId = nil
|
||||
@@ -453,7 +396,6 @@ var receiverStarted = false
|
||||
let startLock = DispatchSemaphore(value: 1)
|
||||
let suspendLock = DispatchSemaphore(value: 1)
|
||||
var networkConfig: NetCfg = getNetCfg()
|
||||
let xftpConfig: XFTPFileConfig? = getXFTPCfg()
|
||||
|
||||
// startChat uses semaphore startLock to ensure that only one didReceive thread can start chat controller
|
||||
// Subsequent calls to didReceive will be waiting on semaphore and won't start chat again, as it will be .active
|
||||
@@ -499,7 +441,6 @@ func doStartChat() -> DBMigrationResult? {
|
||||
try setNetworkConfig(networkConfig)
|
||||
try apiSetTempFolder(tempFolder: getTempFilesDirectory().path)
|
||||
try apiSetFilesFolder(filesFolder: getAppFilesDirectory().path)
|
||||
try setXFTPConfig(xftpConfig)
|
||||
try apiSetEncryptLocalFiles(privacyEncryptLocalFilesGroupDefault.get())
|
||||
// prevent suspension while starting chat
|
||||
suspendLock.wait()
|
||||
@@ -574,7 +515,7 @@ func chatSuspended() {
|
||||
}
|
||||
|
||||
// A single loop is used per Notification service extension process to receive and process all messages depending on the NSE state
|
||||
// If the extension is not active yet, or suspended/suspending, or the app is running, the notifications will no be received.
|
||||
// If the extension is not active yet, or suspended/suspending, or the app is running, the notifications will not be received.
|
||||
func receiveMessages() async {
|
||||
logger.debug("NotificationService receiveMessages")
|
||||
while true {
|
||||
@@ -593,8 +534,7 @@ func receiveMessages() async {
|
||||
logger.debug("NotificationService receiveMsg: message")
|
||||
if let (id, ntf) = await receivedMsgNtf(msg) {
|
||||
logger.debug("NotificationService receiveMsg: notification")
|
||||
await PendingNtfs.shared.createStream(id)
|
||||
await PendingNtfs.shared.writeStream(id, ntf)
|
||||
await NSEThreads.shared.processNotification(id, ntf)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -642,10 +582,9 @@ func receivedMsgNtf(_ res: ChatResponse) async -> (String, NSENotification)? {
|
||||
cleanupDirectFile(aChatItem)
|
||||
return nil
|
||||
case let .sndFileRcvCancelled(_, aChatItem, _):
|
||||
cleanupDirectFile(aChatItem)
|
||||
return nil
|
||||
case let .sndFileCompleteXFTP(_, aChatItem, _):
|
||||
cleanupFile(aChatItem)
|
||||
if let aChatItem = aChatItem {
|
||||
cleanupDirectFile(aChatItem)
|
||||
}
|
||||
return nil
|
||||
case let .callInvitation(invitation):
|
||||
// Do not post it without CallKit support, iOS will stop launching the app without showing CallKit
|
||||
@@ -733,12 +672,6 @@ func apiSetFilesFolder(filesFolder: String) throws {
|
||||
throw r
|
||||
}
|
||||
|
||||
func setXFTPConfig(_ cfg: XFTPFileConfig?) throws {
|
||||
let r = sendSimpleXCmd(.apiSetXFTPConfig(config: cfg))
|
||||
if case .cmdOk = r { return }
|
||||
throw r
|
||||
}
|
||||
|
||||
func apiSetEncryptLocalFiles(_ enable: Bool) throws {
|
||||
let r = sendSimpleXCmd(.apiSetEncryptLocalFiles(enable: enable))
|
||||
if case .cmdOk = r { return }
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
/* Bundle display name */
|
||||
"CFBundleDisplayName" = "SimpleX NSE";
|
||||
|
||||
/* Bundle name */
|
||||
"CFBundleName" = "SimpleX NSE";
|
||||
|
||||
/* Copyright (human-readable) */
|
||||
"NSHumanReadableCopyright" = "Copyright © 2022 SimpleX Chat. Minden jog fenntartva.";
|
||||
|
||||
@@ -29,6 +29,11 @@
|
||||
5C116CDC27AABE0400E66D01 /* ContactRequestView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C116CDB27AABE0400E66D01 /* ContactRequestView.swift */; };
|
||||
5C13730B28156D2700F43030 /* ContactConnectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C13730A28156D2700F43030 /* ContactConnectionView.swift */; };
|
||||
5C1A4C1E27A715B700EAD5AD /* ChatItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C1A4C1D27A715B700EAD5AD /* ChatItemView.swift */; };
|
||||
5C2217842BD4465500A8B0E7 /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C22177F2BD4465500A8B0E7 /* libgmpxx.a */; };
|
||||
5C2217852BD4465500A8B0E7 /* libHSsimplex-chat-5.7.0.1-4ZWOoOvQtRR3xwBSWKICWy-ghc9.6.3.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C2217802BD4465500A8B0E7 /* libHSsimplex-chat-5.7.0.1-4ZWOoOvQtRR3xwBSWKICWy-ghc9.6.3.a */; };
|
||||
5C2217862BD4465500A8B0E7 /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C2217812BD4465500A8B0E7 /* libgmp.a */; };
|
||||
5C2217872BD4465500A8B0E7 /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C2217822BD4465500A8B0E7 /* libffi.a */; };
|
||||
5C2217882BD4465500A8B0E7 /* libHSsimplex-chat-5.7.0.1-4ZWOoOvQtRR3xwBSWKICWy.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C2217832BD4465500A8B0E7 /* libHSsimplex-chat-5.7.0.1-4ZWOoOvQtRR3xwBSWKICWy.a */; };
|
||||
5C2E260727A2941F00F70299 /* SimpleXAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C2E260627A2941F00F70299 /* SimpleXAPI.swift */; };
|
||||
5C2E260B27A30CFA00F70299 /* ChatListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C2E260A27A30CFA00F70299 /* ChatListView.swift */; };
|
||||
5C2E260F27A30FDC00F70299 /* ChatView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C2E260E27A30FDC00F70299 /* ChatView.swift */; };
|
||||
@@ -112,11 +117,6 @@
|
||||
5CC2C0FF2809BF11000C35E3 /* SimpleX--iOS--InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 5CC2C0FD2809BF11000C35E3 /* SimpleX--iOS--InfoPlist.strings */; };
|
||||
5CC868F329EB540C0017BBFD /* CIRcvDecryptionError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CC868F229EB540C0017BBFD /* CIRcvDecryptionError.swift */; };
|
||||
5CCB939C297EFCB100399E78 /* NavStackCompat.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CCB939B297EFCB100399E78 /* NavStackCompat.swift */; };
|
||||
5CCD2C462B5C800E00F76440 /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CCD2C412B5C800E00F76440 /* libgmpxx.a */; };
|
||||
5CCD2C472B5C800E00F76440 /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CCD2C422B5C800E00F76440 /* libgmp.a */; };
|
||||
5CCD2C482B5C800E00F76440 /* libHSsimplex-chat-5.5.0.3-3tzCJyUgrnK8CcHdWjtxcl-ghc9.6.3.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CCD2C432B5C800E00F76440 /* libHSsimplex-chat-5.5.0.3-3tzCJyUgrnK8CcHdWjtxcl-ghc9.6.3.a */; };
|
||||
5CCD2C492B5C800E00F76440 /* libHSsimplex-chat-5.5.0.3-3tzCJyUgrnK8CcHdWjtxcl.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CCD2C442B5C800E00F76440 /* libHSsimplex-chat-5.5.0.3-3tzCJyUgrnK8CcHdWjtxcl.a */; };
|
||||
5CCD2C4A2B5C800E00F76440 /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CCD2C452B5C800E00F76440 /* libffi.a */; };
|
||||
5CD67B8F2B0E858A00C510B1 /* hs_init.h in Headers */ = {isa = PBXBuildFile; fileRef = 5CD67B8D2B0E858A00C510B1 /* hs_init.h */; settings = {ATTRIBUTES = (Public, ); }; };
|
||||
5CD67B902B0E858A00C510B1 /* hs_init.c in Sources */ = {isa = PBXBuildFile; fileRef = 5CD67B8E2B0E858A00C510B1 /* hs_init.c */; };
|
||||
5CDCAD482818589900503DA2 /* NotificationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CDCAD472818589900503DA2 /* NotificationService.swift */; };
|
||||
@@ -144,7 +144,6 @@
|
||||
5CEACCED27DEA495000BD591 /* MsgContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CEACCEC27DEA495000BD591 /* MsgContentView.swift */; };
|
||||
5CEBD7462A5C0A8F00665FE2 /* KeyboardPadding.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CEBD7452A5C0A8F00665FE2 /* KeyboardPadding.swift */; };
|
||||
5CEBD7482A5F115D00665FE2 /* SetDeliveryReceiptsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CEBD7472A5F115D00665FE2 /* SetDeliveryReceiptsView.swift */; };
|
||||
5CF9371E2B23429500E1D781 /* ConcurrentQueue.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CF9371D2B23429500E1D781 /* ConcurrentQueue.swift */; };
|
||||
5CF937202B24DE8C00E1D781 /* SharedFileSubscriber.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CF9371F2B24DE8C00E1D781 /* SharedFileSubscriber.swift */; };
|
||||
5CF937232B2503D000E1D781 /* NSESubscriber.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CF937212B25034A00E1D781 /* NSESubscriber.swift */; };
|
||||
5CFA59C42860BC6200863A68 /* MigrateToAppGroupView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CFA59C32860BC6200863A68 /* MigrateToAppGroupView.swift */; };
|
||||
@@ -164,11 +163,6 @@
|
||||
64466DC829FC2B3B00E3D48D /* CreateSimpleXAddress.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64466DC729FC2B3B00E3D48D /* CreateSimpleXAddress.swift */; };
|
||||
64466DCC29FFE3E800E3D48D /* MailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64466DCB29FFE3E800E3D48D /* MailView.swift */; };
|
||||
6448BBB628FA9D56000D2AB9 /* GroupLinkView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6448BBB528FA9D56000D2AB9 /* GroupLinkView.swift */; };
|
||||
6449333A2AF8E51000AC506E /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 644933352AF8E51000AC506E /* libgmpxx.a */; };
|
||||
6449333B2AF8E51000AC506E /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 644933362AF8E51000AC506E /* libgmp.a */; };
|
||||
6449333C2AF8E51000AC506E /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 644933372AF8E51000AC506E /* libffi.a */; };
|
||||
6449333D2AF8E51000AC506E /* libHSsimplex-chat-5.4.0.3-EnhmkSQK6HvJ11g1uZERg8-ghc9.6.3.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 644933382AF8E51000AC506E /* libHSsimplex-chat-5.4.0.3-EnhmkSQK6HvJ11g1uZERg8-ghc9.6.3.a */; };
|
||||
6449333E2AF8E51000AC506E /* libHSsimplex-chat-5.4.0.3-EnhmkSQK6HvJ11g1uZERg8.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 644933392AF8E51000AC506E /* libHSsimplex-chat-5.4.0.3-EnhmkSQK6HvJ11g1uZERg8.a */; };
|
||||
644EFFDE292BCD9D00525D5B /* ComposeVoiceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 644EFFDD292BCD9D00525D5B /* ComposeVoiceView.swift */; };
|
||||
644EFFE0292CFD7F00525D5B /* CIVoiceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 644EFFDF292CFD7F00525D5B /* CIVoiceView.swift */; };
|
||||
644EFFE2292D089800525D5B /* FramedCIVoiceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 644EFFE1292D089800525D5B /* FramedCIVoiceView.swift */; };
|
||||
@@ -178,6 +172,7 @@
|
||||
646BB38E283FDB6D001CE359 /* LocalAuthenticationUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 646BB38D283FDB6D001CE359 /* LocalAuthenticationUtils.swift */; };
|
||||
647F090E288EA27B00644C40 /* GroupMemberInfoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 647F090D288EA27B00644C40 /* GroupMemberInfoView.swift */; };
|
||||
648010AB281ADD15009009B9 /* CIFileView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 648010AA281ADD15009009B9 /* CIFileView.swift */; };
|
||||
648679AB2BC96A74006456E7 /* ChatItemForwardingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 648679AA2BC96A74006456E7 /* ChatItemForwardingView.swift */; };
|
||||
649BCDA0280460FD00C3A862 /* ComposeImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 649BCD9F280460FD00C3A862 /* ComposeImageView.swift */; };
|
||||
649BCDA22805D6EF00C3A862 /* CIImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 649BCDA12805D6EF00C3A862 /* CIImageView.swift */; };
|
||||
64AA1C6927EE10C800AC7277 /* ContextItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64AA1C6827EE10C800AC7277 /* ContextItemView.swift */; };
|
||||
@@ -190,6 +185,10 @@
|
||||
64E972072881BB22008DBC02 /* CIGroupInvitationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64E972062881BB22008DBC02 /* CIGroupInvitationView.swift */; };
|
||||
64F1CC3B28B39D8600CD1FB1 /* IncognitoHelp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64F1CC3A28B39D8600CD1FB1 /* IncognitoHelp.swift */; };
|
||||
8C05382E2B39887E006436DC /* VideoUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8C05382D2B39887E006436DC /* VideoUtils.swift */; };
|
||||
8C69FE7D2B8C7D2700267E38 /* AppSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8C69FE7C2B8C7D2700267E38 /* AppSettings.swift */; };
|
||||
8C7D949A2B88952700B7B9E1 /* MigrateToDevice.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8C7D94992B88952700B7B9E1 /* MigrateToDevice.swift */; };
|
||||
8C7DF3202B7CDB0A00C886D0 /* MigrateFromDevice.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8C7DF31F2B7CDB0A00C886D0 /* MigrateFromDevice.swift */; };
|
||||
8CC956EE2BC0041000412A11 /* NetworkObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8CC956ED2BC0041000412A11 /* NetworkObserver.swift */; };
|
||||
D7197A1829AE89660055C05A /* WebRTC in Frameworks */ = {isa = PBXBuildFile; productRef = D7197A1729AE89660055C05A /* WebRTC */; };
|
||||
D72A9088294BD7A70047C86D /* NativeTextEditor.swift in Sources */ = {isa = PBXBuildFile; fileRef = D72A9087294BD7A70047C86D /* NativeTextEditor.swift */; };
|
||||
D741547829AF89AF0022400A /* StoreKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D741547729AF89AF0022400A /* StoreKit.framework */; };
|
||||
@@ -280,6 +279,11 @@
|
||||
5C13730A28156D2700F43030 /* ContactConnectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactConnectionView.swift; sourceTree = "<group>"; };
|
||||
5C13730C2815740A00F43030 /* DebugJSON.playground */ = {isa = PBXFileReference; lastKnownFileType = file.playground; path = DebugJSON.playground; sourceTree = "<group>"; xcLanguageSpecificationIdentifier = xcode.lang.swift; };
|
||||
5C1A4C1D27A715B700EAD5AD /* ChatItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatItemView.swift; sourceTree = "<group>"; };
|
||||
5C22177F2BD4465500A8B0E7 /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = "<group>"; };
|
||||
5C2217802BD4465500A8B0E7 /* libHSsimplex-chat-5.7.0.1-4ZWOoOvQtRR3xwBSWKICWy-ghc9.6.3.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-5.7.0.1-4ZWOoOvQtRR3xwBSWKICWy-ghc9.6.3.a"; sourceTree = "<group>"; };
|
||||
5C2217812BD4465500A8B0E7 /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = "<group>"; };
|
||||
5C2217822BD4465500A8B0E7 /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = "<group>"; };
|
||||
5C2217832BD4465500A8B0E7 /* libHSsimplex-chat-5.7.0.1-4ZWOoOvQtRR3xwBSWKICWy.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-5.7.0.1-4ZWOoOvQtRR3xwBSWKICWy.a"; sourceTree = "<group>"; };
|
||||
5C245F3C2B501E98001CC39F /* tr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = tr; path = tr.lproj/Localizable.strings; sourceTree = "<group>"; };
|
||||
5C245F3D2B501F13001CC39F /* tr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = tr; path = "tr.lproj/SimpleX--iOS--InfoPlist.strings"; sourceTree = "<group>"; };
|
||||
5C245F3E2B501F13001CC39F /* tr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = tr; path = tr.lproj/InfoPlist.strings; sourceTree = "<group>"; };
|
||||
@@ -290,6 +294,9 @@
|
||||
5C35CFC727B2782E00FB6C6D /* BGManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BGManager.swift; sourceTree = "<group>"; };
|
||||
5C35CFCA27B2E91D00FB6C6D /* NtfManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NtfManager.swift; sourceTree = "<group>"; };
|
||||
5C36027227F47AD5009F19D9 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
|
||||
5C371E4E2BA9AAA200100AD3 /* hu */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = hu; path = hu.lproj/Localizable.strings; sourceTree = "<group>"; };
|
||||
5C371E4F2BA9AB6400100AD3 /* hu */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = hu; path = "hu.lproj/SimpleX--iOS--InfoPlist.strings"; sourceTree = "<group>"; };
|
||||
5C371E502BA9AB6400100AD3 /* hu */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = hu; path = hu.lproj/InfoPlist.strings; sourceTree = "<group>"; };
|
||||
5C3A88CD27DF50170060F1C2 /* DetermineWidth.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DetermineWidth.swift; sourceTree = "<group>"; };
|
||||
5C3A88D027DF57800060F1C2 /* FramedItemView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FramedItemView.swift; sourceTree = "<group>"; };
|
||||
5C3CCFCB2AE6BD3100C3F0C3 /* ConnectDesktopView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ConnectDesktopView.swift; sourceTree = "<group>"; };
|
||||
@@ -403,11 +410,6 @@
|
||||
5CC2C0FE2809BF11000C35E3 /* ru */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ru; path = "ru.lproj/SimpleX--iOS--InfoPlist.strings"; sourceTree = "<group>"; };
|
||||
5CC868F229EB540C0017BBFD /* CIRcvDecryptionError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CIRcvDecryptionError.swift; sourceTree = "<group>"; };
|
||||
5CCB939B297EFCB100399E78 /* NavStackCompat.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavStackCompat.swift; sourceTree = "<group>"; };
|
||||
5CCD2C412B5C800E00F76440 /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = "<group>"; };
|
||||
5CCD2C422B5C800E00F76440 /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = "<group>"; };
|
||||
5CCD2C432B5C800E00F76440 /* libHSsimplex-chat-5.5.0.3-3tzCJyUgrnK8CcHdWjtxcl-ghc9.6.3.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-5.5.0.3-3tzCJyUgrnK8CcHdWjtxcl-ghc9.6.3.a"; sourceTree = "<group>"; };
|
||||
5CCD2C442B5C800E00F76440 /* libHSsimplex-chat-5.5.0.3-3tzCJyUgrnK8CcHdWjtxcl.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-5.5.0.3-3tzCJyUgrnK8CcHdWjtxcl.a"; sourceTree = "<group>"; };
|
||||
5CCD2C452B5C800E00F76440 /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = "<group>"; };
|
||||
5CD67B8D2B0E858A00C510B1 /* hs_init.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = hs_init.h; sourceTree = "<group>"; };
|
||||
5CD67B8E2B0E858A00C510B1 /* hs_init.c */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.c; path = hs_init.c; sourceTree = "<group>"; };
|
||||
5CDCAD452818589900503DA2 /* SimpleX NSE.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = "SimpleX NSE.appex"; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
@@ -436,7 +438,6 @@
|
||||
5CEACCEC27DEA495000BD591 /* MsgContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MsgContentView.swift; sourceTree = "<group>"; };
|
||||
5CEBD7452A5C0A8F00665FE2 /* KeyboardPadding.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyboardPadding.swift; sourceTree = "<group>"; };
|
||||
5CEBD7472A5F115D00665FE2 /* SetDeliveryReceiptsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SetDeliveryReceiptsView.swift; sourceTree = "<group>"; };
|
||||
5CF9371D2B23429500E1D781 /* ConcurrentQueue.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConcurrentQueue.swift; sourceTree = "<group>"; };
|
||||
5CF9371F2B24DE8C00E1D781 /* SharedFileSubscriber.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SharedFileSubscriber.swift; sourceTree = "<group>"; };
|
||||
5CF937212B25034A00E1D781 /* NSESubscriber.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSESubscriber.swift; sourceTree = "<group>"; };
|
||||
5CFA59C32860BC6200863A68 /* MigrateToAppGroupView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MigrateToAppGroupView.swift; sourceTree = "<group>"; };
|
||||
@@ -455,11 +456,6 @@
|
||||
64466DC729FC2B3B00E3D48D /* CreateSimpleXAddress.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateSimpleXAddress.swift; sourceTree = "<group>"; };
|
||||
64466DCB29FFE3E800E3D48D /* MailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MailView.swift; sourceTree = "<group>"; };
|
||||
6448BBB528FA9D56000D2AB9 /* GroupLinkView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupLinkView.swift; sourceTree = "<group>"; };
|
||||
644933352AF8E51000AC506E /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = "<group>"; };
|
||||
644933362AF8E51000AC506E /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = "<group>"; };
|
||||
644933372AF8E51000AC506E /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = "<group>"; };
|
||||
644933382AF8E51000AC506E /* libHSsimplex-chat-5.4.0.3-EnhmkSQK6HvJ11g1uZERg8-ghc9.6.3.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-5.4.0.3-EnhmkSQK6HvJ11g1uZERg8-ghc9.6.3.a"; sourceTree = "<group>"; };
|
||||
644933392AF8E51000AC506E /* libHSsimplex-chat-5.4.0.3-EnhmkSQK6HvJ11g1uZERg8.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-5.4.0.3-EnhmkSQK6HvJ11g1uZERg8.a"; sourceTree = "<group>"; };
|
||||
644EFFDD292BCD9D00525D5B /* ComposeVoiceView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeVoiceView.swift; sourceTree = "<group>"; };
|
||||
644EFFDF292CFD7F00525D5B /* CIVoiceView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CIVoiceView.swift; sourceTree = "<group>"; };
|
||||
644EFFE1292D089800525D5B /* FramedCIVoiceView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FramedCIVoiceView.swift; sourceTree = "<group>"; };
|
||||
@@ -469,6 +465,7 @@
|
||||
646BB38D283FDB6D001CE359 /* LocalAuthenticationUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalAuthenticationUtils.swift; sourceTree = "<group>"; };
|
||||
647F090D288EA27B00644C40 /* GroupMemberInfoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupMemberInfoView.swift; sourceTree = "<group>"; };
|
||||
648010AA281ADD15009009B9 /* CIFileView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CIFileView.swift; sourceTree = "<group>"; };
|
||||
648679AA2BC96A74006456E7 /* ChatItemForwardingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatItemForwardingView.swift; sourceTree = "<group>"; };
|
||||
6493D667280ED77F007A76FB /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = "<group>"; };
|
||||
649BCD9F280460FD00C3A862 /* ComposeImageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeImageView.swift; sourceTree = "<group>"; };
|
||||
649BCDA12805D6EF00C3A862 /* CIImageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CIImageView.swift; sourceTree = "<group>"; };
|
||||
@@ -483,6 +480,10 @@
|
||||
64E972062881BB22008DBC02 /* CIGroupInvitationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CIGroupInvitationView.swift; sourceTree = "<group>"; };
|
||||
64F1CC3A28B39D8600CD1FB1 /* IncognitoHelp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IncognitoHelp.swift; sourceTree = "<group>"; };
|
||||
8C05382D2B39887E006436DC /* VideoUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoUtils.swift; sourceTree = "<group>"; };
|
||||
8C69FE7C2B8C7D2700267E38 /* AppSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppSettings.swift; sourceTree = "<group>"; };
|
||||
8C7D94992B88952700B7B9E1 /* MigrateToDevice.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MigrateToDevice.swift; sourceTree = "<group>"; };
|
||||
8C7DF31F2B7CDB0A00C886D0 /* MigrateFromDevice.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MigrateFromDevice.swift; sourceTree = "<group>"; };
|
||||
8CC956ED2BC0041000412A11 /* NetworkObserver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkObserver.swift; sourceTree = "<group>"; };
|
||||
D72A9087294BD7A70047C86D /* NativeTextEditor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NativeTextEditor.swift; sourceTree = "<group>"; };
|
||||
D741547729AF89AF0022400A /* StoreKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = StoreKit.framework; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS16.1.sdk/System/Library/Frameworks/StoreKit.framework; sourceTree = DEVELOPER_DIR; };
|
||||
D741547929AF90B00022400A /* PushKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = PushKit.framework; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS16.1.sdk/System/Library/Frameworks/PushKit.framework; sourceTree = DEVELOPER_DIR; };
|
||||
@@ -524,13 +525,13 @@
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
5CCD2C462B5C800E00F76440 /* libgmpxx.a in Frameworks */,
|
||||
5CCD2C492B5C800E00F76440 /* libHSsimplex-chat-5.5.0.3-3tzCJyUgrnK8CcHdWjtxcl.a in Frameworks */,
|
||||
5CCD2C482B5C800E00F76440 /* libHSsimplex-chat-5.5.0.3-3tzCJyUgrnK8CcHdWjtxcl-ghc9.6.3.a in Frameworks */,
|
||||
5C2217842BD4465500A8B0E7 /* libgmpxx.a in Frameworks */,
|
||||
5C2217872BD4465500A8B0E7 /* libffi.a in Frameworks */,
|
||||
5C2217862BD4465500A8B0E7 /* libgmp.a in Frameworks */,
|
||||
5C2217882BD4465500A8B0E7 /* libHSsimplex-chat-5.7.0.1-4ZWOoOvQtRR3xwBSWKICWy.a in Frameworks */,
|
||||
5CE2BA93284534B000EC33A6 /* libiconv.tbd in Frameworks */,
|
||||
5CCD2C4A2B5C800E00F76440 /* libffi.a in Frameworks */,
|
||||
5CCD2C472B5C800E00F76440 /* libgmp.a in Frameworks */,
|
||||
5CE2BA94284534BB00EC33A6 /* libz.tbd in Frameworks */,
|
||||
5C2217852BD4465500A8B0E7 /* libHSsimplex-chat-5.7.0.1-4ZWOoOvQtRR3xwBSWKICWy-ghc9.6.3.a in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
@@ -563,6 +564,7 @@
|
||||
5CB924DD27A8622200ACCCDD /* NewChat */,
|
||||
5CFA59C22860B04D00863A68 /* Database */,
|
||||
5CB634AB29E46CDB0066AD6B /* LocalAuth */,
|
||||
8C7D94982B8894D300B7B9E1 /* Migration */,
|
||||
5CA8D01B2AD9B076001FD661 /* RemoteAccess */,
|
||||
5CB924DF27A8678B00ACCCDD /* UserSettings */,
|
||||
5C2E261127A30FEA00F70299 /* TerminalView.swift */,
|
||||
@@ -585,6 +587,7 @@
|
||||
5CBE6C11294487F7002D9531 /* VerifyCodeView.swift */,
|
||||
5CBE6C132944CC12002D9531 /* ScanCodeView.swift */,
|
||||
64C06EB42A0A4A7C00792D4D /* ChatItemInfoView.swift */,
|
||||
648679AA2BC96A74006456E7 /* ChatItemForwardingView.swift */,
|
||||
);
|
||||
path = Chat;
|
||||
sourceTree = "<group>";
|
||||
@@ -592,11 +595,11 @@
|
||||
5C764E5C279C70B7000C6508 /* Libraries */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
5CCD2C452B5C800E00F76440 /* libffi.a */,
|
||||
5CCD2C422B5C800E00F76440 /* libgmp.a */,
|
||||
5CCD2C412B5C800E00F76440 /* libgmpxx.a */,
|
||||
5CCD2C432B5C800E00F76440 /* libHSsimplex-chat-5.5.0.3-3tzCJyUgrnK8CcHdWjtxcl-ghc9.6.3.a */,
|
||||
5CCD2C442B5C800E00F76440 /* libHSsimplex-chat-5.5.0.3-3tzCJyUgrnK8CcHdWjtxcl.a */,
|
||||
5C2217822BD4465500A8B0E7 /* libffi.a */,
|
||||
5C2217812BD4465500A8B0E7 /* libgmp.a */,
|
||||
5C22177F2BD4465500A8B0E7 /* libgmpxx.a */,
|
||||
5C2217802BD4465500A8B0E7 /* libHSsimplex-chat-5.7.0.1-4ZWOoOvQtRR3xwBSWKICWy-ghc9.6.3.a */,
|
||||
5C2217832BD4465500A8B0E7 /* libHSsimplex-chat-5.7.0.1-4ZWOoOvQtRR3xwBSWKICWy.a */,
|
||||
);
|
||||
path = Libraries;
|
||||
sourceTree = "<group>";
|
||||
@@ -625,6 +628,7 @@
|
||||
5CB346E82869E8BA001FD2EF /* PushEnvironment.swift */,
|
||||
5C93293E2928E0FD0090FFF9 /* AudioRecPlay.swift */,
|
||||
5CBD2859295711D700EC2CF4 /* ImageUtils.swift */,
|
||||
8CC956ED2BC0041000412A11 /* NetworkObserver.swift */,
|
||||
);
|
||||
path = Model;
|
||||
sourceTree = "<group>";
|
||||
@@ -776,6 +780,7 @@
|
||||
64D0C2BF29F9688300B38D5F /* UserAddressView.swift */,
|
||||
64D0C2C129FA57AB00B38D5F /* UserAddressLearnMore.swift */,
|
||||
5CEBD7472A5F115D00665FE2 /* SetDeliveryReceiptsView.swift */,
|
||||
8C69FE7C2B8C7D2700267E38 /* AppSettings.swift */,
|
||||
);
|
||||
path = UserSettings;
|
||||
sourceTree = "<group>";
|
||||
@@ -799,7 +804,6 @@
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
5CDCAD5128186DE400503DA2 /* SimpleX NSE.entitlements */,
|
||||
5CF9371D2B23429500E1D781 /* ConcurrentQueue.swift */,
|
||||
5CDCAD472818589900503DA2 /* NotificationService.swift */,
|
||||
5CDCAD492818589900503DA2 /* Info.plist */,
|
||||
5CB0BA862826CB3A00B3292C /* InfoPlist.strings */,
|
||||
@@ -903,6 +907,15 @@
|
||||
path = Group;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
8C7D94982B8894D300B7B9E1 /* Migration */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
8C7DF31F2B7CDB0A00C886D0 /* MigrateFromDevice.swift */,
|
||||
8C7D94992B88952700B7B9E1 /* MigrateToDevice.swift */,
|
||||
);
|
||||
path = Migration;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
/* End PBXGroup section */
|
||||
|
||||
/* Begin PBXHeadersBuildPhase section */
|
||||
@@ -1053,6 +1066,7 @@
|
||||
uk,
|
||||
bg,
|
||||
tr,
|
||||
hu,
|
||||
);
|
||||
mainGroup = 5CA059BD279559F40002BEB4;
|
||||
packageReferences = (
|
||||
@@ -1134,6 +1148,7 @@
|
||||
5CBD285A295711D700EC2CF4 /* ImageUtils.swift in Sources */,
|
||||
6419EC562AB8BC8B004A607A /* ContextInvitingContactMemberView.swift in Sources */,
|
||||
5CE4407927ADB701007B033A /* EmojiItemView.swift in Sources */,
|
||||
8C7D949A2B88952700B7B9E1 /* MigrateToDevice.swift in Sources */,
|
||||
5C3F1D562842B68D00EC8A82 /* IntegrityErrorItemView.swift in Sources */,
|
||||
5C029EAA283942EA004A9677 /* CallController.swift in Sources */,
|
||||
5CBE6C142944CC12002D9531 /* ScanCodeView.swift in Sources */,
|
||||
@@ -1149,6 +1164,7 @@
|
||||
5C10D88828EED12E00E58BF0 /* ContactConnectionInfo.swift in Sources */,
|
||||
5CBE6C12294487F7002D9531 /* VerifyCodeView.swift in Sources */,
|
||||
3CDBCF4227FAE51000354CDD /* ComposeLinkView.swift in Sources */,
|
||||
648679AB2BC96A74006456E7 /* ChatItemForwardingView.swift in Sources */,
|
||||
64466DC829FC2B3B00E3D48D /* CreateSimpleXAddress.swift in Sources */,
|
||||
3CDBCF4827FF621E00354CDD /* CILinkView.swift in Sources */,
|
||||
5C7505A827B6D34800BE3227 /* ChatInfoToolbar.swift in Sources */,
|
||||
@@ -1167,6 +1183,7 @@
|
||||
5C9A5BDB2871E05400A5B906 /* SetNotificationsMode.swift in Sources */,
|
||||
5CB0BA8E2827126500B3292C /* OnboardingView.swift in Sources */,
|
||||
6442E0BE2880182D00CEC0F9 /* GroupChatInfoView.swift in Sources */,
|
||||
8CC956EE2BC0041000412A11 /* NetworkObserver.swift in Sources */,
|
||||
5C2E261227A30FEA00F70299 /* TerminalView.swift in Sources */,
|
||||
5C9FD96E27A5D6ED0075386C /* SendMessageView.swift in Sources */,
|
||||
5CA7DFC329302AF000F7FDDE /* AppSheet.swift in Sources */,
|
||||
@@ -1189,6 +1206,7 @@
|
||||
5CEBD7462A5C0A8F00665FE2 /* KeyboardPadding.swift in Sources */,
|
||||
5C35CFC827B2782E00FB6C6D /* BGManager.swift in Sources */,
|
||||
5CB634B129E5EFEA0066AD6B /* PasscodeView.swift in Sources */,
|
||||
8C69FE7D2B8C7D2700267E38 /* AppSettings.swift in Sources */,
|
||||
5C2E260F27A30FDC00F70299 /* ChatView.swift in Sources */,
|
||||
5C2E260B27A30CFA00F70299 /* ChatListView.swift in Sources */,
|
||||
6442E0BA287F169300CEC0F9 /* AddGroupView.swift in Sources */,
|
||||
@@ -1230,6 +1248,7 @@
|
||||
5CB0BA92282713FD00B3292C /* CreateProfile.swift in Sources */,
|
||||
5C5F2B7027EBC704006A9D5F /* ProfileImage.swift in Sources */,
|
||||
5C9329412929248A0090FFF9 /* ScanProtocolServer.swift in Sources */,
|
||||
8C7DF3202B7CDB0A00C886D0 /* MigrateFromDevice.swift in Sources */,
|
||||
64AA1C6C27F3537400AC7277 /* DeletedItemView.swift in Sources */,
|
||||
5C93293F2928E0FD0090FFF9 /* AudioRecPlay.swift in Sources */,
|
||||
5C029EA82837DBB3004A9677 /* CICallItemView.swift in Sources */,
|
||||
@@ -1271,7 +1290,6 @@
|
||||
files = (
|
||||
5CDCAD482818589900503DA2 /* NotificationService.swift in Sources */,
|
||||
5CFE0922282EEAF60002594B /* ZoomableScrollView.swift in Sources */,
|
||||
5CF9371E2B23429500E1D781 /* ConcurrentQueue.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
@@ -1341,6 +1359,7 @@
|
||||
5C636F672AAB3D2400751C84 /* uk */,
|
||||
5C5B67932ABAF56000DA9412 /* bg */,
|
||||
5C245F3E2B501F13001CC39F /* tr */,
|
||||
5C371E502BA9AB6400100AD3 /* hu */,
|
||||
);
|
||||
name = InfoPlist.strings;
|
||||
sourceTree = "<group>";
|
||||
@@ -1364,6 +1383,7 @@
|
||||
5CE6C7B42AAB1527007F345C /* uk */,
|
||||
5C5B67912ABAF4B500DA9412 /* bg */,
|
||||
5C245F3C2B501E98001CC39F /* tr */,
|
||||
5C371E4E2BA9AAA200100AD3 /* hu */,
|
||||
);
|
||||
name = Localizable.strings;
|
||||
sourceTree = "<group>";
|
||||
@@ -1386,6 +1406,7 @@
|
||||
5C636F662AAB3D2400751C84 /* uk */,
|
||||
5C5B67922ABAF56000DA9412 /* bg */,
|
||||
5C245F3D2B501F13001CC39F /* tr */,
|
||||
5C371E4F2BA9AB6400100AD3 /* hu */,
|
||||
);
|
||||
name = "SimpleX--iOS--InfoPlist.strings";
|
||||
sourceTree = "<group>";
|
||||
@@ -1428,6 +1449,7 @@
|
||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
COPY_PHASE_STRIP = NO;
|
||||
DEAD_CODE_STRIPPING = YES;
|
||||
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
ENABLE_TESTABILITY = YES;
|
||||
@@ -1489,6 +1511,7 @@
|
||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
COPY_PHASE_STRIP = NO;
|
||||
DEAD_CODE_STRIPPING = YES;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
ENABLE_NS_ASSERTIONS = NO;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
@@ -1517,12 +1540,16 @@
|
||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||
ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = YES;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CLANG_TIDY_BUGPRONE_REDUNDANT_BRANCH_CONDITION = YES;
|
||||
CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 191;
|
||||
CURRENT_PROJECT_VERSION = 208;
|
||||
DEAD_CODE_STRIPPING = YES;
|
||||
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
||||
ENABLE_BITCODE = NO;
|
||||
ENABLE_PREVIEWS = YES;
|
||||
GCC_OPTIMIZATION_LEVEL = s;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_FILE = "SimpleX--iOS--Info.plist";
|
||||
INFOPLIST_KEY_NSCameraUsageDescription = "SimpleX needs camera access to scan QR codes to connect to other users and for video calls.";
|
||||
@@ -1541,11 +1568,13 @@
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 5.5;
|
||||
LLVM_LTO = YES_THIN;
|
||||
MARKETING_VERSION = 5.7;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.app;
|
||||
PRODUCT_NAME = SimpleX;
|
||||
SDKROOT = iphoneos;
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-O";
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = 1;
|
||||
};
|
||||
@@ -1560,12 +1589,16 @@
|
||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||
ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = YES;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CLANG_TIDY_BUGPRONE_REDUNDANT_BRANCH_CONDITION = YES;
|
||||
CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 191;
|
||||
CURRENT_PROJECT_VERSION = 208;
|
||||
DEAD_CODE_STRIPPING = YES;
|
||||
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
||||
ENABLE_BITCODE = NO;
|
||||
ENABLE_PREVIEWS = YES;
|
||||
ENABLE_CODE_COVERAGE = NO;
|
||||
ENABLE_PREVIEWS = NO;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_FILE = "SimpleX--iOS--Info.plist";
|
||||
INFOPLIST_KEY_NSCameraUsageDescription = "SimpleX needs camera access to scan QR codes to connect to other users and for video calls.";
|
||||
@@ -1584,7 +1617,8 @@
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 5.5;
|
||||
LLVM_LTO = YES;
|
||||
MARKETING_VERSION = 5.7;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.app;
|
||||
PRODUCT_NAME = SimpleX;
|
||||
SDKROOT = iphoneos;
|
||||
@@ -1640,12 +1674,15 @@
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CLANG_TIDY_BUGPRONE_REDUNDANT_BRANCH_CONDITION = YES;
|
||||
CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = "SimpleX NSE/SimpleX NSE.entitlements";
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 191;
|
||||
CURRENT_PROJECT_VERSION = 208;
|
||||
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
||||
ENABLE_BITCODE = NO;
|
||||
GCC_OPTIMIZATION_LEVEL = s;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_FILE = "SimpleX NSE/Info.plist";
|
||||
INFOPLIST_KEY_CFBundleDisplayName = "SimpleX NSE";
|
||||
@@ -1656,13 +1693,15 @@
|
||||
"@executable_path/Frameworks",
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 5.5;
|
||||
LLVM_LTO = YES;
|
||||
MARKETING_VERSION = 5.7;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.app.SimpleX-NSE";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
SDKROOT = iphoneos;
|
||||
SKIP_INSTALL = YES;
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Osize";
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = 1;
|
||||
};
|
||||
@@ -1672,12 +1711,15 @@
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CLANG_TIDY_BUGPRONE_REDUNDANT_BRANCH_CONDITION = YES;
|
||||
CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = "SimpleX NSE/SimpleX NSE.entitlements";
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 191;
|
||||
CURRENT_PROJECT_VERSION = 208;
|
||||
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
||||
ENABLE_BITCODE = NO;
|
||||
ENABLE_CODE_COVERAGE = NO;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_FILE = "SimpleX NSE/Info.plist";
|
||||
INFOPLIST_KEY_CFBundleDisplayName = "SimpleX NSE";
|
||||
@@ -1688,13 +1730,15 @@
|
||||
"@executable_path/Frameworks",
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 5.5;
|
||||
LLVM_LTO = YES;
|
||||
MARKETING_VERSION = 5.7;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.app.SimpleX-NSE";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
SDKROOT = iphoneos;
|
||||
SKIP_INSTALL = YES;
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Osize";
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = 1;
|
||||
VALIDATE_PRODUCT = YES;
|
||||
@@ -1706,14 +1750,17 @@
|
||||
buildSettings = {
|
||||
APPLICATION_EXTENSION_API_ONLY = YES;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CLANG_TIDY_BUGPRONE_REDUNDANT_BRANCH_CONDITION = YES;
|
||||
CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 191;
|
||||
CURRENT_PROJECT_VERSION = 208;
|
||||
DEFINES_MODULE = YES;
|
||||
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
||||
DYLIB_COMPATIBILITY_VERSION = 1;
|
||||
DYLIB_CURRENT_VERSION = 1;
|
||||
DYLIB_INSTALL_NAME_BASE = "@rpath";
|
||||
ENABLE_BITCODE = NO;
|
||||
GCC_OPTIMIZATION_LEVEL = s;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2022 SimpleX Chat. All rights reserved.";
|
||||
INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
|
||||
@@ -1731,7 +1778,8 @@
|
||||
"$(inherited)",
|
||||
"$(PROJECT_DIR)/Libraries/sim",
|
||||
);
|
||||
MARKETING_VERSION = 5.5;
|
||||
LLVM_LTO = YES;
|
||||
MARKETING_VERSION = 5.7;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.SimpleXChat;
|
||||
PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
|
||||
SDKROOT = iphoneos;
|
||||
@@ -1740,6 +1788,7 @@
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
SWIFT_INCLUDE_PATHS = "";
|
||||
SWIFT_OBJC_BRIDGING_HEADER = ./SimpleXChat/SimpleX.h;
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-O";
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
VERSIONING_SYSTEM = "apple-generic";
|
||||
@@ -1752,14 +1801,17 @@
|
||||
buildSettings = {
|
||||
APPLICATION_EXTENSION_API_ONLY = YES;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CLANG_TIDY_BUGPRONE_REDUNDANT_BRANCH_CONDITION = YES;
|
||||
CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 191;
|
||||
CURRENT_PROJECT_VERSION = 208;
|
||||
DEFINES_MODULE = YES;
|
||||
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
||||
DYLIB_COMPATIBILITY_VERSION = 1;
|
||||
DYLIB_CURRENT_VERSION = 1;
|
||||
DYLIB_INSTALL_NAME_BASE = "@rpath";
|
||||
ENABLE_BITCODE = NO;
|
||||
ENABLE_CODE_COVERAGE = NO;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2022 SimpleX Chat. All rights reserved.";
|
||||
INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
|
||||
@@ -1777,7 +1829,8 @@
|
||||
"$(inherited)",
|
||||
"$(PROJECT_DIR)/Libraries/sim",
|
||||
);
|
||||
MARKETING_VERSION = 5.5;
|
||||
LLVM_LTO = YES;
|
||||
MARKETING_VERSION = 5.7;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.SimpleXChat;
|
||||
PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
|
||||
SDKROOT = iphoneos;
|
||||
@@ -1786,6 +1839,7 @@
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
SWIFT_INCLUDE_PATHS = "";
|
||||
SWIFT_OBJC_BRIDGING_HEADER = ./SimpleXChat/SimpleX.h;
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-O";
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
VALIDATE_PRODUCT = YES;
|
||||
|
||||
@@ -54,7 +54,44 @@ public func chatMigrateInit(_ useKey: String? = nil, confirmMigrations: Migratio
|
||||
return result
|
||||
}
|
||||
|
||||
public func chatInitTemporaryDatabase(url: URL, key: String? = nil, confirmation: MigrationConfirmation = .error) -> (DBMigrationResult, chat_ctrl?) {
|
||||
let dbPath = url.path
|
||||
let dbKey = key ?? randomDatabasePassword()
|
||||
logger.debug("chatInitTemporaryDatabase path: \(dbPath)")
|
||||
var temporaryController: chat_ctrl? = nil
|
||||
var cPath = dbPath.cString(using: .utf8)!
|
||||
var cKey = dbKey.cString(using: .utf8)!
|
||||
var cConfirm = confirmation.rawValue.cString(using: .utf8)!
|
||||
let cjson = chat_migrate_init_key(&cPath, &cKey, 1, &cConfirm, 0, &temporaryController)!
|
||||
return (dbMigrationResult(fromCString(cjson)), temporaryController)
|
||||
}
|
||||
|
||||
public func chatInitControllerRemovingDatabases() {
|
||||
let dbPath = getAppDatabasePath().path
|
||||
let fm = FileManager.default
|
||||
// Remove previous databases, otherwise, can be .errorNotADatabase with nil controller
|
||||
try? fm.removeItem(atPath: dbPath + CHAT_DB)
|
||||
try? fm.removeItem(atPath: dbPath + AGENT_DB)
|
||||
|
||||
let dbKey = randomDatabasePassword()
|
||||
logger.debug("chatInitControllerRemovingDatabases path: \(dbPath)")
|
||||
var cPath = dbPath.cString(using: .utf8)!
|
||||
var cKey = dbKey.cString(using: .utf8)!
|
||||
var cConfirm = MigrationConfirmation.error.rawValue.cString(using: .utf8)!
|
||||
chat_migrate_init_key(&cPath, &cKey, 1, &cConfirm, 0, &chatController)
|
||||
|
||||
// We need only controller, not databases
|
||||
try? fm.removeItem(atPath: dbPath + CHAT_DB)
|
||||
try? fm.removeItem(atPath: dbPath + AGENT_DB)
|
||||
}
|
||||
|
||||
|
||||
public func chatCloseStore() {
|
||||
// Prevent crash when exiting the app with already closed store (for example, after changing a database passpharase)
|
||||
guard hasChatCtrl() else {
|
||||
logger.error("chatCloseStore: already closed, chatCtrl is nil")
|
||||
return
|
||||
}
|
||||
let err = fromCString(chat_close_store(getChatCtrl()))
|
||||
if err != "" {
|
||||
logger.error("chatCloseStore error: \(err)")
|
||||
@@ -73,17 +110,17 @@ public func resetChatCtrl() {
|
||||
migrationResult = nil
|
||||
}
|
||||
|
||||
public func sendSimpleXCmd(_ cmd: ChatCommand) -> ChatResponse {
|
||||
public func sendSimpleXCmd(_ cmd: ChatCommand, _ ctrl: chat_ctrl? = nil) -> ChatResponse {
|
||||
var c = cmd.cmdString.cString(using: .utf8)!
|
||||
let cjson = chat_send_cmd(getChatCtrl(), &c)!
|
||||
let cjson = chat_send_cmd(ctrl ?? getChatCtrl(), &c)!
|
||||
return chatResponse(fromCString(cjson))
|
||||
}
|
||||
|
||||
// in microseconds
|
||||
let MESSAGE_TIMEOUT: Int32 = 15_000_000
|
||||
|
||||
public func recvSimpleXMsg() -> ChatResponse? {
|
||||
if let cjson = chat_recv_msg_wait(getChatCtrl(), MESSAGE_TIMEOUT) {
|
||||
public func recvSimpleXMsg(_ ctrl: chat_ctrl? = nil) -> ChatResponse? {
|
||||
if let cjson = chat_recv_msg_wait(ctrl ?? getChatCtrl(), MESSAGE_TIMEOUT) {
|
||||
let s = fromCString(cjson)
|
||||
return s == "" ? nil : chatResponse(s)
|
||||
}
|
||||
@@ -105,6 +142,11 @@ public func parseSimpleXMarkdown(_ s: String) -> [FormattedText]? {
|
||||
return nil
|
||||
}
|
||||
|
||||
public func chatJsonLength(_ s: String) -> Int {
|
||||
var c = s.cString(using: .utf8)!
|
||||
return Int(chat_json_length(&c))
|
||||
}
|
||||
|
||||
struct ParsedMarkdown: Decodable {
|
||||
var formattedText: [FormattedText]?
|
||||
}
|
||||
@@ -176,13 +218,11 @@ public func chatResponse(_ s: String) -> ChatResponse {
|
||||
}
|
||||
} else if type == "chatCmdError" {
|
||||
if let jError = jResp["chatCmdError"] as? NSDictionary {
|
||||
let user: UserRef? = try? decodeObject(jError["user_"] as Any)
|
||||
return .chatCmdError(user_: user, chatError: .invalidJSON(json: prettyJSON(jError) ?? ""))
|
||||
return .chatCmdError(user_: decodeUser_(jError), chatError: .invalidJSON(json: prettyJSON(jError) ?? ""))
|
||||
}
|
||||
} else if type == "chatError" {
|
||||
if let jError = jResp["chatError"] as? NSDictionary {
|
||||
let user: UserRef? = try? decodeObject(jError["user_"] as Any)
|
||||
return .chatError(user_: user, chatError: .invalidJSON(json: prettyJSON(jError) ?? ""))
|
||||
return .chatError(user_: decodeUser_(jError), chatError: .invalidJSON(json: prettyJSON(jError) ?? ""))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -191,6 +231,14 @@ public func chatResponse(_ s: String) -> ChatResponse {
|
||||
return ChatResponse.response(type: type ?? "invalid", json: json ?? s)
|
||||
}
|
||||
|
||||
private func decodeUser_(_ jDict: NSDictionary) -> UserRef? {
|
||||
if let user_ = jDict["user_"] {
|
||||
try? decodeObject(user_ as Any)
|
||||
} else {
|
||||
nil
|
||||
}
|
||||
}
|
||||
|
||||
func parseChatData(_ jChat: Any) throws -> ChatData {
|
||||
let jChatDict = jChat as! NSDictionary
|
||||
let chatInfo: ChatInfo = try decodeObject(jChatDict["chatInfo"]!)
|
||||
|
||||
@@ -31,12 +31,16 @@ public enum ChatCommand {
|
||||
case apiSuspendChat(timeoutMicroseconds: Int)
|
||||
case setTempFolder(tempFolder: String)
|
||||
case setFilesFolder(filesFolder: String)
|
||||
case apiSetXFTPConfig(config: XFTPFileConfig?)
|
||||
case apiSetEncryptLocalFiles(enable: Bool)
|
||||
case apiSetPQEncryption(enable: Bool)
|
||||
case apiSetContactPQ(contactId: Int64, enable: Bool)
|
||||
case apiExportArchive(config: ArchiveConfig)
|
||||
case apiImportArchive(config: ArchiveConfig)
|
||||
case apiDeleteStorage
|
||||
case apiStorageEncryption(config: DBEncryptionConfig)
|
||||
case testStorageEncryption(key: String)
|
||||
case apiSaveSettings(settings: AppSettings)
|
||||
case apiGetSettings(settings: AppSettings)
|
||||
case apiGetChats(userId: Int64)
|
||||
case apiGetChat(type: ChatType, id: Int64, pagination: ChatPagination, search: String)
|
||||
case apiGetChatItemInfo(type: ChatType, id: Int64, itemId: Int64)
|
||||
@@ -46,6 +50,7 @@ public enum ChatCommand {
|
||||
case apiDeleteChatItem(type: ChatType, id: Int64, itemId: Int64, mode: CIDeleteMode)
|
||||
case apiDeleteMemberChatItem(groupId: Int64, groupMemberId: Int64, itemId: Int64)
|
||||
case apiChatItemReaction(type: ChatType, id: Int64, itemId: Int64, add: Bool, reaction: MsgReaction)
|
||||
case apiForwardChatItem(toChatType: ChatType, toChatId: Int64, fromChatType: ChatType, fromChatId: Int64, itemId: Int64)
|
||||
case apiGetNtfToken
|
||||
case apiRegisterToken(token: DeviceToken, notificationMode: NotificationsMode)
|
||||
case apiVerifyToken(token: DeviceToken, nonce: String, code: String)
|
||||
@@ -73,6 +78,7 @@ public enum ChatCommand {
|
||||
case apiGetChatItemTTL(userId: Int64)
|
||||
case apiSetNetworkConfig(networkConfig: NetCfg)
|
||||
case apiGetNetworkConfig
|
||||
case apiSetNetworkInfo(networkInfo: UserNetworkInfo)
|
||||
case reconnectAllServers
|
||||
case apiSetChatSettings(type: ChatType, id: Int64, chatSettings: ChatSettings)
|
||||
case apiSetMemberSettings(groupId: Int64, groupMemberId: Int64, memberSettings: GroupMemberSettings)
|
||||
@@ -131,6 +137,9 @@ public enum ChatCommand {
|
||||
case listRemoteCtrls
|
||||
case stopRemoteCtrl
|
||||
case deleteRemoteCtrl(remoteCtrlId: Int64)
|
||||
case apiUploadStandaloneFile(userId: Int64, file: CryptoFile)
|
||||
case apiDownloadStandaloneFile(userId: Int64, url: String, file: CryptoFile)
|
||||
case apiStandaloneFileInfo(url: String)
|
||||
// misc
|
||||
case showVersion
|
||||
case string(String)
|
||||
@@ -162,16 +171,16 @@ public enum ChatCommand {
|
||||
case let .apiSuspendChat(timeoutMicroseconds): return "/_app suspend \(timeoutMicroseconds)"
|
||||
case let .setTempFolder(tempFolder): return "/_temp_folder \(tempFolder)"
|
||||
case let .setFilesFolder(filesFolder): return "/_files_folder \(filesFolder)"
|
||||
case let .apiSetXFTPConfig(cfg): if let cfg = cfg {
|
||||
return "/_xftp on \(encodeJSON(cfg))"
|
||||
} else {
|
||||
return "/_xftp off"
|
||||
}
|
||||
case let .apiSetEncryptLocalFiles(enable): return "/_files_encrypt \(onOff(enable))"
|
||||
case let .apiSetPQEncryption(enable): return "/pq \(onOff(enable))"
|
||||
case let .apiSetContactPQ(contactId, enable): return "/_pq @\(contactId) \(onOff(enable))"
|
||||
case let .apiExportArchive(cfg): return "/_db export \(encodeJSON(cfg))"
|
||||
case let .apiImportArchive(cfg): return "/_db import \(encodeJSON(cfg))"
|
||||
case .apiDeleteStorage: return "/_db delete"
|
||||
case let .apiStorageEncryption(cfg): return "/_db encryption \(encodeJSON(cfg))"
|
||||
case let .testStorageEncryption(key): return "/db test key \(key)"
|
||||
case let .apiSaveSettings(settings): return "/_save app settings \(encodeJSON(settings))"
|
||||
case let .apiGetSettings(settings): return "/_get app settings \(encodeJSON(settings))"
|
||||
case let .apiGetChats(userId): return "/_get chats \(userId) pcc=on"
|
||||
case let .apiGetChat(type, id, pagination, search): return "/_get chat \(ref(type, id)) \(pagination.cmdString)" +
|
||||
(search == "" ? "" : " search=\(search)")
|
||||
@@ -187,6 +196,7 @@ public enum ChatCommand {
|
||||
case let .apiDeleteChatItem(type, id, itemId, mode): return "/_delete item \(ref(type, id)) \(itemId) \(mode.rawValue)"
|
||||
case let .apiDeleteMemberChatItem(groupId, groupMemberId, itemId): return "/_delete member item #\(groupId) \(groupMemberId) \(itemId)"
|
||||
case let .apiChatItemReaction(type, id, itemId, add, reaction): return "/_reaction \(ref(type, id)) \(itemId) \(onOff(add)) \(encodeJSON(reaction))"
|
||||
case let .apiForwardChatItem(toChatType, toChatId, fromChatType, fromChatId, itemId): return "/_forward \(ref(toChatType, toChatId)) \(ref(fromChatType, fromChatId)) \(itemId)"
|
||||
case .apiGetNtfToken: return "/_ntf get "
|
||||
case let .apiRegisterToken(token, notificationMode): return "/_ntf register \(token.cmdString) \(notificationMode.rawValue)"
|
||||
case let .apiVerifyToken(token, nonce, code): return "/_ntf verify \(token.cmdString) \(nonce) \(code)"
|
||||
@@ -214,6 +224,7 @@ public enum ChatCommand {
|
||||
case let .apiGetChatItemTTL(userId): return "/_ttl \(userId)"
|
||||
case let .apiSetNetworkConfig(networkConfig): return "/_network \(encodeJSON(networkConfig))"
|
||||
case .apiGetNetworkConfig: return "/network"
|
||||
case let .apiSetNetworkInfo(networkInfo): return "/_network info \(encodeJSON(networkInfo))"
|
||||
case .reconnectAllServers: return "/reconnect"
|
||||
case let .apiSetChatSettings(type, id, chatSettings): return "/_settings \(ref(type, id)) \(encodeJSON(chatSettings))"
|
||||
case let .apiSetMemberSettings(groupId, groupMemberId, memberSettings): return "/_member settings #\(groupId) \(groupMemberId) \(encodeJSON(memberSettings))"
|
||||
@@ -284,6 +295,9 @@ public enum ChatCommand {
|
||||
case .listRemoteCtrls: return "/list remote ctrls"
|
||||
case .stopRemoteCtrl: return "/stop remote ctrl"
|
||||
case let .deleteRemoteCtrl(rcId): return "/delete remote ctrl \(rcId)"
|
||||
case let .apiUploadStandaloneFile(userId, file): return "/_upload \(userId) \(file.filePath)"
|
||||
case let .apiDownloadStandaloneFile(userId, link, file): return "/_download \(userId) \(link) \(file.filePath)"
|
||||
case let .apiStandaloneFileInfo(link): return "/_download info \(link)"
|
||||
case .showVersion: return "/version"
|
||||
case let .string(str): return str
|
||||
}
|
||||
@@ -311,12 +325,16 @@ public enum ChatCommand {
|
||||
case .apiSuspendChat: return "apiSuspendChat"
|
||||
case .setTempFolder: return "setTempFolder"
|
||||
case .setFilesFolder: return "setFilesFolder"
|
||||
case .apiSetXFTPConfig: return "apiSetXFTPConfig"
|
||||
case .apiSetEncryptLocalFiles: return "apiSetEncryptLocalFiles"
|
||||
case .apiSetPQEncryption: return "apiSetPQEncryption"
|
||||
case .apiSetContactPQ: return "apiSetContactPQ"
|
||||
case .apiExportArchive: return "apiExportArchive"
|
||||
case .apiImportArchive: return "apiImportArchive"
|
||||
case .apiDeleteStorage: return "apiDeleteStorage"
|
||||
case .apiStorageEncryption: return "apiStorageEncryption"
|
||||
case .testStorageEncryption: return "testStorageEncryption"
|
||||
case .apiSaveSettings: return "apiSaveSettings"
|
||||
case .apiGetSettings: return "apiGetSettings"
|
||||
case .apiGetChats: return "apiGetChats"
|
||||
case .apiGetChat: return "apiGetChat"
|
||||
case .apiGetChatItemInfo: return "apiGetChatItemInfo"
|
||||
@@ -327,6 +345,7 @@ public enum ChatCommand {
|
||||
case .apiConnectContactViaAddress: return "apiConnectContactViaAddress"
|
||||
case .apiDeleteMemberChatItem: return "apiDeleteMemberChatItem"
|
||||
case .apiChatItemReaction: return "apiChatItemReaction"
|
||||
case .apiForwardChatItem: return "apiForwardChatItem"
|
||||
case .apiGetNtfToken: return "apiGetNtfToken"
|
||||
case .apiRegisterToken: return "apiRegisterToken"
|
||||
case .apiVerifyToken: return "apiVerifyToken"
|
||||
@@ -354,6 +373,7 @@ public enum ChatCommand {
|
||||
case .apiGetChatItemTTL: return "apiGetChatItemTTL"
|
||||
case .apiSetNetworkConfig: return "apiSetNetworkConfig"
|
||||
case .apiGetNetworkConfig: return "apiGetNetworkConfig"
|
||||
case .apiSetNetworkInfo: return "apiSetNetworkInfo"
|
||||
case .reconnectAllServers: return "reconnectAllServers"
|
||||
case .apiSetChatSettings: return "apiSetChatSettings"
|
||||
case .apiSetMemberSettings: return "apiSetMemberSettings"
|
||||
@@ -409,6 +429,9 @@ public enum ChatCommand {
|
||||
case .listRemoteCtrls: return "listRemoteCtrls"
|
||||
case .stopRemoteCtrl: return "stopRemoteCtrl"
|
||||
case .deleteRemoteCtrl: return "deleteRemoteCtrl"
|
||||
case .apiUploadStandaloneFile: return "apiUploadStandaloneFile"
|
||||
case .apiDownloadStandaloneFile: return "apiDownloadStandaloneFile"
|
||||
case .apiStandaloneFileInfo: return "apiStandaloneFileInfo"
|
||||
case .showVersion: return "showVersion"
|
||||
case .string: return "console command"
|
||||
}
|
||||
@@ -443,6 +466,8 @@ public enum ChatCommand {
|
||||
return .apiUnhideUser(userId: userId, viewPwd: obfuscate(viewPwd))
|
||||
case let .apiDeleteUser(userId, delSMPQueues, viewPwd):
|
||||
return .apiDeleteUser(userId: userId, delSMPQueues: delSMPQueues, viewPwd: obfuscate(viewPwd))
|
||||
case let .testStorageEncryption(key):
|
||||
return .testStorageEncryption(key: obfuscate(key))
|
||||
default: return self
|
||||
}
|
||||
}
|
||||
@@ -538,11 +563,6 @@ public enum ChatResponse: Decodable, Error {
|
||||
case contactRequestRejected(user: UserRef)
|
||||
case contactUpdated(user: UserRef, toContact: Contact)
|
||||
case groupMemberUpdated(user: UserRef, groupInfo: GroupInfo, fromMember: GroupMember, toMember: GroupMember)
|
||||
// TODO remove events below
|
||||
case contactsSubscribed(server: String, contactRefs: [ContactRef])
|
||||
case contactsDisconnected(server: String, contactRefs: [ContactRef])
|
||||
case contactSubSummary(user: UserRef, contactSubscriptions: [ContactSubStatus])
|
||||
// TODO remove events above
|
||||
case networkStatus(networkStatus: NetworkStatus, connections: [String])
|
||||
case networkStatuses(user_: UserRef?, networkStatuses: [ConnNetworkStatus])
|
||||
case groupSubscribed(user: UserRef, groupInfo: GroupRef)
|
||||
@@ -591,20 +611,28 @@ public enum ChatResponse: Decodable, Error {
|
||||
// receiving file events
|
||||
case rcvFileAccepted(user: UserRef, chatItem: AChatItem)
|
||||
case rcvFileAcceptedSndCancelled(user: UserRef, rcvFileTransfer: RcvFileTransfer)
|
||||
case rcvFileStart(user: UserRef, chatItem: AChatItem)
|
||||
case rcvFileProgressXFTP(user: UserRef, chatItem: AChatItem, receivedSize: Int64, totalSize: Int64)
|
||||
case standaloneFileInfo(fileMeta: MigrationFileLinkData?)
|
||||
case rcvStandaloneFileCreated(user: UserRef, rcvFileTransfer: RcvFileTransfer)
|
||||
case rcvFileStart(user: UserRef, chatItem: AChatItem) // send by chats
|
||||
case rcvFileProgressXFTP(user: UserRef, chatItem_: AChatItem?, receivedSize: Int64, totalSize: Int64, rcvFileTransfer: RcvFileTransfer)
|
||||
case rcvFileComplete(user: UserRef, chatItem: AChatItem)
|
||||
case rcvFileCancelled(user: UserRef, chatItem: AChatItem, rcvFileTransfer: RcvFileTransfer)
|
||||
case rcvStandaloneFileComplete(user: UserRef, targetPath: String, rcvFileTransfer: RcvFileTransfer)
|
||||
case rcvFileCancelled(user: UserRef, chatItem_: AChatItem?, rcvFileTransfer: RcvFileTransfer)
|
||||
case rcvFileSndCancelled(user: UserRef, chatItem: AChatItem, rcvFileTransfer: RcvFileTransfer)
|
||||
case rcvFileError(user: UserRef, chatItem: AChatItem)
|
||||
case rcvFileError(user: UserRef, chatItem_: AChatItem?, rcvFileTransfer: RcvFileTransfer)
|
||||
// sending file events
|
||||
case sndFileStart(user: UserRef, chatItem: AChatItem, sndFileTransfer: SndFileTransfer)
|
||||
case sndFileComplete(user: UserRef, chatItem: AChatItem, sndFileTransfer: SndFileTransfer)
|
||||
case sndFileCancelled(user: UserRef, chatItem: AChatItem, fileTransferMeta: FileTransferMeta, sndFileTransfers: [SndFileTransfer])
|
||||
case sndFileRcvCancelled(user: UserRef, chatItem: AChatItem, sndFileTransfer: SndFileTransfer)
|
||||
case sndFileProgressXFTP(user: UserRef, chatItem: AChatItem, fileTransferMeta: FileTransferMeta, sentSize: Int64, totalSize: Int64)
|
||||
case sndFileRcvCancelled(user: UserRef, chatItem_: AChatItem?, sndFileTransfer: SndFileTransfer)
|
||||
case sndFileCancelled(user: UserRef, chatItem_: AChatItem?, fileTransferMeta: FileTransferMeta, sndFileTransfers: [SndFileTransfer])
|
||||
case sndStandaloneFileCreated(user: UserRef, fileTransferMeta: FileTransferMeta) // returned by _upload
|
||||
case sndFileStartXFTP(user: UserRef, chatItem: AChatItem, fileTransferMeta: FileTransferMeta) // not used
|
||||
case sndFileProgressXFTP(user: UserRef, chatItem_: AChatItem?, fileTransferMeta: FileTransferMeta, sentSize: Int64, totalSize: Int64)
|
||||
case sndFileRedirectStartXFTP(user: UserRef, fileTransferMeta: FileTransferMeta, redirectMeta: FileTransferMeta)
|
||||
case sndFileCompleteXFTP(user: UserRef, chatItem: AChatItem, fileTransferMeta: FileTransferMeta)
|
||||
case sndFileError(user: UserRef, chatItem: AChatItem)
|
||||
case sndStandaloneFileComplete(user: UserRef, fileTransferMeta: FileTransferMeta, rcvURIs: [String])
|
||||
case sndFileCancelledXFTP(user: UserRef, chatItem_: AChatItem?, fileTransferMeta: FileTransferMeta)
|
||||
case sndFileError(user: UserRef, chatItem_: AChatItem?, fileTransferMeta: FileTransferMeta)
|
||||
// call events
|
||||
case callInvitation(callInvitation: RcvCallInvitation)
|
||||
case callOffer(user: UserRef, contact: Contact, callType: CallType, offer: WebRTCSession, sharedKey: String?, askConfirmation: Bool)
|
||||
@@ -613,7 +641,7 @@ public enum ChatResponse: Decodable, Error {
|
||||
case callEnded(user: UserRef, contact: Contact)
|
||||
case callInvitations(callInvitations: [RcvCallInvitation])
|
||||
case ntfTokenStatus(status: NtfTknStatus)
|
||||
case ntfToken(token: DeviceToken, status: NtfTknStatus, ntfMode: NotificationsMode)
|
||||
case ntfToken(token: DeviceToken, status: NtfTknStatus, ntfMode: NotificationsMode, ntfServer: String)
|
||||
case ntfMessages(user_: User?, connEntity_: ConnectionEntity?, msgTs: Date?, ntfMessages: [NtfMsgInfo])
|
||||
case ntfMessage(user: UserRef, connEntity: ConnectionEntity, ntfMessage: NtfMsgInfo)
|
||||
case contactConnectionDeleted(user: UserRef, connection: PendingContactConnection)
|
||||
@@ -624,12 +652,16 @@ public enum ChatResponse: Decodable, Error {
|
||||
case remoteCtrlSessionCode(remoteCtrl_: RemoteCtrlInfo?, sessionCode: String)
|
||||
case remoteCtrlConnected(remoteCtrl: RemoteCtrlInfo)
|
||||
case remoteCtrlStopped(rcsState: RemoteCtrlSessionState, rcStopReason: RemoteCtrlStopReason)
|
||||
// pq
|
||||
case contactPQAllowed(user: UserRef, contact: Contact, pqEncryption: Bool)
|
||||
case contactPQEnabled(user: UserRef, contact: Contact, pqEnabled: Bool)
|
||||
// misc
|
||||
case versionInfo(versionInfo: CoreVersionInfo, chatMigrations: [UpMigration], agentMigrations: [UpMigration])
|
||||
case cmdOk(user: UserRef?)
|
||||
case chatCmdError(user_: UserRef?, chatError: ChatError)
|
||||
case chatError(user_: UserRef?, chatError: ChatError)
|
||||
case archiveImported(archiveErrors: [ArchiveError])
|
||||
case appSettings(appSettings: AppSettings)
|
||||
|
||||
public var responseType: String {
|
||||
get {
|
||||
@@ -693,9 +725,6 @@ public enum ChatResponse: Decodable, Error {
|
||||
case .contactRequestRejected: return "contactRequestRejected"
|
||||
case .contactUpdated: return "contactUpdated"
|
||||
case .groupMemberUpdated: return "groupMemberUpdated"
|
||||
case .contactsSubscribed: return "contactsSubscribed"
|
||||
case .contactsDisconnected: return "contactsDisconnected"
|
||||
case .contactSubSummary: return "contactSubSummary"
|
||||
case .networkStatus: return "networkStatus"
|
||||
case .networkStatuses: return "networkStatuses"
|
||||
case .groupSubscribed: return "groupSubscribed"
|
||||
@@ -742,18 +771,26 @@ public enum ChatResponse: Decodable, Error {
|
||||
case .newMemberContactReceivedInv: return "newMemberContactReceivedInv"
|
||||
case .rcvFileAccepted: return "rcvFileAccepted"
|
||||
case .rcvFileAcceptedSndCancelled: return "rcvFileAcceptedSndCancelled"
|
||||
case .standaloneFileInfo: return "standaloneFileInfo"
|
||||
case .rcvStandaloneFileCreated: return "rcvStandaloneFileCreated"
|
||||
case .rcvFileStart: return "rcvFileStart"
|
||||
case .rcvFileProgressXFTP: return "rcvFileProgressXFTP"
|
||||
case .rcvFileComplete: return "rcvFileComplete"
|
||||
case .rcvStandaloneFileComplete: return "rcvStandaloneFileComplete"
|
||||
case .rcvFileCancelled: return "rcvFileCancelled"
|
||||
case .rcvFileSndCancelled: return "rcvFileSndCancelled"
|
||||
case .rcvFileError: return "rcvFileError"
|
||||
case .sndFileStart: return "sndFileStart"
|
||||
case .sndFileComplete: return "sndFileComplete"
|
||||
case .sndFileCancelled: return "sndFileCancelled"
|
||||
case .sndFileRcvCancelled: return "sndFileRcvCancelled"
|
||||
case .sndStandaloneFileCreated: return "sndStandaloneFileCreated"
|
||||
case .sndFileStartXFTP: return "sndFileStartXFTP"
|
||||
case .sndFileProgressXFTP: return "sndFileProgressXFTP"
|
||||
case .sndFileRedirectStartXFTP: return "sndFileRedirectStartXFTP"
|
||||
case .sndFileRcvCancelled: return "sndFileRcvCancelled"
|
||||
case .sndFileCompleteXFTP: return "sndFileCompleteXFTP"
|
||||
case .sndStandaloneFileComplete: return "sndStandaloneFileComplete"
|
||||
case .sndFileCancelledXFTP: return "sndFileCancelledXFTP"
|
||||
case .sndFileError: return "sndFileError"
|
||||
case .callInvitation: return "callInvitation"
|
||||
case .callOffer: return "callOffer"
|
||||
@@ -772,11 +809,14 @@ public enum ChatResponse: Decodable, Error {
|
||||
case .remoteCtrlSessionCode: return "remoteCtrlSessionCode"
|
||||
case .remoteCtrlConnected: return "remoteCtrlConnected"
|
||||
case .remoteCtrlStopped: return "remoteCtrlStopped"
|
||||
case .contactPQAllowed: return "contactPQAllowed"
|
||||
case .contactPQEnabled: return "contactPQAllowed"
|
||||
case .versionInfo: return "versionInfo"
|
||||
case .cmdOk: return "cmdOk"
|
||||
case .chatCmdError: return "chatCmdError"
|
||||
case .chatError: return "chatError"
|
||||
case .archiveImported: return "archiveImported"
|
||||
case .appSettings: return "appSettings"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -843,9 +883,6 @@ public enum ChatResponse: Decodable, Error {
|
||||
case .contactRequestRejected: return noDetails
|
||||
case let .contactUpdated(u, toContact): return withUser(u, String(describing: toContact))
|
||||
case let .groupMemberUpdated(u, groupInfo, fromMember, toMember): return withUser(u, "groupInfo: \(groupInfo)\nfromMember: \(fromMember)\ntoMember: \(toMember)")
|
||||
case let .contactsSubscribed(server, contactRefs): return "server: \(server)\ncontacts:\n\(String(describing: contactRefs))"
|
||||
case let .contactsDisconnected(server, contactRefs): return "server: \(server)\ncontacts:\n\(String(describing: contactRefs))"
|
||||
case let .contactSubSummary(u, contactSubscriptions): return withUser(u, String(describing: contactSubscriptions))
|
||||
case let .networkStatus(status, conns): return "networkStatus: \(String(describing: status))\nconnections: \(String(describing: conns))"
|
||||
case let .networkStatuses(u, statuses): return withUser(u, String(describing: statuses))
|
||||
case let .groupSubscribed(u, groupInfo): return withUser(u, String(describing: groupInfo))
|
||||
@@ -892,19 +929,27 @@ public enum ChatResponse: Decodable, Error {
|
||||
case let .newMemberContactReceivedInv(u, contact, groupInfo, member): return withUser(u, "contact: \(contact)\ngroupInfo: \(groupInfo)\nmember: \(member)")
|
||||
case let .rcvFileAccepted(u, chatItem): return withUser(u, String(describing: chatItem))
|
||||
case .rcvFileAcceptedSndCancelled: return noDetails
|
||||
case let .standaloneFileInfo(fileMeta): return String(describing: fileMeta)
|
||||
case .rcvStandaloneFileCreated: return noDetails
|
||||
case let .rcvFileStart(u, chatItem): return withUser(u, String(describing: chatItem))
|
||||
case let .rcvFileProgressXFTP(u, chatItem, receivedSize, totalSize): return withUser(u, "chatItem: \(String(describing: chatItem))\nreceivedSize: \(receivedSize)\ntotalSize: \(totalSize)")
|
||||
case let .rcvFileProgressXFTP(u, chatItem, receivedSize, totalSize, _): return withUser(u, "chatItem: \(String(describing: chatItem))\nreceivedSize: \(receivedSize)\ntotalSize: \(totalSize)")
|
||||
case let .rcvStandaloneFileComplete(u, targetPath, _): return withUser(u, targetPath)
|
||||
case let .rcvFileComplete(u, chatItem): return withUser(u, String(describing: chatItem))
|
||||
case let .rcvFileCancelled(u, chatItem, _): return withUser(u, String(describing: chatItem))
|
||||
case let .rcvFileSndCancelled(u, chatItem, _): return withUser(u, String(describing: chatItem))
|
||||
case let .rcvFileError(u, chatItem): return withUser(u, String(describing: chatItem))
|
||||
case let .rcvFileError(u, chatItem, _): return withUser(u, String(describing: chatItem))
|
||||
case let .sndFileStart(u, chatItem, _): return withUser(u, String(describing: chatItem))
|
||||
case let .sndFileComplete(u, chatItem, _): return withUser(u, String(describing: chatItem))
|
||||
case let .sndFileCancelled(u, chatItem, _, _): return withUser(u, String(describing: chatItem))
|
||||
case .sndStandaloneFileCreated: return noDetails
|
||||
case let .sndFileStartXFTP(u, chatItem, _): return withUser(u, String(describing: chatItem))
|
||||
case let .sndFileRcvCancelled(u, chatItem, _): return withUser(u, String(describing: chatItem))
|
||||
case let .sndFileProgressXFTP(u, chatItem, _, sentSize, totalSize): return withUser(u, "chatItem: \(String(describing: chatItem))\nsentSize: \(sentSize)\ntotalSize: \(totalSize)")
|
||||
case let .sndFileRedirectStartXFTP(u, _, redirectMeta): return withUser(u, String(describing: redirectMeta))
|
||||
case let .sndFileCompleteXFTP(u, chatItem, _): return withUser(u, String(describing: chatItem))
|
||||
case let .sndFileError(u, chatItem): return withUser(u, String(describing: chatItem))
|
||||
case let .sndStandaloneFileComplete(u, _, rcvURIs): return withUser(u, String(rcvURIs.count))
|
||||
case let .sndFileCancelledXFTP(u, chatItem, _): return withUser(u, String(describing: chatItem))
|
||||
case let .sndFileError(u, chatItem, _): return withUser(u, String(describing: chatItem))
|
||||
case let .callInvitation(inv): return String(describing: inv)
|
||||
case let .callOffer(u, contact, callType, offer, sharedKey, askConfirmation): return withUser(u, "contact: \(contact.id)\ncallType: \(String(describing: callType))\nsharedKey: \(sharedKey ?? "")\naskConfirmation: \(askConfirmation)\noffer: \(String(describing: offer))")
|
||||
case let .callAnswer(u, contact, answer): return withUser(u, "contact: \(contact.id)\nanswer: \(String(describing: answer))")
|
||||
@@ -912,7 +957,7 @@ public enum ChatResponse: Decodable, Error {
|
||||
case let .callEnded(u, contact): return withUser(u, "contact: \(contact.id)")
|
||||
case let .callInvitations(invs): return String(describing: invs)
|
||||
case let .ntfTokenStatus(status): return String(describing: status)
|
||||
case let .ntfToken(token, status, ntfMode): return "token: \(token)\nstatus: \(status.rawValue)\nntfMode: \(ntfMode.rawValue)"
|
||||
case let .ntfToken(token, status, ntfMode, ntfServer): return "token: \(token)\nstatus: \(status.rawValue)\nntfMode: \(ntfMode.rawValue)\nntfServer: \(ntfServer)"
|
||||
case let .ntfMessages(u, connEntity, msgTs, ntfMessages): return withUser(u, "connEntity: \(String(describing: connEntity))\nmsgTs: \(String(describing: msgTs))\nntfMessages: \(String(describing: ntfMessages))")
|
||||
case let .ntfMessage(u, connEntity, ntfMessage): return withUser(u, "connEntity: \(String(describing: connEntity))\nntfMessage: \(String(describing: ntfMessage))")
|
||||
case let .contactConnectionDeleted(u, connection): return withUser(u, String(describing: connection))
|
||||
@@ -922,11 +967,14 @@ public enum ChatResponse: Decodable, Error {
|
||||
case let .remoteCtrlSessionCode(remoteCtrl_, sessionCode): return "remoteCtrl_:\n\(String(describing: remoteCtrl_))\nsessionCode: \(sessionCode)"
|
||||
case let .remoteCtrlConnected(remoteCtrl): return String(describing: remoteCtrl)
|
||||
case .remoteCtrlStopped: return noDetails
|
||||
case let .contactPQAllowed(u, contact, pqEncryption): return withUser(u, "contact: \(String(describing: contact))\npqEncryption: \(pqEncryption)")
|
||||
case let .contactPQEnabled(u, contact, pqEnabled): return withUser(u, "contact: \(String(describing: contact))\npqEnabled: \(pqEnabled)")
|
||||
case let .versionInfo(versionInfo, chatMigrations, agentMigrations): return "\(String(describing: versionInfo))\n\nchat migrations: \(chatMigrations.map(\.upName))\n\nagent migrations: \(agentMigrations.map(\.upName))"
|
||||
case .cmdOk: return noDetails
|
||||
case let .chatCmdError(u, chatError): return withUser(u, String(describing: chatError))
|
||||
case let .chatError(u, chatError): return withUser(u, String(describing: chatError))
|
||||
case let .archiveImported(archiveErrors): return String(describing: archiveErrors)
|
||||
case let .appSettings(appSettings): return String(describing: appSettings)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1005,10 +1053,6 @@ struct ComposedMessage: Encodable {
|
||||
var msgContent: MsgContent
|
||||
}
|
||||
|
||||
public struct XFTPFileConfig: Encodable {
|
||||
var minFileSize: Int64
|
||||
}
|
||||
|
||||
public struct ArchiveConfig: Encodable {
|
||||
var archivePath: String
|
||||
var disableCompression: Bool?
|
||||
@@ -1213,6 +1257,7 @@ public struct NetCfg: Codable, Equatable {
|
||||
public var tcpConnectTimeout: Int // microseconds
|
||||
public var tcpTimeout: Int // microseconds
|
||||
public var tcpTimeoutPerKb: Int // microseconds
|
||||
public var rcvConcurrency: Int // pool size
|
||||
public var tcpKeepAlive: KeepAliveOpts?
|
||||
public var smpPingInterval: Int // microseconds
|
||||
public var smpPingCount: Int // times
|
||||
@@ -1221,9 +1266,10 @@ public struct NetCfg: Codable, Equatable {
|
||||
public static let defaults: NetCfg = NetCfg(
|
||||
socksProxy: nil,
|
||||
sessionMode: TransportSessionMode.user,
|
||||
tcpConnectTimeout: 20_000_000,
|
||||
tcpConnectTimeout: 10_000_000,
|
||||
tcpTimeout: 15_000_000,
|
||||
tcpTimeoutPerKb: 45_000,
|
||||
tcpTimeoutPerKb: 10_000,
|
||||
rcvConcurrency: 12,
|
||||
tcpKeepAlive: KeepAliveOpts.defaults,
|
||||
smpPingInterval: 1200_000_000,
|
||||
smpPingCount: 3,
|
||||
@@ -1233,9 +1279,10 @@ public struct NetCfg: Codable, Equatable {
|
||||
public static let proxyDefaults: NetCfg = NetCfg(
|
||||
socksProxy: nil,
|
||||
sessionMode: TransportSessionMode.user,
|
||||
tcpConnectTimeout: 30_000_000,
|
||||
tcpConnectTimeout: 20_000_000,
|
||||
tcpTimeout: 20_000_000,
|
||||
tcpTimeoutPerKb: 60_000,
|
||||
tcpTimeoutPerKb: 15_000,
|
||||
rcvConcurrency: 8,
|
||||
tcpKeepAlive: KeepAliveOpts.defaults,
|
||||
smpPingInterval: 1200_000_000,
|
||||
smpPingCount: 3,
|
||||
@@ -1532,7 +1579,7 @@ public enum NotificationsMode: String, Decodable, SelectableItem {
|
||||
public static var values: [NotificationsMode] = [.instant, .periodic, .off]
|
||||
}
|
||||
|
||||
public enum NotificationPreviewMode: String, SelectableItem {
|
||||
public enum NotificationPreviewMode: String, SelectableItem, Codable {
|
||||
case hidden
|
||||
case contact
|
||||
case message
|
||||
@@ -1676,6 +1723,8 @@ public enum ChatErrorType: Decodable {
|
||||
case fallbackToSMPProhibited(fileId: Int64)
|
||||
case inlineFileProhibited(fileId: Int64)
|
||||
case invalidQuote
|
||||
case invalidForward
|
||||
case forwardNoFile
|
||||
case invalidChatItemUpdate
|
||||
case invalidChatItemDelete
|
||||
case hasCurrentCall
|
||||
@@ -1732,6 +1781,7 @@ public enum StoreError: Decodable {
|
||||
case fileIdNotFoundBySharedMsgId(sharedMsgId: String)
|
||||
case sndFileNotFoundXFTP(agentSndFileId: String)
|
||||
case rcvFileNotFoundXFTP(agentRcvFileId: String)
|
||||
case extraFileDescrNotFoundXFTP(fileId: Int64)
|
||||
case connectionNotFound(agentConnId: String)
|
||||
case connectionNotFoundById(connId: Int64)
|
||||
case connectionNotFoundByMemberId(groupMemberId: Int64)
|
||||
@@ -1777,6 +1827,7 @@ public enum AgentErrorType: Decodable {
|
||||
case BROKER(brokerAddress: String, brokerErr: BrokerErrorType)
|
||||
case AGENT(agentErr: SMPAgentError)
|
||||
case INTERNAL(internalErr: String)
|
||||
case CRITICAL(offerRestart: Bool, criticalErr: String)
|
||||
case INACTIVE
|
||||
}
|
||||
|
||||
@@ -1828,6 +1879,8 @@ public enum XFTPErrorType: Decodable {
|
||||
case NO_FILE
|
||||
case HAS_FILE
|
||||
case FILE_IO
|
||||
case TIMEOUT
|
||||
case REDIRECT(redirectError: String)
|
||||
case INTERNAL
|
||||
}
|
||||
|
||||
@@ -1835,6 +1888,8 @@ public enum RCErrorType: Decodable {
|
||||
case `internal`(internalErr: String)
|
||||
case identity
|
||||
case noLocalAddress
|
||||
case newController
|
||||
case notDiscovered
|
||||
case tlsStartFailed
|
||||
case exception(exception: String)
|
||||
case ctrlAuth
|
||||
@@ -1860,6 +1915,7 @@ public enum ProtocolTransportError: Decodable {
|
||||
case badBlock
|
||||
case largeMsg
|
||||
case badSession
|
||||
case noServerAuth
|
||||
case handshake(handshakeErr: SMPHandshakeError)
|
||||
}
|
||||
|
||||
@@ -1867,6 +1923,7 @@ public enum SMPHandshakeError: Decodable {
|
||||
case PARSE
|
||||
case VERSION
|
||||
case IDENTITY
|
||||
case BAD_AUTH
|
||||
}
|
||||
|
||||
public enum SMPAgentError: Decodable {
|
||||
@@ -1888,8 +1945,183 @@ public enum RemoteCtrlError: Decodable {
|
||||
case badState
|
||||
case busy
|
||||
case timeout
|
||||
case noKnownControllers
|
||||
case badController
|
||||
case disconnected(remoteCtrlId: Int64, reason: String)
|
||||
case badInvitation
|
||||
case badVersion(appVersion: String)
|
||||
// case protocolError(protocolError: RemoteProtocolError)
|
||||
case hTTP2Error(http2Error: String)
|
||||
case protocolError
|
||||
}
|
||||
|
||||
public struct MigrationFileLinkData: Codable {
|
||||
let networkConfig: NetworkConfig?
|
||||
|
||||
public init(networkConfig: NetworkConfig) {
|
||||
self.networkConfig = networkConfig
|
||||
}
|
||||
|
||||
public struct NetworkConfig: Codable {
|
||||
let socksProxy: String?
|
||||
let hostMode: HostMode?
|
||||
let requiredHostMode: Bool?
|
||||
|
||||
public init(socksProxy: String?, hostMode: HostMode?, requiredHostMode: Bool?) {
|
||||
self.socksProxy = socksProxy
|
||||
self.hostMode = hostMode
|
||||
self.requiredHostMode = requiredHostMode
|
||||
}
|
||||
|
||||
public func transformToPlatformSupported() -> NetworkConfig {
|
||||
return if let hostMode, let requiredHostMode {
|
||||
NetworkConfig(
|
||||
socksProxy: nil,
|
||||
hostMode: hostMode == .onionViaSocks ? .onionHost : hostMode,
|
||||
requiredHostMode: requiredHostMode
|
||||
)
|
||||
} else { self }
|
||||
}
|
||||
}
|
||||
|
||||
public func addToLink(link: String) -> String {
|
||||
"\(link)&data=\(encodeJSON(self).addingPercentEncoding(withAllowedCharacters: .urlHostAllowed)!)"
|
||||
}
|
||||
|
||||
public static func readFromLink(link: String) -> MigrationFileLinkData? {
|
||||
// standaloneFileInfo(link)
|
||||
nil
|
||||
}
|
||||
}
|
||||
|
||||
public struct AppSettings: Codable, Equatable {
|
||||
public var networkConfig: NetCfg? = nil
|
||||
public var privacyEncryptLocalFiles: Bool? = nil
|
||||
public var privacyAcceptImages: Bool? = nil
|
||||
public var privacyLinkPreviews: Bool? = nil
|
||||
public var privacyShowChatPreviews: Bool? = nil
|
||||
public var privacySaveLastDraft: Bool? = nil
|
||||
public var privacyProtectScreen: Bool? = nil
|
||||
public var notificationMode: AppSettingsNotificationMode? = nil
|
||||
public var notificationPreviewMode: NotificationPreviewMode? = nil
|
||||
public var webrtcPolicyRelay: Bool? = nil
|
||||
public var webrtcICEServers: [String]? = nil
|
||||
public var confirmRemoteSessions: Bool? = nil
|
||||
public var connectRemoteViaMulticast: Bool? = nil
|
||||
public var connectRemoteViaMulticastAuto: Bool? = nil
|
||||
public var developerTools: Bool? = nil
|
||||
public var confirmDBUpgrades: Bool? = nil
|
||||
public var androidCallOnLockScreen: AppSettingsLockScreenCalls? = nil
|
||||
public var iosCallKitEnabled: Bool? = nil
|
||||
public var iosCallKitCallsInRecents: Bool? = nil
|
||||
|
||||
public func prepareForExport() -> AppSettings {
|
||||
var empty = AppSettings()
|
||||
let def = AppSettings.defaults
|
||||
if networkConfig != def.networkConfig { empty.networkConfig = networkConfig }
|
||||
if privacyEncryptLocalFiles != def.privacyEncryptLocalFiles { empty.privacyEncryptLocalFiles = privacyEncryptLocalFiles }
|
||||
if privacyAcceptImages != def.privacyAcceptImages { empty.privacyAcceptImages = privacyAcceptImages }
|
||||
if privacyLinkPreviews != def.privacyLinkPreviews { empty.privacyLinkPreviews = privacyLinkPreviews }
|
||||
if privacyShowChatPreviews != def.privacyShowChatPreviews { empty.privacyShowChatPreviews = privacyShowChatPreviews }
|
||||
if privacySaveLastDraft != def.privacySaveLastDraft { empty.privacySaveLastDraft = privacySaveLastDraft }
|
||||
if privacyProtectScreen != def.privacyProtectScreen { empty.privacyProtectScreen = privacyProtectScreen }
|
||||
if notificationMode != def.notificationMode { empty.notificationMode = notificationMode }
|
||||
if notificationPreviewMode != def.notificationPreviewMode { empty.notificationPreviewMode = notificationPreviewMode }
|
||||
if webrtcPolicyRelay != def.webrtcPolicyRelay { empty.webrtcPolicyRelay = webrtcPolicyRelay }
|
||||
if webrtcICEServers != def.webrtcICEServers { empty.webrtcICEServers = webrtcICEServers }
|
||||
if confirmRemoteSessions != def.confirmRemoteSessions { empty.confirmRemoteSessions = confirmRemoteSessions }
|
||||
if connectRemoteViaMulticast != def.connectRemoteViaMulticast {empty.connectRemoteViaMulticast = connectRemoteViaMulticast }
|
||||
if connectRemoteViaMulticastAuto != def.connectRemoteViaMulticastAuto { empty.connectRemoteViaMulticastAuto = connectRemoteViaMulticastAuto }
|
||||
if developerTools != def.developerTools { empty.developerTools = developerTools }
|
||||
if confirmDBUpgrades != def.confirmDBUpgrades { empty.confirmDBUpgrades = confirmDBUpgrades }
|
||||
if androidCallOnLockScreen != def.androidCallOnLockScreen { empty.androidCallOnLockScreen = androidCallOnLockScreen }
|
||||
if iosCallKitEnabled != def.iosCallKitEnabled { empty.iosCallKitEnabled = iosCallKitEnabled }
|
||||
if iosCallKitCallsInRecents != def.iosCallKitCallsInRecents { empty.iosCallKitCallsInRecents = iosCallKitCallsInRecents }
|
||||
return empty
|
||||
}
|
||||
|
||||
public static var defaults: AppSettings {
|
||||
AppSettings (
|
||||
networkConfig: NetCfg.defaults,
|
||||
privacyEncryptLocalFiles: true,
|
||||
privacyAcceptImages: true,
|
||||
privacyLinkPreviews: true,
|
||||
privacyShowChatPreviews: true,
|
||||
privacySaveLastDraft: true,
|
||||
privacyProtectScreen: false,
|
||||
notificationMode: AppSettingsNotificationMode.instant,
|
||||
notificationPreviewMode: NotificationPreviewMode.message,
|
||||
webrtcPolicyRelay: true,
|
||||
webrtcICEServers: [],
|
||||
confirmRemoteSessions: false,
|
||||
connectRemoteViaMulticast: true,
|
||||
connectRemoteViaMulticastAuto: true,
|
||||
developerTools: false,
|
||||
confirmDBUpgrades: false,
|
||||
androidCallOnLockScreen: AppSettingsLockScreenCalls.show,
|
||||
iosCallKitEnabled: true,
|
||||
iosCallKitCallsInRecents: false
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
public enum AppSettingsNotificationMode: String, Codable {
|
||||
case off
|
||||
case periodic
|
||||
case instant
|
||||
|
||||
public func toNotificationsMode() -> NotificationsMode {
|
||||
switch self {
|
||||
case .instant: .instant
|
||||
case .periodic: .periodic
|
||||
case .off: .off
|
||||
}
|
||||
}
|
||||
|
||||
public static func from(_ mode: NotificationsMode) -> AppSettingsNotificationMode {
|
||||
switch mode {
|
||||
case .instant: .instant
|
||||
case .periodic: .periodic
|
||||
case .off: .off
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//public enum NotificationPreviewMode: Codable {
|
||||
// case hidden
|
||||
// case contact
|
||||
// case message
|
||||
//}
|
||||
|
||||
public enum AppSettingsLockScreenCalls: String, Codable {
|
||||
case disable
|
||||
case show
|
||||
case accept
|
||||
}
|
||||
|
||||
public struct UserNetworkInfo: Codable, Equatable {
|
||||
public let networkType: UserNetworkType
|
||||
public let online: Bool
|
||||
|
||||
public init(networkType: UserNetworkType, online: Bool) {
|
||||
self.networkType = networkType
|
||||
self.online = online
|
||||
}
|
||||
}
|
||||
|
||||
public enum UserNetworkType: String, Codable {
|
||||
case none
|
||||
case cellular
|
||||
case wifi
|
||||
case ethernet
|
||||
case other
|
||||
|
||||
public var text: LocalizedStringKey {
|
||||
switch self {
|
||||
case .none: "No network connection"
|
||||
case .cellular: "Cellular"
|
||||
case .wifi: "WiFi"
|
||||
case .ethernet: "Wired ethernet"
|
||||
case .other: "Other"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ public let GROUP_DEFAULT_CHAT_LAST_BACKGROUND_RUN = "chatLastBackgroundRun"
|
||||
let GROUP_DEFAULT_NTF_PREVIEW_MODE = "ntfPreviewMode"
|
||||
public let GROUP_DEFAULT_NTF_ENABLE_LOCAL = "ntfEnableLocal" // no longer used
|
||||
public let GROUP_DEFAULT_NTF_ENABLE_PERIODIC = "ntfEnablePeriodic" // no longer used
|
||||
// This setting is a main one, while having an unused duplicate from the past: DEFAULT_PRIVACY_ACCEPT_IMAGES
|
||||
let GROUP_DEFAULT_PRIVACY_ACCEPT_IMAGES = "privacyAcceptImages"
|
||||
public let GROUP_DEFAULT_PRIVACY_TRANSFER_IMAGES_INLINE = "privacyTransferImagesInline" // no longer used
|
||||
public let GROUP_DEFAULT_PRIVACY_ENCRYPT_LOCAL_FILES = "privacyEncryptLocalFiles"
|
||||
@@ -28,6 +29,7 @@ let GROUP_DEFAULT_NETWORK_SESSION_MODE = "networkSessionMode"
|
||||
let GROUP_DEFAULT_NETWORK_TCP_CONNECT_TIMEOUT = "networkTCPConnectTimeout"
|
||||
let GROUP_DEFAULT_NETWORK_TCP_TIMEOUT = "networkTCPTimeout"
|
||||
let GROUP_DEFAULT_NETWORK_TCP_TIMEOUT_PER_KB = "networkTCPTimeoutPerKb"
|
||||
let GROUP_DEFAULT_NETWORK_RCV_CONCURRENCY = "networkRcvConcurrency"
|
||||
let GROUP_DEFAULT_NETWORK_SMP_PING_INTERVAL = "networkSMPPingInterval"
|
||||
let GROUP_DEFAULT_NETWORK_SMP_PING_COUNT = "networkSMPPingCount"
|
||||
let GROUP_DEFAULT_NETWORK_ENABLE_KEEP_ALIVE = "networkEnableKeepAlive"
|
||||
@@ -36,9 +38,10 @@ let GROUP_DEFAULT_NETWORK_TCP_KEEP_INTVL = "networkTCPKeepIntvl"
|
||||
let GROUP_DEFAULT_NETWORK_TCP_KEEP_CNT = "networkTCPKeepCnt"
|
||||
public let GROUP_DEFAULT_INCOGNITO = "incognito"
|
||||
let GROUP_DEFAULT_STORE_DB_PASSPHRASE = "storeDBPassphrase"
|
||||
let GROUP_DEFAULT_INITIAL_RANDOM_DB_PASSPHRASE = "initialRandomDBPassphrase"
|
||||
public let GROUP_DEFAULT_INITIAL_RANDOM_DB_PASSPHRASE = "initialRandomDBPassphrase"
|
||||
public let GROUP_DEFAULT_CONFIRM_DB_UPGRADES = "confirmDBUpgrades"
|
||||
public let GROUP_DEFAULT_CALL_KIT_ENABLED = "callKitEnabled"
|
||||
public let GROUP_DEFAULT_PQ_EXPERIMENTAL_ENABLED = "pqExperimentalEnabled"
|
||||
|
||||
public let APP_GROUP_NAME = "group.chat.simplex.app"
|
||||
|
||||
@@ -53,6 +56,7 @@ public func registerGroupDefaults() {
|
||||
GROUP_DEFAULT_NETWORK_TCP_CONNECT_TIMEOUT: NetCfg.defaults.tcpConnectTimeout,
|
||||
GROUP_DEFAULT_NETWORK_TCP_TIMEOUT: NetCfg.defaults.tcpTimeout,
|
||||
GROUP_DEFAULT_NETWORK_TCP_TIMEOUT_PER_KB: NetCfg.defaults.tcpTimeoutPerKb,
|
||||
GROUP_DEFAULT_NETWORK_RCV_CONCURRENCY: NetCfg.defaults.rcvConcurrency,
|
||||
GROUP_DEFAULT_NETWORK_SMP_PING_INTERVAL: NetCfg.defaults.smpPingInterval,
|
||||
GROUP_DEFAULT_NETWORK_SMP_PING_COUNT: NetCfg.defaults.smpPingCount,
|
||||
GROUP_DEFAULT_NETWORK_ENABLE_KEEP_ALIVE: NetCfg.defaults.enableKeepAlive,
|
||||
@@ -67,6 +71,7 @@ public func registerGroupDefaults() {
|
||||
GROUP_DEFAULT_PRIVACY_ENCRYPT_LOCAL_FILES: true,
|
||||
GROUP_DEFAULT_CONFIRM_DB_UPGRADES: false,
|
||||
GROUP_DEFAULT_CALL_KIT_ENABLED: true,
|
||||
GROUP_DEFAULT_PQ_EXPERIMENTAL_ENABLED: false,
|
||||
])
|
||||
}
|
||||
|
||||
@@ -167,6 +172,7 @@ public let ntfPreviewModeGroupDefault = EnumDefault<NotificationPreviewMode>(
|
||||
|
||||
public let incognitoGroupDefault = BoolDefault(defaults: groupDefaults, forKey: GROUP_DEFAULT_INCOGNITO)
|
||||
|
||||
// This setting is a main one, while having an unused duplicate from the past: DEFAULT_PRIVACY_ACCEPT_IMAGES
|
||||
public let privacyAcceptImagesGroupDefault = BoolDefault(defaults: groupDefaults, forKey: GROUP_DEFAULT_PRIVACY_ACCEPT_IMAGES)
|
||||
|
||||
public let privacyEncryptLocalFilesGroupDefault = BoolDefault(defaults: groupDefaults, forKey: GROUP_DEFAULT_PRIVACY_ENCRYPT_LOCAL_FILES)
|
||||
@@ -193,6 +199,8 @@ public let confirmDBUpgradesGroupDefault = BoolDefault(defaults: groupDefaults,
|
||||
|
||||
public let callKitEnabledGroupDefault = BoolDefault(defaults: groupDefaults, forKey: GROUP_DEFAULT_CALL_KIT_ENABLED)
|
||||
|
||||
public let pqExperimentalEnabledDefault = BoolDefault(defaults: groupDefaults, forKey: GROUP_DEFAULT_PQ_EXPERIMENTAL_ENABLED)
|
||||
|
||||
public class DateDefault {
|
||||
var defaults: UserDefaults
|
||||
var key: String
|
||||
@@ -265,10 +273,6 @@ public class Default<T> {
|
||||
}
|
||||
}
|
||||
|
||||
public func getXFTPCfg() -> XFTPFileConfig {
|
||||
return XFTPFileConfig(minFileSize: 0)
|
||||
}
|
||||
|
||||
public func getNetCfg() -> NetCfg {
|
||||
let onionHosts = networkUseOnionHostsGroupDefault.get()
|
||||
let (hostMode, requiredHostMode) = onionHosts.hostMode
|
||||
@@ -276,6 +280,7 @@ public func getNetCfg() -> NetCfg {
|
||||
let tcpConnectTimeout = groupDefaults.integer(forKey: GROUP_DEFAULT_NETWORK_TCP_CONNECT_TIMEOUT)
|
||||
let tcpTimeout = groupDefaults.integer(forKey: GROUP_DEFAULT_NETWORK_TCP_TIMEOUT)
|
||||
let tcpTimeoutPerKb = groupDefaults.integer(forKey: GROUP_DEFAULT_NETWORK_TCP_TIMEOUT_PER_KB)
|
||||
let rcvConcurrency = groupDefaults.integer(forKey: GROUP_DEFAULT_NETWORK_RCV_CONCURRENCY)
|
||||
let smpPingInterval = groupDefaults.integer(forKey: GROUP_DEFAULT_NETWORK_SMP_PING_INTERVAL)
|
||||
let smpPingCount = groupDefaults.integer(forKey: GROUP_DEFAULT_NETWORK_SMP_PING_COUNT)
|
||||
let enableKeepAlive = groupDefaults.bool(forKey: GROUP_DEFAULT_NETWORK_ENABLE_KEEP_ALIVE)
|
||||
@@ -295,6 +300,7 @@ public func getNetCfg() -> NetCfg {
|
||||
tcpConnectTimeout: tcpConnectTimeout,
|
||||
tcpTimeout: tcpTimeout,
|
||||
tcpTimeoutPerKb: tcpTimeoutPerKb,
|
||||
rcvConcurrency: rcvConcurrency,
|
||||
tcpKeepAlive: tcpKeepAlive,
|
||||
smpPingInterval: smpPingInterval,
|
||||
smpPingCount: smpPingCount,
|
||||
@@ -308,6 +314,7 @@ public func setNetCfg(_ cfg: NetCfg) {
|
||||
groupDefaults.set(cfg.tcpConnectTimeout, forKey: GROUP_DEFAULT_NETWORK_TCP_CONNECT_TIMEOUT)
|
||||
groupDefaults.set(cfg.tcpTimeout, forKey: GROUP_DEFAULT_NETWORK_TCP_TIMEOUT)
|
||||
groupDefaults.set(cfg.tcpTimeoutPerKb, forKey: GROUP_DEFAULT_NETWORK_TCP_TIMEOUT_PER_KB)
|
||||
groupDefaults.set(cfg.rcvConcurrency, forKey: GROUP_DEFAULT_NETWORK_RCV_CONCURRENCY)
|
||||
groupDefaults.set(cfg.smpPingInterval, forKey: GROUP_DEFAULT_NETWORK_SMP_PING_INTERVAL)
|
||||
groupDefaults.set(cfg.smpPingCount, forKey: GROUP_DEFAULT_NETWORK_SMP_PING_COUNT)
|
||||
if let tcpKeepAlive = cfg.tcpKeepAlive {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user