mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2024-12-17 17:20:21 +01:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| fa9f5b3250 |
+16
-25
@@ -1,41 +1,32 @@
|
|||||||
ARG TAG=22.04
|
FROM ubuntu:focal AS build
|
||||||
|
|
||||||
FROM ubuntu:${TAG} AS build
|
# 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
|
||||||
### 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
|
# Install ghcup
|
||||||
RUN curl --proto '=https' --tlsv1.2 -sSf https://get-ghcup.haskell.org | BOOTSTRAP_HASKELL_NONINTERACTIVE=1 sh
|
RUN a=$(arch); curl https://downloads.haskell.org/~ghcup/$a-linux-ghcup -o /usr/bin/ghcup && \
|
||||||
|
chmod +x /usr/bin/ghcup
|
||||||
# Adjust PATH
|
|
||||||
ENV PATH="/root/.cabal/bin:/root/.ghcup/bin:$PATH"
|
|
||||||
|
|
||||||
|
# Install ghc
|
||||||
|
RUN ghcup install ghc 9.6.3
|
||||||
|
# Install cabal
|
||||||
|
RUN ghcup install cabal 3.10.1.0
|
||||||
# Set both as default
|
# Set both as default
|
||||||
RUN ghcup set ghc "${BOOTSTRAP_HASKELL_GHC_VERSION}" && \
|
RUN ghcup set ghc 9.6.3 && \
|
||||||
ghcup set cabal "${BOOTSTRAP_HASKELL_CABAL_VERSION}"
|
ghcup set cabal 3.10.1.0
|
||||||
|
|
||||||
COPY . /project
|
COPY . /project
|
||||||
WORKDIR /project
|
WORKDIR /project
|
||||||
|
|
||||||
|
# Adjust PATH
|
||||||
|
ENV PATH="/root/.cabal/bin:/root/.ghcup/bin:$PATH"
|
||||||
|
|
||||||
# Adjust build
|
# Adjust build
|
||||||
RUN cp ./scripts/cabal.project.local.linux ./cabal.project.local
|
RUN cp ./scripts/cabal.project.local.linux ./cabal.project.local
|
||||||
|
|
||||||
# Compile simplex-chat
|
# Compile simplex-chat
|
||||||
RUN cabal update
|
RUN cabal update
|
||||||
RUN cabal build exe:simplex-chat
|
RUN cabal install
|
||||||
|
|
||||||
# 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
|
FROM scratch AS export-stage
|
||||||
COPY --from=build /project/simplex-chat /
|
COPY --from=build /root/.cabal/bin/simplex-chat /
|
||||||
|
|||||||
+58
-92
@@ -1,168 +1,134 @@
|
|||||||
# SimpleX Chat Privacy Policy and Conditions of Use
|
# SimpleX Chat Terms & Privacy Policy
|
||||||
|
|
||||||
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.
|
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 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.
|
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).
|
||||||
|
|
||||||
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
|
## Privacy Policy
|
||||||
|
|
||||||
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 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 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 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 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.
|
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).
|
||||||
|
|
||||||
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.
|
### Information you provide
|
||||||
|
|
||||||
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
|
#### User profiles
|
||||||
|
|
||||||
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.
|
We do not store user profiles. The profile you create in the app is local to your device.
|
||||||
|
|
||||||
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.
|
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.
|
||||||
|
|
||||||
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
|
#### Messages and Files
|
||||||
|
|
||||||
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.
|
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.
|
||||||
|
|
||||||
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.
|
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.
|
||||||
|
|
||||||
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 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).
|
||||||
|
|
||||||
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).
|
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 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 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).
|
||||||
|
|
||||||
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
|
#### Connections with other users
|
||||||
|
|
||||||
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.
|
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.
|
||||||
|
|
||||||
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.
|
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.
|
||||||
|
|
||||||
#### 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
|
#### 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.
|
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.
|
||||||
|
|
||||||
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.
|
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.
|
||||||
|
|
||||||
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).
|
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).
|
||||||
|
|
||||||
#### Another information stored on the servers
|
#### 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 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.
|
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.
|
||||||
|
|
||||||
#### SimpleX Directory
|
#### SimpleX Directory Service
|
||||||
|
|
||||||
[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).
|
[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).
|
||||||
|
|
||||||
#### User Support
|
#### User Support.
|
||||||
|
|
||||||
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.
|
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.
|
||||||
|
|
||||||
### Information we may share
|
### Information we may share
|
||||||
|
|
||||||
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 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.
|
||||||
|
|
||||||
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).
|
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.
|
||||||
|
|
||||||
The cases when SimpleX Chat Ltd may share the data temporarily stored on the servers:
|
The cases when SimpleX Chat may need to share the data we temporarily store on the servers:
|
||||||
|
|
||||||
- To meet any applicable law, or enforceable governmental request or court order.
|
- To meet any applicable law, regulation, legal process or enforceable governmental request.
|
||||||
- To enforce applicable terms, including investigation of potential violations.
|
- To enforce applicable Terms, including investigation of potential violations.
|
||||||
- To detect, prevent, or otherwise address fraud, security, or technical issues.
|
- To detect, prevent, or otherwise address fraud, security, or technical issues.
|
||||||
- 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.
|
- To protect against harm to the rights, property, or safety of SimpleX Chat, our users, 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 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.
|
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.
|
||||||
|
|
||||||
### Updates
|
### 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 software applications and preset relays infrastructure 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 Services confirms your acceptance of our updated Privacy Policy.
|
||||||
|
|
||||||
Please also read our Conditions of Use of Software and Infrastructure below.
|
Please also read our Terms of Service 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).
|
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).
|
||||||
|
|
||||||
## Conditions of Use of Software and Infrastructure
|
## Terms of Service
|
||||||
|
|
||||||
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.
|
You accept our Terms of Service ("Terms") by installing or using any of our apps or services ("Services").
|
||||||
|
|
||||||
**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.
|
**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.
|
||||||
|
|
||||||
**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.
|
**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.
|
||||||
|
|
||||||
**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.
|
**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.
|
||||||
|
|
||||||
**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.
|
**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.
|
||||||
|
|
||||||
**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.
|
**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.
|
||||||
|
|
||||||
**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.
|
**Traffic and device costs**. You are solely responsible for the traffic and device costs on which you use our Services, and any associated taxes.
|
||||||
|
|
||||||
**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.
|
**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.
|
||||||
|
|
||||||
**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.
|
**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.
|
||||||
|
|
||||||
**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.
|
**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.
|
||||||
|
|
||||||
**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 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.
|
||||||
|
|
||||||
**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.
|
**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.
|
||||||
|
|
||||||
**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.
|
**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 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.
|
**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.
|
||||||
|
|
||||||
**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.
|
**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.
|
||||||
|
|
||||||
**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.
|
**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)
|
||||||
|
|
||||||
**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.
|
**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.
|
||||||
|
|
||||||
**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).
|
**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.
|
||||||
|
|
||||||
**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.
|
**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.
|
||||||
|
|
||||||
**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.
|
**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.
|
||||||
|
|
||||||
**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.
|
**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.
|
||||||
|
|
||||||
**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.
|
**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.
|
||||||
|
|
||||||
**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.
|
**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.
|
||||||
|
|
||||||
**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.
|
**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.
|
||||||
|
|
||||||
**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.
|
Updated August 17, 2023
|
||||||
|
|
||||||
**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
|
|
||||||
|
|||||||
@@ -234,8 +234,6 @@ You can use SimpleX with your own servers and still communicate with people usin
|
|||||||
|
|
||||||
Recent and important updates:
|
Recent and important updates:
|
||||||
|
|
||||||
[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).
|
[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).
|
[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).
|
||||||
@@ -301,7 +299,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.
|
11. Transport isolation - different TCP connections and Tor circuits are used for traffic of different user profiles, optionally - for different contacts and group member connections.
|
||||||
12. Manual messaging queue rotations to move conversation to another SMP relay.
|
12. Manual messaging queue rotations to move conversation to another SMP relay.
|
||||||
13. Sending end-to-end encrypted files using [XFTP protocol](https://simplex.chat/blog/20230301-simplex-file-transfer-protocol.html).
|
13. Sending end-to-end encrypted files using [XFTP protocol](https://simplex.chat/blog/20230301-simplex-file-transfer-protocol.html).
|
||||||
14. Local files encryption.
|
14. Local files encryption, except videos (to be added later).
|
||||||
|
|
||||||
We plan to add:
|
We plan to add:
|
||||||
|
|
||||||
@@ -373,13 +371,12 @@ Please also join [#simplex-devs](https://simplex.chat/contact#/?v=1-2&smp=smp%3A
|
|||||||
- ✅ Desktop client.
|
- ✅ Desktop client.
|
||||||
- ✅ Encryption of local files stored in the app.
|
- ✅ Encryption of local files stored in the app.
|
||||||
- ✅ Using mobile profiles from the desktop app.
|
- ✅ Using mobile profiles from the desktop app.
|
||||||
- ✅ Private notes.
|
|
||||||
- ✅ Improve sending videos (including encryption of locally stored videos).
|
|
||||||
- 🏗 Improve experience for the new users.
|
- 🏗 Improve experience for the new users.
|
||||||
- 🏗 Post-quantum resistant key exchange in double ratchet protocol.
|
- 🏗 Post-quantum resistant key exchange in double ratchet protocol.
|
||||||
- 🏗 Large groups, communities and public channels.
|
- 🏗 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.
|
- Privacy & security slider - a simple way to set all settings at once.
|
||||||
|
- Improve sending videos (including encryption of locally stored videos).
|
||||||
- SMP queue redundancy and rotation (manual is supported).
|
- SMP queue redundancy and rotation (manual is supported).
|
||||||
- Include optional message into connection request sent via contact address.
|
- Include optional message into connection request sent via contact address.
|
||||||
- Improved navigation and search in the conversation (expand and scroll to quoted message, scroll to search results, etc.).
|
- Improved navigation and search in the conversation (expand and scroll to quoted message, scroll to search results, etc.).
|
||||||
|
|||||||
@@ -16,7 +16,6 @@ class AppDelegate: NSObject, UIApplicationDelegate {
|
|||||||
application.registerForRemoteNotifications()
|
application.registerForRemoteNotifications()
|
||||||
if #available(iOS 17.0, *) { trackKeyboard() }
|
if #available(iOS 17.0, *) { trackKeyboard() }
|
||||||
NotificationCenter.default.addObserver(self, selector: #selector(pasteboardChanged), name: UIPasteboard.changedNotification, object: nil)
|
NotificationCenter.default.addObserver(self, selector: #selector(pasteboardChanged), name: UIPasteboard.changedNotification, object: nil)
|
||||||
removePasscodesIfReinstalled()
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -128,19 +127,6 @@ class AppDelegate: NSObject, UIApplicationDelegate {
|
|||||||
BGManager.shared.receiveMessages(complete)
|
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) {
|
static func keepScreenOn(_ on: Bool) {
|
||||||
UIApplication.shared.isIdleTimerDisabled = on
|
UIApplication.shared.isIdleTimerDisabled = on
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,8 +34,6 @@ struct ContentView: View {
|
|||||||
@State private var waitingForOrPassedAuth = true
|
@State private var waitingForOrPassedAuth = true
|
||||||
@State private var chatListActionSheet: ChatListActionSheet? = nil
|
@State private var chatListActionSheet: ChatListActionSheet? = nil
|
||||||
|
|
||||||
private let callTopPadding: CGFloat = 50
|
|
||||||
|
|
||||||
private enum ChatListActionSheet: Identifiable {
|
private enum ChatListActionSheet: Identifiable {
|
||||||
case planAndConnectSheet(sheet: PlanAndConnectActionSheet)
|
case planAndConnectSheet(sheet: PlanAndConnectActionSheet)
|
||||||
|
|
||||||
@@ -52,28 +50,16 @@ struct ContentView: View {
|
|||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
ZStack {
|
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.
|
// 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() }
|
// i.e. with separate branches like this settings are closed: `if prefPerformLA { ... contentView() ... } else { contentView() }
|
||||||
if !prefPerformLA || accessAuthenticated {
|
if !prefPerformLA || accessAuthenticated {
|
||||||
contentView()
|
contentView()
|
||||||
.padding(.top, showCallArea ? callTopPadding : 0)
|
|
||||||
} else {
|
} else {
|
||||||
lockButton()
|
lockButton()
|
||||||
.padding(.top, showCallArea ? callTopPadding : 0)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if showCallArea, let call = chatModel.activeCall {
|
|
||||||
VStack {
|
|
||||||
activeCallInteractiveArea(call)
|
|
||||||
Spacer()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if chatModel.showCallView, let call = chatModel.activeCall {
|
if chatModel.showCallView, let call = chatModel.activeCall {
|
||||||
callView(call)
|
callView(call)
|
||||||
}
|
}
|
||||||
|
|
||||||
if !showSettings, let la = chatModel.laRequest {
|
if !showSettings, let la = chatModel.laRequest {
|
||||||
LocalAuthView(authRequest: la)
|
LocalAuthView(authRequest: la)
|
||||||
.onDisappear {
|
.onDisappear {
|
||||||
@@ -149,11 +135,11 @@ struct ContentView: View {
|
|||||||
if case .onboardingComplete = step,
|
if case .onboardingComplete = step,
|
||||||
chatModel.currentUser != nil {
|
chatModel.currentUser != nil {
|
||||||
mainView()
|
mainView()
|
||||||
.actionSheet(item: $chatListActionSheet) { sheet in
|
.actionSheet(item: $chatListActionSheet) { sheet in
|
||||||
switch sheet {
|
switch sheet {
|
||||||
case let .planAndConnectSheet(sheet): return planAndConnectActionSheet(sheet, dismiss: false)
|
case let .planAndConnectSheet(sheet): return planAndConnectActionSheet(sheet, dismiss: false)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
OnboardingView(onboarding: step)
|
OnboardingView(onboarding: step)
|
||||||
}
|
}
|
||||||
@@ -177,40 +163,6 @@ 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 {
|
private func lockButton() -> some View {
|
||||||
Button(action: authenticateContentViewAccess) { Label("Unlock", systemImage: "lock") }
|
Button(action: authenticateContentViewAccess) { Label("Unlock", systemImage: "lock") }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -80,7 +80,6 @@ final class ChatModel: ObservableObject {
|
|||||||
@Published var tokenRegistered = false
|
@Published var tokenRegistered = false
|
||||||
@Published var tokenStatus: NtfTknStatus?
|
@Published var tokenStatus: NtfTknStatus?
|
||||||
@Published var notificationMode = NotificationsMode.off
|
@Published var notificationMode = NotificationsMode.off
|
||||||
@Published var notificationServer: String?
|
|
||||||
@Published var notificationPreview: NotificationPreviewMode = ntfPreviewModeGroupDefault.get()
|
@Published var notificationPreview: NotificationPreviewMode = ntfPreviewModeGroupDefault.get()
|
||||||
// pending notification actions
|
// pending notification actions
|
||||||
@Published var ntfContactRequest: NTFContactRequest?
|
@Published var ntfContactRequest: NTFContactRequest?
|
||||||
@@ -90,7 +89,6 @@ final class ChatModel: ObservableObject {
|
|||||||
@Published var activeCall: Call?
|
@Published var activeCall: Call?
|
||||||
let callCommand: WebRTCCommandProcessor = WebRTCCommandProcessor()
|
let callCommand: WebRTCCommandProcessor = WebRTCCommandProcessor()
|
||||||
@Published var showCallView = false
|
@Published var showCallView = false
|
||||||
@Published var activeCallViewIsCollapsed = false
|
|
||||||
// remote desktop
|
// remote desktop
|
||||||
@Published var remoteCtrlSession: RemoteCtrlSession?
|
@Published var remoteCtrlSession: RemoteCtrlSession?
|
||||||
// currently showing invitation
|
// currently showing invitation
|
||||||
|
|||||||
@@ -252,24 +252,18 @@ func apiSetFilesFolder(filesFolder: String) throws {
|
|||||||
throw r
|
throw r
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func setXFTPConfig(_ cfg: XFTPFileConfig?) throws {
|
||||||
|
let r = chatSendCmdSync(.apiSetXFTPConfig(config: cfg))
|
||||||
|
if case .cmdOk = r { return }
|
||||||
|
throw r
|
||||||
|
}
|
||||||
|
|
||||||
func apiSetEncryptLocalFiles(_ enable: Bool) throws {
|
func apiSetEncryptLocalFiles(_ enable: Bool) throws {
|
||||||
let r = chatSendCmdSync(.apiSetEncryptLocalFiles(enable: enable))
|
let r = chatSendCmdSync(.apiSetEncryptLocalFiles(enable: enable))
|
||||||
if case .cmdOk = r { return }
|
if case .cmdOk = r { return }
|
||||||
throw r
|
throw r
|
||||||
}
|
}
|
||||||
|
|
||||||
func apiSetPQEnabled(_ enable: Bool) throws {
|
|
||||||
let r = chatSendCmdSync(.apiSetPQEnabled(enable: enable))
|
|
||||||
if case .cmdOk = r { return }
|
|
||||||
throw r
|
|
||||||
}
|
|
||||||
|
|
||||||
func apiAllowContactPQ(_ contactId: Int64) async throws -> Contact {
|
|
||||||
let r = await chatSendCmd(.apiAllowContactPQ(contactId: contactId))
|
|
||||||
if case let .contactPQAllowed(_, contact) = r { return contact }
|
|
||||||
throw r
|
|
||||||
}
|
|
||||||
|
|
||||||
func apiExportArchive(config: ArchiveConfig) async throws {
|
func apiExportArchive(config: ArchiveConfig) async throws {
|
||||||
try await sendCommandOkResp(.apiExportArchive(config: config))
|
try await sendCommandOkResp(.apiExportArchive(config: config))
|
||||||
}
|
}
|
||||||
@@ -418,14 +412,14 @@ func apiDeleteMemberChatItem(groupId: Int64, groupMemberId: Int64, itemId: Int64
|
|||||||
throw r
|
throw r
|
||||||
}
|
}
|
||||||
|
|
||||||
func apiGetNtfToken() -> (DeviceToken?, NtfTknStatus?, NotificationsMode, String?) {
|
func apiGetNtfToken() -> (DeviceToken?, NtfTknStatus?, NotificationsMode) {
|
||||||
let r = chatSendCmdSync(.apiGetNtfToken)
|
let r = chatSendCmdSync(.apiGetNtfToken)
|
||||||
switch r {
|
switch r {
|
||||||
case let .ntfToken(token, status, ntfMode, ntfServer): return (token, status, ntfMode, ntfServer)
|
case let .ntfToken(token, status, ntfMode): return (token, status, ntfMode)
|
||||||
case .chatCmdError(_, .errorAgent(.CMD(.PROHIBITED))): return (nil, nil, .off, nil)
|
case .chatCmdError(_, .errorAgent(.CMD(.PROHIBITED))): return (nil, nil, .off)
|
||||||
default:
|
default:
|
||||||
logger.debug("apiGetNtfToken response: \(String(describing: r))")
|
logger.debug("apiGetNtfToken response: \(String(describing: r))")
|
||||||
return (nil, nil, .off, nil)
|
return (nil, nil, .off)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1178,7 +1172,7 @@ func filterMembersToAdd(_ ms: [GMember]) -> [Contact] {
|
|||||||
let memberContactIds = ms.compactMap{ m in m.wrapped.memberCurrent ? m.wrapped.memberContactId : nil }
|
let memberContactIds = ms.compactMap{ m in m.wrapped.memberCurrent ? m.wrapped.memberContactId : nil }
|
||||||
return ChatModel.shared.chats
|
return ChatModel.shared.chats
|
||||||
.compactMap{ $0.chatInfo.contact }
|
.compactMap{ $0.chatInfo.contact }
|
||||||
.filter{ c in c.ready && c.active && !memberContactIds.contains(c.apiId) }
|
.filter{ !memberContactIds.contains($0.apiId) }
|
||||||
.sorted{ $0.displayName.lowercased() < $1.displayName.lowercased() }
|
.sorted{ $0.displayName.lowercased() < $1.displayName.lowercased() }
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1255,8 +1249,8 @@ func initializeChat(start: Bool, confirmStart: Bool = false, dbKey: String? = ni
|
|||||||
}
|
}
|
||||||
try apiSetTempFolder(tempFolder: getTempFilesDirectory().path)
|
try apiSetTempFolder(tempFolder: getTempFilesDirectory().path)
|
||||||
try apiSetFilesFolder(filesFolder: getAppFilesDirectory().path)
|
try apiSetFilesFolder(filesFolder: getAppFilesDirectory().path)
|
||||||
|
try setXFTPConfig(getXFTPCfg())
|
||||||
try apiSetEncryptLocalFiles(privacyEncryptLocalFilesGroupDefault.get())
|
try apiSetEncryptLocalFiles(privacyEncryptLocalFilesGroupDefault.get())
|
||||||
try apiSetPQEnabled(pqExperimentalEnabledDefault.get())
|
|
||||||
m.chatInitialized = true
|
m.chatInitialized = true
|
||||||
m.currentUser = try apiGetActiveUser()
|
m.currentUser = try apiGetActiveUser()
|
||||||
if m.currentUser == nil {
|
if m.currentUser == nil {
|
||||||
@@ -1315,7 +1309,7 @@ func startChat(refreshInvitations: Bool = true) throws {
|
|||||||
if (refreshInvitations) {
|
if (refreshInvitations) {
|
||||||
try refreshCallInvitations()
|
try refreshCallInvitations()
|
||||||
}
|
}
|
||||||
(m.savedToken, m.tokenStatus, m.notificationMode, m.notificationServer) = apiGetNtfToken()
|
(m.savedToken, m.tokenStatus, m.notificationMode) = apiGetNtfToken()
|
||||||
// deviceToken is set when AppDelegate.application(didRegisterForRemoteNotificationsWithDeviceToken:) is called,
|
// deviceToken is set when AppDelegate.application(didRegisterForRemoteNotificationsWithDeviceToken:) is called,
|
||||||
// when it is called before startChat
|
// when it is called before startChat
|
||||||
if let token = m.deviceToken {
|
if let token = m.deviceToken {
|
||||||
@@ -1831,12 +1825,6 @@ func processReceivedMsg(_ res: ChatResponse) async {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
case let .contactPQEnabled(user, contact, _):
|
|
||||||
if active(user) {
|
|
||||||
await MainActor.run {
|
|
||||||
m.updateContact(contact) // or updateContactConnectionStats?
|
|
||||||
}
|
|
||||||
}
|
|
||||||
default:
|
default:
|
||||||
logger.debug("unsupported event: \(res.responseType)")
|
logger.debug("unsupported event: \(res.responseType)")
|
||||||
}
|
}
|
||||||
@@ -1873,9 +1861,7 @@ func chatItemSimpleUpdate(_ user: any UserLike, _ aChatItem: AChatItem) async {
|
|||||||
let cItem = aChatItem.chatItem
|
let cItem = aChatItem.chatItem
|
||||||
if active(user) {
|
if active(user) {
|
||||||
if await MainActor.run(body: { m.upsertChatItem(cInfo, cItem) }) {
|
if await MainActor.run(body: { m.upsertChatItem(cInfo, cItem) }) {
|
||||||
if cItem.showNotification {
|
NtfManager.shared.notifyMessageReceived(user, cInfo, cItem)
|
||||||
NtfManager.shared.notifyMessageReceived(user, cInfo, cItem)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,67 +12,49 @@ import SimpleXChat
|
|||||||
|
|
||||||
struct ActiveCallView: View {
|
struct ActiveCallView: View {
|
||||||
@EnvironmentObject var m: ChatModel
|
@EnvironmentObject var m: ChatModel
|
||||||
@Environment(\.colorScheme) var colorScheme
|
|
||||||
@ObservedObject var call: Call
|
@ObservedObject var call: Call
|
||||||
@Environment(\.scenePhase) var scenePhase
|
@Environment(\.scenePhase) var scenePhase
|
||||||
@State private var client: WebRTCClient? = nil
|
@State private var client: WebRTCClient? = nil
|
||||||
@State private var activeCall: WebRTCClient.Call? = nil
|
@State private var activeCall: WebRTCClient.Call? = nil
|
||||||
@State private var localRendererAspectRatio: CGFloat? = nil
|
@State private var localRendererAspectRatio: CGFloat? = nil
|
||||||
@Binding var canConnectCall: Bool
|
@Binding var canConnectCall: Bool
|
||||||
@State var prevColorScheme: ColorScheme = .dark
|
|
||||||
@State var pipShown = false
|
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
ZStack(alignment: .topLeading) {
|
ZStack(alignment: .bottom) {
|
||||||
ZStack(alignment: .bottom) {
|
if let client = client, [call.peerMedia, call.localMedia].contains(.video), activeCall != nil {
|
||||||
if let client = client, [call.peerMedia, call.localMedia].contains(.video), activeCall != nil {
|
GeometryReader { g in
|
||||||
GeometryReader { g in
|
let width = g.size.width * 0.3
|
||||||
let width = g.size.width * 0.3
|
ZStack(alignment: .topTrailing) {
|
||||||
ZStack(alignment: .topTrailing) {
|
CallViewRemote(client: client, activeCall: $activeCall)
|
||||||
CallViewRemote(client: client, activeCall: $activeCall, activeCallViewIsCollapsed: $m.activeCallViewIsCollapsed, pipShown: $pipShown)
|
CallViewLocal(client: client, activeCall: $activeCall, localRendererAspectRatio: $localRendererAspectRatio)
|
||||||
CallViewLocal(client: client, activeCall: $activeCall, localRendererAspectRatio: $localRendererAspectRatio, pipShown: $pipShown)
|
.cornerRadius(10)
|
||||||
.cornerRadius(10)
|
.frame(width: width, height: width / (localRendererAspectRatio ?? 1))
|
||||||
.frame(width: width, height: width / (localRendererAspectRatio ?? 1))
|
.padding([.top, .trailing], 17)
|
||||||
.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, (!pipShown || !call.supportsVideo) {
|
}
|
||||||
ActiveCallOverlay(call: call, client: client)
|
if let call = m.activeCall, let client = client {
|
||||||
}
|
ActiveCallOverlay(call: call, client: client)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.allowsHitTesting(!m.activeCallViewIsCollapsed)
|
|
||||||
.opacity(m.activeCallViewIsCollapsed ? 0 : 1)
|
|
||||||
.onAppear {
|
.onAppear {
|
||||||
logger.debug("ActiveCallView: appear client is nil \(client == nil), scenePhase \(String(describing: scenePhase)), canConnectCall \(canConnectCall)")
|
logger.debug("ActiveCallView: appear client is nil \(client == nil), scenePhase \(String(describing: scenePhase)), canConnectCall \(canConnectCall)")
|
||||||
AppDelegate.keepScreenOn(true)
|
AppDelegate.keepScreenOn(true)
|
||||||
createWebRTCClient()
|
createWebRTCClient()
|
||||||
dismissAllSheets()
|
dismissAllSheets()
|
||||||
hideKeyboard()
|
|
||||||
prevColorScheme = colorScheme
|
|
||||||
}
|
}
|
||||||
.onChange(of: canConnectCall) { _ in
|
.onChange(of: canConnectCall) { _ in
|
||||||
logger.debug("ActiveCallView: canConnectCall changed to \(canConnectCall)")
|
logger.debug("ActiveCallView: canConnectCall changed to \(canConnectCall)")
|
||||||
createWebRTCClient()
|
createWebRTCClient()
|
||||||
}
|
}
|
||||||
.onChange(of: m.activeCallViewIsCollapsed) { _ in
|
|
||||||
hideKeyboard()
|
|
||||||
}
|
|
||||||
.onDisappear {
|
.onDisappear {
|
||||||
logger.debug("ActiveCallView: disappear")
|
logger.debug("ActiveCallView: disappear")
|
||||||
Task { await m.callCommand.setClient(nil) }
|
Task { await m.callCommand.setClient(nil) }
|
||||||
AppDelegate.keepScreenOn(false)
|
AppDelegate.keepScreenOn(false)
|
||||||
client?.endCall()
|
client?.endCall()
|
||||||
}
|
}
|
||||||
.background(m.activeCallViewIsCollapsed ? .clear : .black)
|
.background(.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(.dark)
|
||||||
.preferredColorScheme(m.activeCallViewIsCollapsed || CallController.useCallKit() ? prevColorScheme : .dark)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private func createWebRTCClient() {
|
private func createWebRTCClient() {
|
||||||
@@ -87,8 +69,8 @@ struct ActiveCallView: View {
|
|||||||
@MainActor
|
@MainActor
|
||||||
private func processRtcMessage(msg: WVAPIMessage) {
|
private func processRtcMessage(msg: WVAPIMessage) {
|
||||||
if call == m.activeCall,
|
if call == m.activeCall,
|
||||||
let call = m.activeCall,
|
let call = m.activeCall,
|
||||||
let client = client {
|
let client = client {
|
||||||
logger.debug("ActiveCallView: response \(msg.resp.respType)")
|
logger.debug("ActiveCallView: response \(msg.resp.respType)")
|
||||||
switch msg.resp {
|
switch msg.resp {
|
||||||
case let .capabilities(capabilities):
|
case let .capabilities(capabilities):
|
||||||
@@ -108,7 +90,7 @@ struct ActiveCallView: View {
|
|||||||
Task {
|
Task {
|
||||||
do {
|
do {
|
||||||
try await apiSendCallOffer(call.contact, offer, iceCandidates,
|
try await apiSendCallOffer(call.contact, offer, iceCandidates,
|
||||||
media: call.localMedia, capabilities: capabilities)
|
media: call.localMedia, capabilities: capabilities)
|
||||||
} catch {
|
} catch {
|
||||||
logger.error("apiSendCallOffer \(responseError(error))")
|
logger.error("apiSendCallOffer \(responseError(error))")
|
||||||
}
|
}
|
||||||
@@ -140,15 +122,13 @@ struct ActiveCallView: View {
|
|||||||
if let callStatus = WebRTCCallStatus.init(rawValue: state.connectionState),
|
if let callStatus = WebRTCCallStatus.init(rawValue: state.connectionState),
|
||||||
case .connected = callStatus {
|
case .connected = callStatus {
|
||||||
call.direction == .outgoing
|
call.direction == .outgoing
|
||||||
? CallController.shared.reportOutgoingCall(call: call, connectedAt: nil)
|
? CallController.shared.reportOutgoingCall(call: call, connectedAt: nil)
|
||||||
: CallController.shared.reportIncomingCall(call: call, connectedAt: nil)
|
: CallController.shared.reportIncomingCall(call: call, connectedAt: nil)
|
||||||
call.callState = .connected
|
call.callState = .connected
|
||||||
call.connectedAt = .now
|
|
||||||
}
|
}
|
||||||
if state.connectionState == "closed" {
|
if state.connectionState == "closed" {
|
||||||
closeCallView(client)
|
closeCallView(client)
|
||||||
m.activeCall = nil
|
m.activeCall = nil
|
||||||
m.activeCallViewIsCollapsed = false
|
|
||||||
}
|
}
|
||||||
Task {
|
Task {
|
||||||
do {
|
do {
|
||||||
@@ -160,7 +140,6 @@ struct ActiveCallView: View {
|
|||||||
case let .connected(connectionInfo):
|
case let .connected(connectionInfo):
|
||||||
call.callState = .connected
|
call.callState = .connected
|
||||||
call.connectionInfo = connectionInfo
|
call.connectionInfo = connectionInfo
|
||||||
call.connectedAt = .now
|
|
||||||
case .ended:
|
case .ended:
|
||||||
closeCallView(client)
|
closeCallView(client)
|
||||||
call.callState = .ended
|
call.callState = .ended
|
||||||
@@ -174,7 +153,6 @@ struct ActiveCallView: View {
|
|||||||
case .end:
|
case .end:
|
||||||
closeCallView(client)
|
closeCallView(client)
|
||||||
m.activeCall = nil
|
m.activeCall = nil
|
||||||
m.activeCallViewIsCollapsed = false
|
|
||||||
default: ()
|
default: ()
|
||||||
}
|
}
|
||||||
case let .error(message):
|
case let .error(message):
|
||||||
@@ -203,7 +181,7 @@ struct ActiveCallOverlay: View {
|
|||||||
VStack {
|
VStack {
|
||||||
switch call.localMedia {
|
switch call.localMedia {
|
||||||
case .video:
|
case .video:
|
||||||
videoCallInfoView(call)
|
callInfoView(call, .leading)
|
||||||
.foregroundColor(.white)
|
.foregroundColor(.white)
|
||||||
.opacity(0.8)
|
.opacity(0.8)
|
||||||
.padding()
|
.padding()
|
||||||
@@ -230,25 +208,16 @@ struct ActiveCallOverlay: View {
|
|||||||
.frame(maxWidth: .infinity, alignment: .center)
|
.frame(maxWidth: .infinity, alignment: .center)
|
||||||
|
|
||||||
case .audio:
|
case .audio:
|
||||||
ZStack(alignment: .topLeading) {
|
VStack {
|
||||||
Button {
|
ProfileImage(imageStr: call.contact.profile.image)
|
||||||
chatModel.activeCallViewIsCollapsed = true
|
.scaledToFit()
|
||||||
} label: {
|
.frame(width: 192, height: 192)
|
||||||
Label("Back", systemImage: "chevron.left")
|
callInfoView(call, .center)
|
||||||
.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()
|
Spacer()
|
||||||
|
|
||||||
@@ -266,12 +235,12 @@ struct ActiveCallOverlay: View {
|
|||||||
.frame(maxWidth: .infinity)
|
.frame(maxWidth: .infinity)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func audioCallInfoView(_ call: Call) -> some View {
|
private func callInfoView(_ call: Call, _ alignment: Alignment) -> some View {
|
||||||
VStack {
|
VStack {
|
||||||
Text(call.contact.chatViewName)
|
Text(call.contact.chatViewName)
|
||||||
.lineLimit(1)
|
.lineLimit(1)
|
||||||
.font(.title)
|
.font(.title)
|
||||||
.frame(maxWidth: .infinity, alignment: .center)
|
.frame(maxWidth: .infinity, alignment: alignment)
|
||||||
Group {
|
Group {
|
||||||
Text(call.callState.text)
|
Text(call.callState.text)
|
||||||
HStack {
|
HStack {
|
||||||
@@ -282,36 +251,7 @@ struct ActiveCallOverlay: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
.font(.subheadline)
|
.font(.subheadline)
|
||||||
.frame(maxWidth: .infinity, alignment: .center)
|
.frame(maxWidth: .infinity, alignment: alignment)
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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,7 +92,6 @@ class CallManager {
|
|||||||
if case .ended = call.callState {
|
if case .ended = call.callState {
|
||||||
logger.debug("CallManager.endCall: call ended")
|
logger.debug("CallManager.endCall: call ended")
|
||||||
m.activeCall = nil
|
m.activeCall = nil
|
||||||
m.activeCallViewIsCollapsed = false
|
|
||||||
m.showCallView = false
|
m.showCallView = false
|
||||||
completed()
|
completed()
|
||||||
} else {
|
} else {
|
||||||
@@ -101,7 +100,6 @@ class CallManager {
|
|||||||
await m.callCommand.processCommand(.end)
|
await m.callCommand.processCommand(.end)
|
||||||
await MainActor.run {
|
await MainActor.run {
|
||||||
m.activeCall = nil
|
m.activeCall = nil
|
||||||
m.activeCallViewIsCollapsed = false
|
|
||||||
m.showCallView = false
|
m.showCallView = false
|
||||||
completed()
|
completed()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,20 +6,14 @@
|
|||||||
import SwiftUI
|
import SwiftUI
|
||||||
import WebRTC
|
import WebRTC
|
||||||
import SimpleXChat
|
import SimpleXChat
|
||||||
import AVKit
|
|
||||||
|
|
||||||
struct CallViewRemote: UIViewRepresentable {
|
struct CallViewRemote: UIViewRepresentable {
|
||||||
var client: WebRTCClient
|
var client: WebRTCClient
|
||||||
var activeCall: Binding<WebRTCClient.Call?>
|
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?>, activeCallViewIsCollapsed: Binding<Bool>, pipShown: Binding<Bool>) {
|
init(client: WebRTCClient, activeCall: Binding<WebRTCClient.Call?>) {
|
||||||
self.client = client
|
self.client = client
|
||||||
self.activeCall = activeCall
|
self.activeCall = activeCall
|
||||||
self._activeCallViewIsCollapsed = activeCallViewIsCollapsed
|
|
||||||
self._pipShown = pipShown
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func makeUIView(context: Context) -> UIView {
|
func makeUIView(context: Context) -> UIView {
|
||||||
@@ -29,120 +23,12 @@ struct CallViewRemote: UIViewRepresentable {
|
|||||||
remoteRenderer.videoContentMode = .scaleAspectFill
|
remoteRenderer.videoContentMode = .scaleAspectFill
|
||||||
client.addRemoteRenderer(call, remoteRenderer)
|
client.addRemoteRenderer(call, remoteRenderer)
|
||||||
addSubviewAndResize(remoteRenderer, into: view)
|
addSubviewAndResize(remoteRenderer, into: view)
|
||||||
|
|
||||||
if AVPictureInPictureController.isPictureInPictureSupported() {
|
|
||||||
makeViewWithRTCRenderer(call, remoteRenderer, view, context)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return view
|
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) {
|
func updateUIView(_ view: UIView, context: Context) {
|
||||||
logger.debug("CallView.updateUIView remote")
|
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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -150,14 +36,11 @@ struct CallViewLocal: UIViewRepresentable {
|
|||||||
var client: WebRTCClient
|
var client: WebRTCClient
|
||||||
var activeCall: Binding<WebRTCClient.Call?>
|
var activeCall: Binding<WebRTCClient.Call?>
|
||||||
var localRendererAspectRatio: Binding<CGFloat?>
|
var localRendererAspectRatio: Binding<CGFloat?>
|
||||||
@State var pipStateChanged: (Bool) -> Void = {_ in }
|
|
||||||
@Binding var pipShown: Bool
|
|
||||||
|
|
||||||
init(client: WebRTCClient, activeCall: Binding<WebRTCClient.Call?>, localRendererAspectRatio: Binding<CGFloat?>, pipShown: Binding<Bool>) {
|
init(client: WebRTCClient, activeCall: Binding<WebRTCClient.Call?>, localRendererAspectRatio: Binding<CGFloat?>) {
|
||||||
self.client = client
|
self.client = client
|
||||||
self.activeCall = activeCall
|
self.activeCall = activeCall
|
||||||
self.localRendererAspectRatio = localRendererAspectRatio
|
self.localRendererAspectRatio = localRendererAspectRatio
|
||||||
self._pipShown = pipShown
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func makeUIView(context: Context) -> UIView {
|
func makeUIView(context: Context) -> UIView {
|
||||||
@@ -167,18 +50,12 @@ struct CallViewLocal: UIViewRepresentable {
|
|||||||
client.addLocalRenderer(call, localRenderer)
|
client.addLocalRenderer(call, localRenderer)
|
||||||
client.startCaptureLocalVideo(call)
|
client.startCaptureLocalVideo(call)
|
||||||
addSubviewAndResize(localRenderer, into: view)
|
addSubviewAndResize(localRenderer, into: view)
|
||||||
DispatchQueue.main.async {
|
|
||||||
pipStateChanged = { shown in
|
|
||||||
localRenderer.isHidden = shown
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return view
|
return view
|
||||||
}
|
}
|
||||||
|
|
||||||
func updateUIView(_ view: UIView, context: Context) {
|
func updateUIView(_ view: UIView, context: Context) {
|
||||||
logger.debug("CallView.updateUIView local")
|
logger.debug("CallView.updateUIView local")
|
||||||
pipStateChanged(pipShown)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -28,7 +28,6 @@ class Call: ObservableObject, Equatable {
|
|||||||
@Published var speakerEnabled = false
|
@Published var speakerEnabled = false
|
||||||
@Published var videoEnabled: Bool
|
@Published var videoEnabled: Bool
|
||||||
@Published var connectionInfo: ConnectionInfo?
|
@Published var connectionInfo: ConnectionInfo?
|
||||||
@Published var connectedAt: Date? = nil
|
|
||||||
|
|
||||||
init(
|
init(
|
||||||
direction: CallDirection,
|
direction: CallDirection,
|
||||||
@@ -60,7 +59,6 @@ class Call: ObservableObject, Equatable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
var hasMedia: Bool { get { callState == .offerSent || callState == .negotiated || callState == .connected } }
|
var hasMedia: Bool { get { callState == .offerSent || callState == .negotiated || callState == .connected } }
|
||||||
var supportsVideo: Bool { get { peerMedia == .video || localMedia == .video } }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
enum CallDirection {
|
enum CallDirection {
|
||||||
|
|||||||
@@ -331,10 +331,6 @@ final class WebRTCClient: NSObject, RTCVideoViewDelegate, RTCFrameEncryptorDeleg
|
|||||||
activeCall.remoteStream?.add(renderer)
|
activeCall.remoteStream?.add(renderer)
|
||||||
}
|
}
|
||||||
|
|
||||||
func removeRemoteRenderer(_ activeCall: Call, _ renderer: RTCVideoRenderer) {
|
|
||||||
activeCall.remoteStream?.remove(renderer)
|
|
||||||
}
|
|
||||||
|
|
||||||
func startCaptureLocalVideo(_ activeCall: Call) {
|
func startCaptureLocalVideo(_ activeCall: Call) {
|
||||||
#if targetEnvironment(simulator)
|
#if targetEnvironment(simulator)
|
||||||
guard
|
guard
|
||||||
@@ -414,7 +410,6 @@ final class WebRTCClient: NSObject, RTCVideoViewDelegate, RTCFrameEncryptorDeleg
|
|||||||
guard let call = activeCall.wrappedValue else { return }
|
guard let call = activeCall.wrappedValue else { return }
|
||||||
logger.debug("WebRTCClient: ending the call")
|
logger.debug("WebRTCClient: ending the call")
|
||||||
activeCall.wrappedValue = nil
|
activeCall.wrappedValue = nil
|
||||||
(call.localCamera as? RTCCameraVideoCapturer)?.stopCapture()
|
|
||||||
call.connection.close()
|
call.connection.close()
|
||||||
call.connection.delegate = nil
|
call.connection.delegate = nil
|
||||||
call.frameEncryptor?.delegate = nil
|
call.frameEncryptor?.delegate = nil
|
||||||
|
|||||||
@@ -103,7 +103,6 @@ struct ChatInfoView: View {
|
|||||||
@State private var sendReceipts = SendReceipts.userDefault(true)
|
@State private var sendReceipts = SendReceipts.userDefault(true)
|
||||||
@State private var sendReceiptsUserDefault = true
|
@State private var sendReceiptsUserDefault = true
|
||||||
@AppStorage(DEFAULT_DEVELOPER_TOOLS) private var developerTools = false
|
@AppStorage(DEFAULT_DEVELOPER_TOOLS) private var developerTools = false
|
||||||
@AppStorage(GROUP_DEFAULT_PQ_EXPERIMENTAL_ENABLED, store: groupDefaults) private var pqExperimentalEnabled = false
|
|
||||||
|
|
||||||
enum ChatInfoViewAlert: Identifiable {
|
enum ChatInfoViewAlert: Identifiable {
|
||||||
case clearChatAlert
|
case clearChatAlert
|
||||||
@@ -111,7 +110,6 @@ struct ChatInfoView: View {
|
|||||||
case switchAddressAlert
|
case switchAddressAlert
|
||||||
case abortSwitchAddressAlert
|
case abortSwitchAddressAlert
|
||||||
case syncConnectionForceAlert
|
case syncConnectionForceAlert
|
||||||
case allowContactPQEncryptionAlert
|
|
||||||
case error(title: LocalizedStringKey, error: LocalizedStringKey = "")
|
case error(title: LocalizedStringKey, error: LocalizedStringKey = "")
|
||||||
|
|
||||||
var id: String {
|
var id: String {
|
||||||
@@ -121,7 +119,6 @@ struct ChatInfoView: View {
|
|||||||
case .switchAddressAlert: return "switchAddressAlert"
|
case .switchAddressAlert: return "switchAddressAlert"
|
||||||
case .abortSwitchAddressAlert: return "abortSwitchAddressAlert"
|
case .abortSwitchAddressAlert: return "abortSwitchAddressAlert"
|
||||||
case .syncConnectionForceAlert: return "syncConnectionForceAlert"
|
case .syncConnectionForceAlert: return "syncConnectionForceAlert"
|
||||||
case .allowContactPQEncryptionAlert: return "allowContactPQEncryptionAlert"
|
|
||||||
case let .error(title, _): return "error \(title)"
|
case let .error(title, _): return "error \(title)"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -168,22 +165,6 @@ struct ChatInfoView: View {
|
|||||||
}
|
}
|
||||||
.disabled(!contact.ready || !contact.active)
|
.disabled(!contact.ready || !contact.active)
|
||||||
|
|
||||||
if pqExperimentalEnabled,
|
|
||||||
let conn = contact.activeConn {
|
|
||||||
Section {
|
|
||||||
infoRow(Text(String("PQ E2E encryption")), conn.connPQEnabled ? "Enabled" : "Disabled")
|
|
||||||
if !conn.enablePQ {
|
|
||||||
allowPQButton()
|
|
||||||
}
|
|
||||||
} header: {
|
|
||||||
Text(String("Post-quantum E2E encryption"))
|
|
||||||
} footer: {
|
|
||||||
if !conn.enablePQ {
|
|
||||||
Text(String("After allowing post-quantum encryption, it will be enabled after several messages if your contact also allows it."))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if let contactLink = contact.contactLink {
|
if let contactLink = contact.contactLink {
|
||||||
Section {
|
Section {
|
||||||
SimpleXLinkQRCode(uri: contactLink)
|
SimpleXLinkQRCode(uri: contactLink)
|
||||||
@@ -256,7 +237,6 @@ struct ChatInfoView: View {
|
|||||||
case .switchAddressAlert: return switchAddressAlert(switchContactAddress)
|
case .switchAddressAlert: return switchAddressAlert(switchContactAddress)
|
||||||
case .abortSwitchAddressAlert: return abortSwitchAddressAlert(abortSwitchContactAddress)
|
case .abortSwitchAddressAlert: return abortSwitchAddressAlert(abortSwitchContactAddress)
|
||||||
case .syncConnectionForceAlert: return syncConnectionForceAlert({ syncContactConnection(force: true) })
|
case .syncConnectionForceAlert: return syncConnectionForceAlert({ syncContactConnection(force: true) })
|
||||||
case .allowContactPQEncryptionAlert: return allowContactPQEncryptionAlert()
|
|
||||||
case let .error(title, error): return mkAlert(title: title, message: error)
|
case let .error(title, error): return mkAlert(title: title, message: error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -430,15 +410,6 @@ 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 {
|
private func networkStatusRow() -> some View {
|
||||||
HStack {
|
HStack {
|
||||||
Text("Network status")
|
Text("Network status")
|
||||||
@@ -572,34 +543,6 @@ struct ChatInfoView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func allowContactPQEncryption() {
|
|
||||||
Task {
|
|
||||||
do {
|
|
||||||
let ct = try await apiAllowContactPQ(contact.apiId)
|
|
||||||
contact = ct
|
|
||||||
await MainActor.run {
|
|
||||||
chatModel.updateContact(contact)
|
|
||||||
dismiss()
|
|
||||||
}
|
|
||||||
} catch let error {
|
|
||||||
logger.error("allowContactPQEncryption apiAllowContactPQ 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 post-quantum encryption?")),
|
|
||||||
message: Text(String("This is an experimental feature, it is not recommended to enable it for high importance communications. It may result in connection errors!")),
|
|
||||||
primaryButton: .destructive(Text(String("Allow")), action: allowContactPQEncryption),
|
|
||||||
secondaryButton: .cancel()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func switchAddressAlert(_ switchAddress: @escaping () -> Void) -> Alert {
|
func switchAddressAlert(_ switchAddress: @escaping () -> Void) -> Alert {
|
||||||
|
|||||||
@@ -29,9 +29,6 @@ struct CIImageView: View {
|
|||||||
FullScreenMediaView(chatItem: chatItem, image: uiImage, showView: $showFullScreenImage, scrollProxy: scrollProxy)
|
FullScreenMediaView(chatItem: chatItem, image: uiImage, showView: $showFullScreenImage, scrollProxy: scrollProxy)
|
||||||
}
|
}
|
||||||
.onTapGesture { showFullScreenImage = true }
|
.onTapGesture { showFullScreenImage = true }
|
||||||
.onChange(of: m.activeCallViewIsCollapsed) { _ in
|
|
||||||
showFullScreenImage = false
|
|
||||||
}
|
|
||||||
} else if let data = Data(base64Encoded: dropImagePrefix(image)),
|
} else if let data = Data(base64Encoded: dropImagePrefix(image)),
|
||||||
let uiImage = UIImage(data: data) {
|
let uiImage = UIImage(data: data) {
|
||||||
imageView(uiImage)
|
imageView(uiImage)
|
||||||
|
|||||||
@@ -120,9 +120,6 @@ struct CIVideoView: View {
|
|||||||
showFullScreenPlayer = urlDecrypted != nil
|
showFullScreenPlayer = urlDecrypted != nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.onChange(of: m.activeCallViewIsCollapsed) { _ in
|
|
||||||
showFullScreenPlayer = false
|
|
||||||
}
|
|
||||||
if !decryptionInProgress {
|
if !decryptionInProgress {
|
||||||
Button {
|
Button {
|
||||||
decrypt(file: file) {
|
decrypt(file: file) {
|
||||||
@@ -171,9 +168,6 @@ struct CIVideoView: View {
|
|||||||
default: ()
|
default: ()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.onChange(of: m.activeCallViewIsCollapsed) { _ in
|
|
||||||
showFullScreenPlayer = false
|
|
||||||
}
|
|
||||||
if !videoPlaying {
|
if !videoPlaying {
|
||||||
Button {
|
Button {
|
||||||
m.stopPreviousRecPlay = url
|
m.stopPreviousRecPlay = url
|
||||||
|
|||||||
@@ -65,8 +65,6 @@ 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 {
|
var markedDeletedText: LocalizedStringKey {
|
||||||
switch chatItem.meta.itemDeleted {
|
switch chatItem.meta.itemDeleted {
|
||||||
case let .moderated(_, byGroupMember): "moderated by \(byGroupMember.displayName)"
|
case let .moderated(_, byGroupMember): "moderated by \(byGroupMember.displayName)"
|
||||||
|
|||||||
@@ -111,11 +111,6 @@ struct ChatItemContentView<Content: View>: View {
|
|||||||
case .rcvModerated: deletedItemView()
|
case .rcvModerated: deletedItemView()
|
||||||
case .rcvBlocked: deletedItemView()
|
case .rcvBlocked: deletedItemView()
|
||||||
case let .invalidJSON(json): CIInvalidJSONView(json: json)
|
case let .invalidJSON(json): CIInvalidJSONView(json: json)
|
||||||
// TODO proper items
|
|
||||||
case .sndDirectE2EEInfo: CIEventView(eventText: Text(chatItem.content.text))
|
|
||||||
case .rcvDirectE2EEInfo: CIEventView(eventText: Text(chatItem.content.text))
|
|
||||||
case .sndGroupE2EEInfo: CIEventView(eventText: Text(chatItem.content.text))
|
|
||||||
case .rcvGroupE2EEInfo: CIEventView(eventText: Text(chatItem.content.text))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -159,17 +159,12 @@ struct ChatView: View {
|
|||||||
switch cInfo {
|
switch cInfo {
|
||||||
case let .direct(contact):
|
case let .direct(contact):
|
||||||
HStack {
|
HStack {
|
||||||
let callsPrefEnabled = contact.mergedPreferences.calls.enabled.forUser
|
if contact.allowsFeature(.calls) {
|
||||||
if callsPrefEnabled {
|
callButton(contact, .audio, imageName: "phone")
|
||||||
if chatModel.activeCall == nil {
|
.disabled(!contact.ready || !contact.active)
|
||||||
callButton(contact, .audio, imageName: "phone")
|
|
||||||
.disabled(!contact.ready || !contact.active)
|
|
||||||
} else if let call = chatModel.activeCall, call.contact.id == cInfo.id {
|
|
||||||
endCallButton(call)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
Menu {
|
Menu {
|
||||||
if callsPrefEnabled && chatModel.activeCall == nil {
|
if contact.allowsFeature(.calls) {
|
||||||
Button {
|
Button {
|
||||||
CallController.shared.startCall(contact, .video)
|
CallController.shared.startCall(contact, .video)
|
||||||
} label: {
|
} label: {
|
||||||
@@ -427,18 +422,6 @@ struct ChatView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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 {
|
private func searchButton() -> some View {
|
||||||
Button {
|
Button {
|
||||||
searchMode = true
|
searchMode = true
|
||||||
@@ -765,9 +748,7 @@ struct ChatView: View {
|
|||||||
if ci.meta.editable && !mc.isVoice && !live {
|
if ci.meta.editable && !mc.isVoice && !live {
|
||||||
menu.append(editAction(ci))
|
menu.append(editAction(ci))
|
||||||
}
|
}
|
||||||
if !ci.isLiveDummy {
|
menu.append(viewInfoUIAction(ci))
|
||||||
menu.append(viewInfoUIAction(ci))
|
|
||||||
}
|
|
||||||
if revealed {
|
if revealed {
|
||||||
menu.append(hideUIAction())
|
menu.append(hideUIAction())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -978,9 +978,6 @@ struct ComposeView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private func cancelLinkPreview() {
|
private func cancelLinkPreview() {
|
||||||
if let pendingLink = pendingLinkUrl?.absoluteString {
|
|
||||||
cancelledLinks.insert(pendingLink)
|
|
||||||
}
|
|
||||||
if let uri = composeState.linkPreview?.uri.absoluteString {
|
if let uri = composeState.linkPreview?.uri.absoluteString {
|
||||||
cancelledLinks.insert(uri)
|
cancelledLinks.insert(uri)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -235,28 +235,38 @@ struct GroupChatInfoView: View {
|
|||||||
memberInfo(member)
|
memberInfo(member)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// revert from this:
|
||||||
if user {
|
if user {
|
||||||
v
|
v
|
||||||
} else if groupInfo.membership.memberRole >= .admin {
|
} else if member.canBeRemoved(groupInfo: groupInfo) {
|
||||||
// TODO if there are more actions, refactor with lists of swipeActions
|
removeSwipe(member, blockSwipe(member, v))
|
||||||
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 {
|
} else {
|
||||||
if !member.blockedByAdmin {
|
blockSwipe(member, v)
|
||||||
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 {
|
@ViewBuilder private func memberInfo(_ member: GroupMember) -> some View {
|
||||||
@@ -360,11 +370,7 @@ struct GroupChatInfoView: View {
|
|||||||
|
|
||||||
private func addOrEditWelcomeMessage() -> some View {
|
private func addOrEditWelcomeMessage() -> some View {
|
||||||
NavigationLink {
|
NavigationLink {
|
||||||
GroupWelcomeView(
|
GroupWelcomeView(groupId: groupInfo.groupId, groupInfo: $groupInfo)
|
||||||
groupInfo: $groupInfo,
|
|
||||||
groupProfile: groupInfo.groupProfile,
|
|
||||||
welcomeText: groupInfo.groupProfile.description ?? ""
|
|
||||||
)
|
|
||||||
.navigationTitle("Welcome message")
|
.navigationTitle("Welcome message")
|
||||||
.navigationBarTitleDisplayMode(.large)
|
.navigationBarTitleDisplayMode(.large)
|
||||||
} label: {
|
} label: {
|
||||||
|
|||||||
@@ -168,11 +168,24 @@ struct GroupMemberInfoView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if groupInfo.membership.memberRole >= .admin {
|
// revert from this:
|
||||||
adminDestructiveSection(member)
|
Section {
|
||||||
} else {
|
if member.memberSettings.showMessages {
|
||||||
nonAdminBlockSection(member)
|
blockMemberButton(member)
|
||||||
|
} else {
|
||||||
|
unblockMemberButton(member)
|
||||||
|
}
|
||||||
|
if member.canBeRemoved(groupInfo: groupInfo) {
|
||||||
|
removeMemberButton(member)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
// revert to this: vvv
|
||||||
|
// if groupInfo.membership.memberRole >= .admin {
|
||||||
|
// adminDestructiveSection(member)
|
||||||
|
// } else {
|
||||||
|
// nonAdminBlockSection(member)
|
||||||
|
// }
|
||||||
|
// ^^^
|
||||||
|
|
||||||
if developerTools {
|
if developerTools {
|
||||||
Section("For console") {
|
Section("For console") {
|
||||||
|
|||||||
@@ -11,32 +11,29 @@ import SimpleXChat
|
|||||||
|
|
||||||
struct GroupWelcomeView: View {
|
struct GroupWelcomeView: View {
|
||||||
@Environment(\.dismiss) var dismiss: DismissAction
|
@Environment(\.dismiss) var dismiss: DismissAction
|
||||||
|
@EnvironmentObject private var m: ChatModel
|
||||||
|
var groupId: Int64
|
||||||
@Binding var groupInfo: GroupInfo
|
@Binding var groupInfo: GroupInfo
|
||||||
@State var groupProfile: GroupProfile
|
@State private var welcomeText: String = ""
|
||||||
@State var welcomeText: String
|
|
||||||
@State private var editMode = true
|
@State private var editMode = true
|
||||||
@FocusState private var keyboardVisible: Bool
|
@FocusState private var keyboardVisible: Bool
|
||||||
@State private var showSaveDialog = false
|
@State private var showSaveDialog = false
|
||||||
|
|
||||||
let maxByteCount = 1200
|
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack {
|
VStack {
|
||||||
if groupInfo.canEdit {
|
if groupInfo.canEdit {
|
||||||
editorView()
|
editorView()
|
||||||
.modifier(BackButton {
|
.modifier(BackButton {
|
||||||
if welcomeTextUnchanged() {
|
if welcomeText == groupInfo.groupProfile.description || (welcomeText == "" && groupInfo.groupProfile.description == nil) {
|
||||||
dismiss()
|
dismiss()
|
||||||
} else {
|
} else {
|
||||||
showSaveDialog = true
|
showSaveDialog = true
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.confirmationDialog(
|
.confirmationDialog("Save welcome message?", isPresented: $showSaveDialog) {
|
||||||
welcomeTextFitsLimit() ? "Save welcome message?" : "Welcome message is too long",
|
Button("Save and update group profile") {
|
||||||
isPresented: $showSaveDialog
|
save()
|
||||||
) {
|
dismiss()
|
||||||
if welcomeTextFitsLimit() {
|
|
||||||
Button("Save and update group profile") { save() }
|
|
||||||
}
|
}
|
||||||
Button("Exit without saving") { dismiss() }
|
Button("Exit without saving") { dismiss() }
|
||||||
}
|
}
|
||||||
@@ -50,15 +47,14 @@ struct GroupWelcomeView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
.onAppear {
|
.onAppear {
|
||||||
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
|
welcomeText = groupInfo.groupProfile.description ?? ""
|
||||||
keyboardVisible = true
|
keyboardVisible = true
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func textPreview() -> some View {
|
private func textPreview() -> some View {
|
||||||
messageText(welcomeText, parseSimpleXMarkdown(welcomeText), nil, showSecrets: false)
|
messageText(welcomeText, parseSimpleXMarkdown(welcomeText), nil, showSecrets: false)
|
||||||
.frame(minHeight: 130, alignment: .topLeading)
|
.frame(minHeight: 140, alignment: .topLeading)
|
||||||
.frame(maxWidth: .infinity, alignment: .leading)
|
.frame(maxWidth: .infinity, alignment: .leading)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -78,7 +74,7 @@ struct GroupWelcomeView: View {
|
|||||||
}
|
}
|
||||||
.padding(.horizontal, -5)
|
.padding(.horizontal, -5)
|
||||||
.padding(.top, -8)
|
.padding(.top, -8)
|
||||||
.frame(height: 130, alignment: .topLeading)
|
.frame(height: 140, alignment: .topLeading)
|
||||||
.frame(maxWidth: .infinity, alignment: .leading)
|
.frame(maxWidth: .infinity, alignment: .leading)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -97,9 +93,6 @@ struct GroupWelcomeView: View {
|
|||||||
}
|
}
|
||||||
.disabled(welcomeText.isEmpty)
|
.disabled(welcomeText.isEmpty)
|
||||||
copyButton()
|
copyButton()
|
||||||
} footer: {
|
|
||||||
Text(!welcomeTextFitsLimit() ? "Message too large" : "")
|
|
||||||
.foregroundColor(.red)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Section {
|
Section {
|
||||||
@@ -120,15 +113,7 @@ struct GroupWelcomeView: View {
|
|||||||
Button("Save and update group profile") {
|
Button("Save and update group profile") {
|
||||||
save()
|
save()
|
||||||
}
|
}
|
||||||
.disabled(welcomeTextUnchanged() || !welcomeTextFitsLimit())
|
.disabled(welcomeText == groupInfo.groupProfile.description || (welcomeText == "" && groupInfo.groupProfile.description == nil))
|
||||||
}
|
|
||||||
|
|
||||||
private func welcomeTextUnchanged() -> Bool {
|
|
||||||
welcomeText == groupInfo.groupProfile.description || (welcomeText == "" && groupInfo.groupProfile.description == nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
private func welcomeTextFitsLimit() -> Bool {
|
|
||||||
chatJsonLength(welcomeText) <= maxByteCount
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private func save() {
|
private func save() {
|
||||||
@@ -138,13 +123,11 @@ struct GroupWelcomeView: View {
|
|||||||
if welcome?.count == 0 {
|
if welcome?.count == 0 {
|
||||||
welcome = nil
|
welcome = nil
|
||||||
}
|
}
|
||||||
groupProfile.description = welcome
|
var groupProfileUpdated = groupInfo.groupProfile
|
||||||
let gInfo = try await apiUpdateGroup(groupInfo.groupId, groupProfile)
|
groupProfileUpdated.description = welcome
|
||||||
await MainActor.run {
|
groupInfo = try await apiUpdateGroup(groupId, groupProfileUpdated)
|
||||||
groupInfo = gInfo
|
m.updateGroup(groupInfo)
|
||||||
ChatModel.shared.updateGroup(gInfo)
|
welcomeText = welcome ?? ""
|
||||||
dismiss()
|
|
||||||
}
|
|
||||||
} catch let error {
|
} catch let error {
|
||||||
logger.error("apiUpdateGroup error: \(responseError(error))")
|
logger.error("apiUpdateGroup error: \(responseError(error))")
|
||||||
}
|
}
|
||||||
@@ -154,6 +137,6 @@ struct GroupWelcomeView: View {
|
|||||||
|
|
||||||
struct GroupWelcomeView_Previews: PreviewProvider {
|
struct GroupWelcomeView_Previews: PreviewProvider {
|
||||||
static var previews: some View {
|
static var previews: some View {
|
||||||
GroupProfileView(groupInfo: Binding.constant(GroupInfo.sampleData), groupProfile: GroupProfile.sampleData)
|
GroupWelcomeView(groupId: 1, groupInfo: Binding.constant(GroupInfo.sampleData))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ struct ChatPreviewView: View {
|
|||||||
HStack(alignment: .top) {
|
HStack(alignment: .top) {
|
||||||
chatPreviewTitle()
|
chatPreviewTitle()
|
||||||
Spacer()
|
Spacer()
|
||||||
(cItem?.timestampText ?? formatTimestampText(chat.chatInfo.chatTs))
|
(cItem?.timestampText ?? formatTimestampText(chat.chatInfo.updatedAt))
|
||||||
.font(.subheadline)
|
.font(.subheadline)
|
||||||
.frame(minWidth: 60, alignment: .trailing)
|
.frame(minWidth: 60, alignment: .trailing)
|
||||||
.foregroundColor(.secondary)
|
.foregroundColor(.secondary)
|
||||||
@@ -171,21 +171,10 @@ struct ChatPreviewView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func chatItemPreview(_ cItem: ChatItem) -> Text {
|
func chatItemPreview(_ cItem: ChatItem) -> Text {
|
||||||
let itemText = cItem.meta.itemDeleted == nil ? cItem.text : markedDeletedText()
|
let itemText = cItem.meta.itemDeleted == nil ? cItem.text : NSLocalizedString("marked deleted", comment: "marked deleted chat item preview text")
|
||||||
let itemFormattedText = cItem.meta.itemDeleted == nil ? cItem.formattedText : nil
|
let itemFormattedText = cItem.meta.itemDeleted == nil ? cItem.formattedText : nil
|
||||||
return messageText(itemText, itemFormattedText, cItem.memberDisplayName, icon: attachment(), preview: true, showSecrets: false)
|
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? {
|
func attachment() -> String? {
|
||||||
switch cItem.content.msgContent {
|
switch cItem.content.msgContent {
|
||||||
case .file: return "doc.fill"
|
case .file: return "doc.fill"
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ import SimpleXChat
|
|||||||
struct DeveloperView: View {
|
struct DeveloperView: View {
|
||||||
@AppStorage(DEFAULT_DEVELOPER_TOOLS) private var developerTools = false
|
@AppStorage(DEFAULT_DEVELOPER_TOOLS) private var developerTools = false
|
||||||
@AppStorage(GROUP_DEFAULT_CONFIRM_DB_UPGRADES, store: groupDefaults) private var confirmDatabaseUpgrades = 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
|
@Environment(\.colorScheme) var colorScheme
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
@@ -44,32 +43,27 @@ struct DeveloperView: View {
|
|||||||
(developerTools ? Text("Show:") : Text("Hide:")) + Text(" ") + Text("Database IDs and Transport isolation option.")
|
(developerTools ? Text("Show:") : Text("Hide:")) + Text(" ") + Text("Database IDs and Transport isolation option.")
|
||||||
}
|
}
|
||||||
|
|
||||||
if developerTools {
|
// Section {
|
||||||
Section {
|
// settingsRow("arrow.up.doc") {
|
||||||
settingsRow("key") {
|
// Toggle("Send videos and files via XFTP", isOn: $xftpSendEnabled)
|
||||||
Toggle("Post-quantum E2EE", isOn: $pqExperimentalEnabled)
|
// .onChange(of: xftpSendEnabled) { _ in
|
||||||
.onChange(of: pqExperimentalEnabled) {
|
// do {
|
||||||
setPQExperimentalEnabled($0)
|
// try setXFTPConfig(getXFTPCfg())
|
||||||
}
|
// } catch {
|
||||||
}
|
// logger.error("setXFTPConfig: cannot set XFTP config \(responseError(error))")
|
||||||
} header: {
|
// }
|
||||||
Text(String("Experimental"))
|
// }
|
||||||
} footer: {
|
// }
|
||||||
Text(String("In this version applies only to new contacts."))
|
// } header: {
|
||||||
}
|
// Text("Experimental")
|
||||||
}
|
// } footer: {
|
||||||
|
// if xftpSendEnabled {
|
||||||
|
// Text("v4.6.1+ is required to receive via XFTP.")
|
||||||
|
// }
|
||||||
|
// }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func setPQExperimentalEnabled(_ enable: Bool) {
|
|
||||||
do {
|
|
||||||
try apiSetPQEnabled(enable)
|
|
||||||
} catch let error {
|
|
||||||
let err = responseError(error)
|
|
||||||
logger.error("apiSetPQEnabled \(err)")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
struct DeveloperView_Previews: PreviewProvider {
|
struct DeveloperView_Previews: PreviewProvider {
|
||||||
|
|||||||
@@ -76,10 +76,6 @@ struct NotificationsView: View {
|
|||||||
Text(m.notificationPreview.label)
|
Text(m.notificationPreview.label)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if let server = m.notificationServer {
|
|
||||||
smpServers("Push server", [server])
|
|
||||||
}
|
|
||||||
} header: {
|
} header: {
|
||||||
Text("Push notifications")
|
Text("Push notifications")
|
||||||
} footer: {
|
} footer: {
|
||||||
@@ -91,9 +87,6 @@ struct NotificationsView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
.disabled(legacyDatabase)
|
.disabled(legacyDatabase)
|
||||||
.onAppear {
|
|
||||||
(m.savedToken, m.tokenStatus, m.notificationMode, m.notificationServer) = apiGetNtfToken()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private func notificationAlert(_ alert: NotificationAlert, _ token: DeviceToken) -> Alert {
|
private func notificationAlert(_ alert: NotificationAlert, _ token: DeviceToken) -> Alert {
|
||||||
@@ -132,7 +125,6 @@ struct NotificationsView: View {
|
|||||||
m.tokenStatus = .new
|
m.tokenStatus = .new
|
||||||
notificationMode = .off
|
notificationMode = .off
|
||||||
m.notificationMode = .off
|
m.notificationMode = .off
|
||||||
m.notificationServer = nil
|
|
||||||
}
|
}
|
||||||
} catch let error {
|
} catch let error {
|
||||||
await MainActor.run {
|
await MainActor.run {
|
||||||
@@ -143,13 +135,11 @@ struct NotificationsView: View {
|
|||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
do {
|
do {
|
||||||
let _ = try await apiRegisterToken(token: token, notificationMode: mode)
|
let status = try await apiRegisterToken(token: token, notificationMode: mode)
|
||||||
let (_, tknStatus, ntfMode, ntfServer) = apiGetNtfToken()
|
|
||||||
await MainActor.run {
|
await MainActor.run {
|
||||||
m.tokenStatus = tknStatus
|
m.tokenStatus = status
|
||||||
notificationMode = ntfMode
|
notificationMode = mode
|
||||||
m.notificationMode = ntfMode
|
m.notificationMode = mode
|
||||||
m.notificationServer = ntfServer
|
|
||||||
}
|
}
|
||||||
} catch let error {
|
} catch let error {
|
||||||
await MainActor.run {
|
await MainActor.run {
|
||||||
|
|||||||
@@ -219,7 +219,6 @@
|
|||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="%lld messages blocked by admin" xml:space="preserve">
|
<trans-unit id="%lld messages blocked by admin" xml:space="preserve">
|
||||||
<source>%lld messages blocked by admin</source>
|
<source>%lld messages blocked by admin</source>
|
||||||
<target>%lld Nachrichten wurden vom Administrator blockiert</target>
|
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="%lld messages marked deleted" xml:space="preserve">
|
<trans-unit id="%lld messages marked deleted" xml:space="preserve">
|
||||||
@@ -645,7 +644,6 @@
|
|||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="All messages will be deleted - this cannot be undone!" xml:space="preserve">
|
<trans-unit id="All messages will be deleted - this cannot be undone!" xml:space="preserve">
|
||||||
<source>All messages will be deleted - this cannot be undone!</source>
|
<source>All messages will be deleted - this cannot be undone!</source>
|
||||||
<target>Es werden alle Nachrichten gelöscht. Dieser Vorgang kann nicht rückgängig gemacht werden!</target>
|
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</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">
|
||||||
@@ -655,7 +653,7 @@
|
|||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="All new messages from %@ will be hidden!" xml:space="preserve">
|
<trans-unit id="All new messages from %@ will be hidden!" xml:space="preserve">
|
||||||
<source>All new messages from %@ will be hidden!</source>
|
<source>All new messages from %@ will be hidden!</source>
|
||||||
<target>Von %@ werden alle neuen Nachrichten ausgeblendet!</target>
|
<target>Alle neuen Nachrichten von %@ werden verborgen!</target>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="All your contacts will remain connected." xml:space="preserve">
|
<trans-unit id="All your contacts will remain connected." xml:space="preserve">
|
||||||
@@ -925,7 +923,6 @@
|
|||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Block for all" xml:space="preserve">
|
<trans-unit id="Block for all" xml:space="preserve">
|
||||||
<source>Block for all</source>
|
<source>Block for all</source>
|
||||||
<target>Für Alle blockieren</target>
|
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Block group members" xml:space="preserve">
|
<trans-unit id="Block group members" xml:space="preserve">
|
||||||
@@ -940,7 +937,6 @@
|
|||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Block member for all?" xml:space="preserve">
|
<trans-unit id="Block member for all?" xml:space="preserve">
|
||||||
<source>Block member for all?</source>
|
<source>Block member for all?</source>
|
||||||
<target>Mitglied für Alle blockieren?</target>
|
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Block member?" xml:space="preserve">
|
<trans-unit id="Block member?" xml:space="preserve">
|
||||||
@@ -950,7 +946,6 @@
|
|||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Blocked by admin" xml:space="preserve">
|
<trans-unit id="Blocked by admin" xml:space="preserve">
|
||||||
<source>Blocked by admin</source>
|
<source>Blocked by admin</source>
|
||||||
<target>wurde vom Administrator blockiert</target>
|
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Both you and your contact can add message reactions." xml:space="preserve">
|
<trans-unit id="Both you and your contact can add message reactions." xml:space="preserve">
|
||||||
@@ -1166,7 +1161,6 @@
|
|||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Clear private notes?" xml:space="preserve">
|
<trans-unit id="Clear private notes?" xml:space="preserve">
|
||||||
<source>Clear private notes?</source>
|
<source>Clear private notes?</source>
|
||||||
<target>Private Notizen löschen?</target>
|
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Clear verification" xml:space="preserve">
|
<trans-unit id="Clear verification" xml:space="preserve">
|
||||||
@@ -1465,12 +1459,10 @@ Das ist Ihr eigener Einmal-Link!</target>
|
|||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Created at" xml:space="preserve">
|
<trans-unit id="Created at" xml:space="preserve">
|
||||||
<source>Created at</source>
|
<source>Created at</source>
|
||||||
<target>Erstellt um</target>
|
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Created at: %@" xml:space="preserve">
|
<trans-unit id="Created at: %@" xml:space="preserve">
|
||||||
<source>Created at: %@</source>
|
<source>Created at: %@</source>
|
||||||
<target>Erstellt um: %@</target>
|
|
||||||
<note>copied message info</note>
|
<note>copied message info</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Created on %@" xml:space="preserve">
|
<trans-unit id="Created on %@" xml:space="preserve">
|
||||||
@@ -2260,7 +2252,6 @@ Das kann nicht rückgängig gemacht werden!</target>
|
|||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Error creating message" xml:space="preserve">
|
<trans-unit id="Error creating message" xml:space="preserve">
|
||||||
<source>Error creating message</source>
|
<source>Error creating message</source>
|
||||||
<target>Fehler beim Erstellen der Nachricht</target>
|
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Error creating profile!" xml:space="preserve">
|
<trans-unit id="Error creating profile!" xml:space="preserve">
|
||||||
@@ -2935,7 +2926,6 @@ Das kann nicht rückgängig gemacht werden!</target>
|
|||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Improved message delivery" xml:space="preserve">
|
<trans-unit id="Improved message delivery" xml:space="preserve">
|
||||||
<source>Improved message delivery</source>
|
<source>Improved message delivery</source>
|
||||||
<target>Verbesserte Zustellung von Nachrichten</target>
|
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Improved privacy and security" xml:space="preserve">
|
<trans-unit id="Improved privacy and security" xml:space="preserve">
|
||||||
@@ -3163,7 +3153,6 @@ Das kann nicht rückgängig gemacht werden!</target>
|
|||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Join group conversations" xml:space="preserve">
|
<trans-unit id="Join group conversations" xml:space="preserve">
|
||||||
<source>Join group conversations</source>
|
<source>Join group conversations</source>
|
||||||
<target>Gruppenunterhaltungen beitreten</target>
|
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Join group?" xml:space="preserve">
|
<trans-unit id="Join group?" xml:space="preserve">
|
||||||
@@ -3874,7 +3863,6 @@ Das ist Ihr Link für die Gruppe %@!</target>
|
|||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Past member %@" xml:space="preserve">
|
<trans-unit id="Past member %@" xml:space="preserve">
|
||||||
<source>Past member %@</source>
|
<source>Past member %@</source>
|
||||||
<target>Ehemaliges Mitglied %@</target>
|
|
||||||
<note>past/unknown group member</note>
|
<note>past/unknown group member</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Paste desktop address" xml:space="preserve">
|
<trans-unit id="Paste desktop address" xml:space="preserve">
|
||||||
@@ -3889,7 +3877,6 @@ Das ist Ihr Link für die Gruppe %@!</target>
|
|||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Paste link to connect!" xml:space="preserve">
|
<trans-unit id="Paste link to connect!" xml:space="preserve">
|
||||||
<source>Paste link to connect!</source>
|
<source>Paste link to connect!</source>
|
||||||
<target>Zum Verbinden den Link einfügen!</target>
|
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Paste the link you received" xml:space="preserve">
|
<trans-unit id="Paste the link you received" xml:space="preserve">
|
||||||
@@ -4026,7 +4013,6 @@ Fehler: %@</target>
|
|||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Private notes" xml:space="preserve">
|
<trans-unit id="Private notes" xml:space="preserve">
|
||||||
<source>Private notes</source>
|
<source>Private notes</source>
|
||||||
<target>Private Notizen</target>
|
|
||||||
<note>name of notes to self</note>
|
<note>name of notes to self</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Profile and server connections" xml:space="preserve">
|
<trans-unit id="Profile and server connections" xml:space="preserve">
|
||||||
@@ -4211,7 +4197,6 @@ Fehler: %@</target>
|
|||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="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)." xml:space="preserve">
|
<trans-unit id="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)." xml:space="preserve">
|
||||||
<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>
|
<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>
|
||||||
<target>Aktueller Nachrichtenverlauf und verbesserter [Gruppenverzeichnis-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).</target>
|
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Recipients see updates as you type them." xml:space="preserve">
|
<trans-unit id="Recipients see updates as you type them." xml:space="preserve">
|
||||||
@@ -4501,7 +4486,6 @@ Fehler: %@</target>
|
|||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Saved message" xml:space="preserve">
|
<trans-unit id="Saved message" xml:space="preserve">
|
||||||
<source>Saved message</source>
|
<source>Saved message</source>
|
||||||
<target>Gespeicherte Nachricht</target>
|
|
||||||
<note>message info title</note>
|
<note>message info title</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Scan QR code" xml:space="preserve">
|
<trans-unit id="Scan QR code" xml:space="preserve">
|
||||||
@@ -4536,7 +4520,6 @@ Fehler: %@</target>
|
|||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Search bar accepts invitation links." xml:space="preserve">
|
<trans-unit id="Search bar accepts invitation links." xml:space="preserve">
|
||||||
<source>Search bar accepts invitation links.</source>
|
<source>Search bar accepts invitation links.</source>
|
||||||
<target>Von der Suchleiste werden Einladungslinks akzeptiert.</target>
|
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Search or paste SimpleX link" xml:space="preserve">
|
<trans-unit id="Search or paste SimpleX link" xml:space="preserve">
|
||||||
@@ -5375,7 +5358,6 @@ Sie werden aufgefordert, die Authentifizierung abzuschließen, bevor diese Funkt
|
|||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Turkish interface" xml:space="preserve">
|
<trans-unit id="Turkish interface" xml:space="preserve">
|
||||||
<source>Turkish interface</source>
|
<source>Turkish interface</source>
|
||||||
<target>Türkische Bedienoberfläche</target>
|
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Turn off" xml:space="preserve">
|
<trans-unit id="Turn off" xml:space="preserve">
|
||||||
@@ -5400,7 +5382,6 @@ Sie werden aufgefordert, die Authentifizierung abzuschließen, bevor diese Funkt
|
|||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Unblock for all" xml:space="preserve">
|
<trans-unit id="Unblock for all" xml:space="preserve">
|
||||||
<source>Unblock for all</source>
|
<source>Unblock for all</source>
|
||||||
<target>Für Alle freigeben</target>
|
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Unblock member" xml:space="preserve">
|
<trans-unit id="Unblock member" xml:space="preserve">
|
||||||
@@ -5410,7 +5391,6 @@ Sie werden aufgefordert, die Authentifizierung abzuschließen, bevor diese Funkt
|
|||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Unblock member for all?" xml:space="preserve">
|
<trans-unit id="Unblock member for all?" xml:space="preserve">
|
||||||
<source>Unblock member for all?</source>
|
<source>Unblock member for all?</source>
|
||||||
<target>Mitglied für Alle freigeben?</target>
|
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Unblock member?" xml:space="preserve">
|
<trans-unit id="Unblock member?" xml:space="preserve">
|
||||||
@@ -5777,7 +5757,6 @@ Bitten Sie Ihren Kontakt darum einen weiteren Verbindungs-Link zu erzeugen, um s
|
|||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="With encrypted files and media." xml:space="preserve">
|
<trans-unit id="With encrypted files and media." xml:space="preserve">
|
||||||
<source>With encrypted files and media.</source>
|
<source>With encrypted files and media.</source>
|
||||||
<target>Mit verschlüsselten Dateien und Medien.</target>
|
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="With optional welcome message." xml:space="preserve">
|
<trans-unit id="With optional welcome message." xml:space="preserve">
|
||||||
@@ -5787,7 +5766,6 @@ Bitten Sie Ihren Kontakt darum einen weiteren Verbindungs-Link zu erzeugen, um s
|
|||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="With reduced battery usage." xml:space="preserve">
|
<trans-unit id="With reduced battery usage." xml:space="preserve">
|
||||||
<source>With reduced battery usage.</source>
|
<source>With reduced battery usage.</source>
|
||||||
<target>Mit reduziertem Akkuverbrauch.</target>
|
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Wrong database passphrase" xml:space="preserve">
|
<trans-unit id="Wrong database passphrase" xml:space="preserve">
|
||||||
@@ -6290,17 +6268,15 @@ SimpleX-Server können Ihr Profil nicht einsehen.</target>
|
|||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="blocked" xml:space="preserve">
|
<trans-unit id="blocked" xml:space="preserve">
|
||||||
<source>blocked</source>
|
<source>blocked</source>
|
||||||
<target>Blockiert</target>
|
<target>blockiert</target>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="blocked %@" xml:space="preserve">
|
<trans-unit id="blocked %@" xml:space="preserve">
|
||||||
<source>blocked %@</source>
|
<source>blocked %@</source>
|
||||||
<target>%@ wurde blockiert</target>
|
|
||||||
<note>rcv group event chat item</note>
|
<note>rcv group event chat item</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="blocked by admin" xml:space="preserve">
|
<trans-unit id="blocked by admin" xml:space="preserve">
|
||||||
<source>blocked by admin</source>
|
<source>blocked by admin</source>
|
||||||
<target>wurde vom Administrator blockiert</target>
|
|
||||||
<note>blocked chat item</note>
|
<note>blocked chat item</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="bold" xml:space="preserve">
|
<trans-unit id="bold" xml:space="preserve">
|
||||||
@@ -6425,7 +6401,6 @@ SimpleX-Server können Ihr Profil nicht einsehen.</target>
|
|||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="contact %@ changed to %@" xml:space="preserve">
|
<trans-unit id="contact %@ changed to %@" xml:space="preserve">
|
||||||
<source>contact %1$@ changed to %2$@</source>
|
<source>contact %1$@ changed to %2$@</source>
|
||||||
<target>Der Kontaktname %1$@ wurde auf %2$@ geändert</target>
|
|
||||||
<note>profile update event chat item</note>
|
<note>profile update event chat item</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="contact has e2e encryption" xml:space="preserve">
|
<trans-unit id="contact has e2e encryption" xml:space="preserve">
|
||||||
@@ -6700,7 +6675,6 @@ SimpleX-Server können Ihr Profil nicht einsehen.</target>
|
|||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="member %@ changed to %@" xml:space="preserve">
|
<trans-unit id="member %@ changed to %@" xml:space="preserve">
|
||||||
<source>member %1$@ changed to %2$@</source>
|
<source>member %1$@ changed to %2$@</source>
|
||||||
<target>Der Mitgliedsname %1$@ wurde auf %2$@ geändert</target>
|
|
||||||
<note>profile update event chat item</note>
|
<note>profile update event chat item</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="member connected" xml:space="preserve">
|
<trans-unit id="member connected" xml:space="preserve">
|
||||||
@@ -6827,12 +6801,10 @@ SimpleX-Server können Ihr Profil nicht einsehen.</target>
|
|||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="removed contact address" xml:space="preserve">
|
<trans-unit id="removed contact address" xml:space="preserve">
|
||||||
<source>removed contact address</source>
|
<source>removed contact address</source>
|
||||||
<target>Kontaktadresse wurde entfernt</target>
|
|
||||||
<note>profile update event chat item</note>
|
<note>profile update event chat item</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="removed profile picture" xml:space="preserve">
|
<trans-unit id="removed profile picture" xml:space="preserve">
|
||||||
<source>removed profile picture</source>
|
<source>removed profile picture</source>
|
||||||
<target>Profil-Bild wurde entfernt</target>
|
|
||||||
<note>profile update event chat item</note>
|
<note>profile update event chat item</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="removed you" xml:space="preserve">
|
<trans-unit id="removed you" xml:space="preserve">
|
||||||
@@ -6867,12 +6839,10 @@ SimpleX-Server können Ihr Profil nicht einsehen.</target>
|
|||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="set new contact address" xml:space="preserve">
|
<trans-unit id="set new contact address" xml:space="preserve">
|
||||||
<source>set new contact address</source>
|
<source>set new contact address</source>
|
||||||
<target>Neue Kontaktadresse wurde festgelegt</target>
|
|
||||||
<note>profile update event chat item</note>
|
<note>profile update event chat item</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="set new profile picture" xml:space="preserve">
|
<trans-unit id="set new profile picture" xml:space="preserve">
|
||||||
<source>set new profile picture</source>
|
<source>set new profile picture</source>
|
||||||
<target>Neues Profil-Bild wurde festgelegt</target>
|
|
||||||
<note>profile update event chat item</note>
|
<note>profile update event chat item</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="starting…" xml:space="preserve">
|
<trans-unit id="starting…" xml:space="preserve">
|
||||||
@@ -6892,7 +6862,6 @@ SimpleX-Server können Ihr Profil nicht einsehen.</target>
|
|||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="unblocked %@" xml:space="preserve">
|
<trans-unit id="unblocked %@" xml:space="preserve">
|
||||||
<source>unblocked %@</source>
|
<source>unblocked %@</source>
|
||||||
<target>%@ wurde freigegeben</target>
|
|
||||||
<note>rcv group event chat item</note>
|
<note>rcv group event chat item</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="unknown" xml:space="preserve">
|
<trans-unit id="unknown" xml:space="preserve">
|
||||||
@@ -6902,7 +6871,6 @@ SimpleX-Server können Ihr Profil nicht einsehen.</target>
|
|||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="unknown status" xml:space="preserve">
|
<trans-unit id="unknown status" xml:space="preserve">
|
||||||
<source>unknown status</source>
|
<source>unknown status</source>
|
||||||
<target>unbekannter Gruppenmitglieds-Status</target>
|
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="updated group profile" xml:space="preserve">
|
<trans-unit id="updated group profile" xml:space="preserve">
|
||||||
@@ -6912,7 +6880,6 @@ SimpleX-Server können Ihr Profil nicht einsehen.</target>
|
|||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="updated profile" xml:space="preserve">
|
<trans-unit id="updated profile" xml:space="preserve">
|
||||||
<source>updated profile</source>
|
<source>updated profile</source>
|
||||||
<target>Das Profil wurde aktualisiert</target>
|
|
||||||
<note>profile update event chat item</note>
|
<note>profile update event chat item</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="v%@" xml:space="preserve">
|
<trans-unit id="v%@" xml:space="preserve">
|
||||||
@@ -6987,7 +6954,6 @@ SimpleX-Server können Ihr Profil nicht einsehen.</target>
|
|||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="you blocked %@" xml:space="preserve">
|
<trans-unit id="you blocked %@" xml:space="preserve">
|
||||||
<source>you blocked %@</source>
|
<source>you blocked %@</source>
|
||||||
<target>Sie haben %@ blockiert</target>
|
|
||||||
<note>snd group event chat item</note>
|
<note>snd group event chat item</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="you changed address" xml:space="preserve">
|
<trans-unit id="you changed address" xml:space="preserve">
|
||||||
@@ -7032,7 +6998,6 @@ SimpleX-Server können Ihr Profil nicht einsehen.</target>
|
|||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="you unblocked %@" xml:space="preserve">
|
<trans-unit id="you unblocked %@" xml:space="preserve">
|
||||||
<source>you unblocked %@</source>
|
<source>you unblocked %@</source>
|
||||||
<target>Sie haben %@ freigegeben</target>
|
|
||||||
<note>snd group event chat item</note>
|
<note>snd group event chat item</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="you: " xml:space="preserve">
|
<trans-unit id="you: " xml:space="preserve">
|
||||||
|
|||||||
@@ -219,7 +219,6 @@
|
|||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="%lld messages blocked by admin" xml:space="preserve">
|
<trans-unit id="%lld messages blocked by admin" xml:space="preserve">
|
||||||
<source>%lld messages blocked by admin</source>
|
<source>%lld messages blocked by admin</source>
|
||||||
<target>%lld messaggi bloccati dall'amministratore</target>
|
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="%lld messages marked deleted" xml:space="preserve">
|
<trans-unit id="%lld messages marked deleted" xml:space="preserve">
|
||||||
@@ -925,7 +924,6 @@
|
|||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Block for all" xml:space="preserve">
|
<trans-unit id="Block for all" xml:space="preserve">
|
||||||
<source>Block for all</source>
|
<source>Block for all</source>
|
||||||
<target>Blocca per tutti</target>
|
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Block group members" xml:space="preserve">
|
<trans-unit id="Block group members" xml:space="preserve">
|
||||||
@@ -940,7 +938,6 @@
|
|||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Block member for all?" xml:space="preserve">
|
<trans-unit id="Block member for all?" xml:space="preserve">
|
||||||
<source>Block member for all?</source>
|
<source>Block member for all?</source>
|
||||||
<target>Bloccare il membro per tutti?</target>
|
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Block member?" xml:space="preserve">
|
<trans-unit id="Block member?" xml:space="preserve">
|
||||||
@@ -950,7 +947,6 @@
|
|||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Blocked by admin" xml:space="preserve">
|
<trans-unit id="Blocked by admin" xml:space="preserve">
|
||||||
<source>Blocked by admin</source>
|
<source>Blocked by admin</source>
|
||||||
<target>Bloccato dall'amministratore</target>
|
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Both you and your contact can add message reactions." xml:space="preserve">
|
<trans-unit id="Both you and your contact can add message reactions." xml:space="preserve">
|
||||||
@@ -5400,7 +5396,6 @@ Ti verrà chiesto di completare l'autenticazione prima di attivare questa funzio
|
|||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Unblock for all" xml:space="preserve">
|
<trans-unit id="Unblock for all" xml:space="preserve">
|
||||||
<source>Unblock for all</source>
|
<source>Unblock for all</source>
|
||||||
<target>Sblocca per tutti</target>
|
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Unblock member" xml:space="preserve">
|
<trans-unit id="Unblock member" xml:space="preserve">
|
||||||
@@ -5410,7 +5405,6 @@ Ti verrà chiesto di completare l'autenticazione prima di attivare questa funzio
|
|||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Unblock member for all?" xml:space="preserve">
|
<trans-unit id="Unblock member for all?" xml:space="preserve">
|
||||||
<source>Unblock member for all?</source>
|
<source>Unblock member for all?</source>
|
||||||
<target>Sbloccare il membro per tutti?</target>
|
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Unblock member?" xml:space="preserve">
|
<trans-unit id="Unblock member?" xml:space="preserve">
|
||||||
@@ -6295,12 +6289,10 @@ I server di SimpleX non possono vedere il tuo profilo.</target>
|
|||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="blocked %@" xml:space="preserve">
|
<trans-unit id="blocked %@" xml:space="preserve">
|
||||||
<source>blocked %@</source>
|
<source>blocked %@</source>
|
||||||
<target>ha bloccato %@</target>
|
|
||||||
<note>rcv group event chat item</note>
|
<note>rcv group event chat item</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="blocked by admin" xml:space="preserve">
|
<trans-unit id="blocked by admin" xml:space="preserve">
|
||||||
<source>blocked by admin</source>
|
<source>blocked by admin</source>
|
||||||
<target>bloccato dall'amministratore</target>
|
|
||||||
<note>blocked chat item</note>
|
<note>blocked chat item</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="bold" xml:space="preserve">
|
<trans-unit id="bold" xml:space="preserve">
|
||||||
@@ -6892,7 +6884,6 @@ I server di SimpleX non possono vedere il tuo profilo.</target>
|
|||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="unblocked %@" xml:space="preserve">
|
<trans-unit id="unblocked %@" xml:space="preserve">
|
||||||
<source>unblocked %@</source>
|
<source>unblocked %@</source>
|
||||||
<target>ha sbloccato %@</target>
|
|
||||||
<note>rcv group event chat item</note>
|
<note>rcv group event chat item</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="unknown" xml:space="preserve">
|
<trans-unit id="unknown" xml:space="preserve">
|
||||||
@@ -6987,7 +6978,6 @@ I server di SimpleX non possono vedere il tuo profilo.</target>
|
|||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="you blocked %@" xml:space="preserve">
|
<trans-unit id="you blocked %@" xml:space="preserve">
|
||||||
<source>you blocked %@</source>
|
<source>you blocked %@</source>
|
||||||
<target>hai bloccato %@</target>
|
|
||||||
<note>snd group event chat item</note>
|
<note>snd group event chat item</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="you changed address" xml:space="preserve">
|
<trans-unit id="you changed address" xml:space="preserve">
|
||||||
@@ -7032,7 +7022,6 @@ I server di SimpleX non possono vedere il tuo profilo.</target>
|
|||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="you unblocked %@" xml:space="preserve">
|
<trans-unit id="you unblocked %@" xml:space="preserve">
|
||||||
<source>you unblocked %@</source>
|
<source>you unblocked %@</source>
|
||||||
<target>hai sbloccato %@</target>
|
|
||||||
<note>snd group event chat item</note>
|
<note>snd group event chat item</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="you: " xml:space="preserve">
|
<trans-unit id="you: " xml:space="preserve">
|
||||||
|
|||||||
@@ -219,7 +219,6 @@
|
|||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="%lld messages blocked by admin" xml:space="preserve">
|
<trans-unit id="%lld messages blocked by admin" xml:space="preserve">
|
||||||
<source>%lld messages blocked by admin</source>
|
<source>%lld messages blocked by admin</source>
|
||||||
<target>%lld berichten geblokkeerd door beheerder</target>
|
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="%lld messages marked deleted" xml:space="preserve">
|
<trans-unit id="%lld messages marked deleted" xml:space="preserve">
|
||||||
@@ -925,7 +924,6 @@
|
|||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Block for all" xml:space="preserve">
|
<trans-unit id="Block for all" xml:space="preserve">
|
||||||
<source>Block for all</source>
|
<source>Block for all</source>
|
||||||
<target>Blokkeren voor iedereen</target>
|
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Block group members" xml:space="preserve">
|
<trans-unit id="Block group members" xml:space="preserve">
|
||||||
@@ -940,7 +938,6 @@
|
|||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Block member for all?" xml:space="preserve">
|
<trans-unit id="Block member for all?" xml:space="preserve">
|
||||||
<source>Block member for all?</source>
|
<source>Block member for all?</source>
|
||||||
<target>Lid voor iedereen blokkeren?</target>
|
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Block member?" xml:space="preserve">
|
<trans-unit id="Block member?" xml:space="preserve">
|
||||||
@@ -950,7 +947,6 @@
|
|||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Blocked by admin" xml:space="preserve">
|
<trans-unit id="Blocked by admin" xml:space="preserve">
|
||||||
<source>Blocked by admin</source>
|
<source>Blocked by admin</source>
|
||||||
<target>Geblokkeerd door beheerder</target>
|
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Both you and your contact can add message reactions." xml:space="preserve">
|
<trans-unit id="Both you and your contact can add message reactions." xml:space="preserve">
|
||||||
@@ -3163,7 +3159,7 @@ Dit kan niet ongedaan gemaakt worden!</target>
|
|||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Join group conversations" xml:space="preserve">
|
<trans-unit id="Join group conversations" xml:space="preserve">
|
||||||
<source>Join group conversations</source>
|
<source>Join group conversations</source>
|
||||||
<target>Neem deel aan groepsgesprekken</target>
|
<target>Neem deel aan groep gesprekken</target>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Join group?" xml:space="preserve">
|
<trans-unit id="Join group?" xml:space="preserve">
|
||||||
@@ -3889,7 +3885,7 @@ Dit is jouw link voor groep %@!</target>
|
|||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Paste link to connect!" xml:space="preserve">
|
<trans-unit id="Paste link to connect!" xml:space="preserve">
|
||||||
<source>Paste link to connect!</source>
|
<source>Paste link to connect!</source>
|
||||||
<target>Plak een link om te verbinden!</target>
|
<target>Plak link om te verbinden!</target>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Paste the link you received" xml:space="preserve">
|
<trans-unit id="Paste the link you received" xml:space="preserve">
|
||||||
@@ -4541,7 +4537,7 @@ Fout: %@</target>
|
|||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Search or paste SimpleX link" xml:space="preserve">
|
<trans-unit id="Search or paste SimpleX link" xml:space="preserve">
|
||||||
<source>Search or paste SimpleX link</source>
|
<source>Search or paste SimpleX link</source>
|
||||||
<target>Zoek of plak een SimpleX link</target>
|
<target>Zoek of plak de SimpleX link</target>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Secure queue" xml:space="preserve">
|
<trans-unit id="Secure queue" xml:space="preserve">
|
||||||
@@ -5400,7 +5396,6 @@ U wordt gevraagd de authenticatie te voltooien voordat deze functie wordt ingesc
|
|||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Unblock for all" xml:space="preserve">
|
<trans-unit id="Unblock for all" xml:space="preserve">
|
||||||
<source>Unblock for all</source>
|
<source>Unblock for all</source>
|
||||||
<target>Deblokkeer voor iedereen</target>
|
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Unblock member" xml:space="preserve">
|
<trans-unit id="Unblock member" xml:space="preserve">
|
||||||
@@ -5410,7 +5405,6 @@ U wordt gevraagd de authenticatie te voltooien voordat deze functie wordt ingesc
|
|||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Unblock member for all?" xml:space="preserve">
|
<trans-unit id="Unblock member for all?" xml:space="preserve">
|
||||||
<source>Unblock member for all?</source>
|
<source>Unblock member for all?</source>
|
||||||
<target>Lid voor iedereen deblokkeren?</target>
|
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Unblock member?" xml:space="preserve">
|
<trans-unit id="Unblock member?" xml:space="preserve">
|
||||||
@@ -6295,12 +6289,10 @@ SimpleX servers kunnen uw profiel niet zien.</target>
|
|||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="blocked %@" xml:space="preserve">
|
<trans-unit id="blocked %@" xml:space="preserve">
|
||||||
<source>blocked %@</source>
|
<source>blocked %@</source>
|
||||||
<target>geblokkeerd %@</target>
|
|
||||||
<note>rcv group event chat item</note>
|
<note>rcv group event chat item</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="blocked by admin" xml:space="preserve">
|
<trans-unit id="blocked by admin" xml:space="preserve">
|
||||||
<source>blocked by admin</source>
|
<source>blocked by admin</source>
|
||||||
<target>geblokkeerd door beheerder</target>
|
|
||||||
<note>blocked chat item</note>
|
<note>blocked chat item</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="bold" xml:space="preserve">
|
<trans-unit id="bold" xml:space="preserve">
|
||||||
@@ -6892,7 +6884,6 @@ SimpleX servers kunnen uw profiel niet zien.</target>
|
|||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="unblocked %@" xml:space="preserve">
|
<trans-unit id="unblocked %@" xml:space="preserve">
|
||||||
<source>unblocked %@</source>
|
<source>unblocked %@</source>
|
||||||
<target>gedeblokkeerd %@</target>
|
|
||||||
<note>rcv group event chat item</note>
|
<note>rcv group event chat item</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="unknown" xml:space="preserve">
|
<trans-unit id="unknown" xml:space="preserve">
|
||||||
@@ -6987,7 +6978,6 @@ SimpleX servers kunnen uw profiel niet zien.</target>
|
|||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="you blocked %@" xml:space="preserve">
|
<trans-unit id="you blocked %@" xml:space="preserve">
|
||||||
<source>you blocked %@</source>
|
<source>you blocked %@</source>
|
||||||
<target>je hebt %@ geblokkeerd</target>
|
|
||||||
<note>snd group event chat item</note>
|
<note>snd group event chat item</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="you changed address" xml:space="preserve">
|
<trans-unit id="you changed address" xml:space="preserve">
|
||||||
@@ -7032,7 +7022,6 @@ SimpleX servers kunnen uw profiel niet zien.</target>
|
|||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="you unblocked %@" xml:space="preserve">
|
<trans-unit id="you unblocked %@" xml:space="preserve">
|
||||||
<source>you unblocked %@</source>
|
<source>you unblocked %@</source>
|
||||||
<target>je hebt %@ gedeblokkeerd</target>
|
|
||||||
<note>snd group event chat item</note>
|
<note>snd group event chat item</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="you: " xml:space="preserve">
|
<trans-unit id="you: " xml:space="preserve">
|
||||||
|
|||||||
@@ -219,7 +219,6 @@
|
|||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="%lld messages blocked by admin" xml:space="preserve">
|
<trans-unit id="%lld messages blocked by admin" xml:space="preserve">
|
||||||
<source>%lld messages blocked by admin</source>
|
<source>%lld messages blocked by admin</source>
|
||||||
<target>%lld wiadomości zablokowanych przez admina</target>
|
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="%lld messages marked deleted" xml:space="preserve">
|
<trans-unit id="%lld messages marked deleted" xml:space="preserve">
|
||||||
@@ -645,7 +644,6 @@
|
|||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="All messages will be deleted - this cannot be undone!" xml:space="preserve">
|
<trans-unit id="All messages will be deleted - this cannot be undone!" xml:space="preserve">
|
||||||
<source>All messages will be deleted - this cannot be undone!</source>
|
<source>All messages will be deleted - this cannot be undone!</source>
|
||||||
<target>Wszystkie wiadomości zostaną usunięte – nie można tego cofnąć!</target>
|
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</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">
|
||||||
@@ -925,7 +923,6 @@
|
|||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Block for all" xml:space="preserve">
|
<trans-unit id="Block for all" xml:space="preserve">
|
||||||
<source>Block for all</source>
|
<source>Block for all</source>
|
||||||
<target>Zablokuj dla wszystkich</target>
|
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Block group members" xml:space="preserve">
|
<trans-unit id="Block group members" xml:space="preserve">
|
||||||
@@ -940,7 +937,6 @@
|
|||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Block member for all?" xml:space="preserve">
|
<trans-unit id="Block member for all?" xml:space="preserve">
|
||||||
<source>Block member for all?</source>
|
<source>Block member for all?</source>
|
||||||
<target>Zablokować członka dla wszystkich?</target>
|
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Block member?" xml:space="preserve">
|
<trans-unit id="Block member?" xml:space="preserve">
|
||||||
@@ -950,7 +946,6 @@
|
|||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Blocked by admin" xml:space="preserve">
|
<trans-unit id="Blocked by admin" xml:space="preserve">
|
||||||
<source>Blocked by admin</source>
|
<source>Blocked by admin</source>
|
||||||
<target>Zablokowany przez admina</target>
|
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Both you and your contact can add message reactions." xml:space="preserve">
|
<trans-unit id="Both you and your contact can add message reactions." xml:space="preserve">
|
||||||
@@ -1166,7 +1161,6 @@
|
|||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Clear private notes?" xml:space="preserve">
|
<trans-unit id="Clear private notes?" xml:space="preserve">
|
||||||
<source>Clear private notes?</source>
|
<source>Clear private notes?</source>
|
||||||
<target>Wyczyścić prywatne notatki?</target>
|
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Clear verification" xml:space="preserve">
|
<trans-unit id="Clear verification" xml:space="preserve">
|
||||||
@@ -1465,12 +1459,10 @@ To jest twój jednorazowy link!</target>
|
|||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Created at" xml:space="preserve">
|
<trans-unit id="Created at" xml:space="preserve">
|
||||||
<source>Created at</source>
|
<source>Created at</source>
|
||||||
<target>Utworzony o</target>
|
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Created at: %@" xml:space="preserve">
|
<trans-unit id="Created at: %@" xml:space="preserve">
|
||||||
<source>Created at: %@</source>
|
<source>Created at: %@</source>
|
||||||
<target>Utworzony o: %@</target>
|
|
||||||
<note>copied message info</note>
|
<note>copied message info</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Created on %@" xml:space="preserve">
|
<trans-unit id="Created on %@" xml:space="preserve">
|
||||||
@@ -1965,7 +1957,6 @@ To nie może być cofnięte!</target>
|
|||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Do not send history to new members." xml:space="preserve">
|
<trans-unit id="Do not send history to new members." xml:space="preserve">
|
||||||
<source>Do not send history to new members.</source>
|
<source>Do not send history to new members.</source>
|
||||||
<target>Nie wysyłaj historii do nowych członków.</target>
|
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Don't create address" xml:space="preserve">
|
<trans-unit id="Don't create address" xml:space="preserve">
|
||||||
@@ -2260,7 +2251,6 @@ To nie może być cofnięte!</target>
|
|||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Error creating message" xml:space="preserve">
|
<trans-unit id="Error creating message" xml:space="preserve">
|
||||||
<source>Error creating message</source>
|
<source>Error creating message</source>
|
||||||
<target>Błąd tworzenia wiadomości</target>
|
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Error creating profile!" xml:space="preserve">
|
<trans-unit id="Error creating profile!" xml:space="preserve">
|
||||||
@@ -2840,7 +2830,6 @@ To nie może być cofnięte!</target>
|
|||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="History is not sent to new members." xml:space="preserve">
|
<trans-unit id="History is not sent to new members." xml:space="preserve">
|
||||||
<source>History is not sent to new members.</source>
|
<source>History is not sent to new members.</source>
|
||||||
<target>Historia nie jest wysyłana do nowych członków.</target>
|
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="How SimpleX works" xml:space="preserve">
|
<trans-unit id="How SimpleX works" xml:space="preserve">
|
||||||
@@ -2935,7 +2924,6 @@ To nie może być cofnięte!</target>
|
|||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Improved message delivery" xml:space="preserve">
|
<trans-unit id="Improved message delivery" xml:space="preserve">
|
||||||
<source>Improved message delivery</source>
|
<source>Improved message delivery</source>
|
||||||
<target>Ulepszona dostawa wiadomości</target>
|
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Improved privacy and security" xml:space="preserve">
|
<trans-unit id="Improved privacy and security" xml:space="preserve">
|
||||||
@@ -3052,7 +3040,6 @@ To nie może być cofnięte!</target>
|
|||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Invalid display name!" xml:space="preserve">
|
<trans-unit id="Invalid display name!" xml:space="preserve">
|
||||||
<source>Invalid display name!</source>
|
<source>Invalid display name!</source>
|
||||||
<target>Nieprawidłowa nazwa wyświetlana!</target>
|
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Invalid link" xml:space="preserve">
|
<trans-unit id="Invalid link" xml:space="preserve">
|
||||||
@@ -3163,7 +3150,6 @@ To nie może być cofnięte!</target>
|
|||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Join group conversations" xml:space="preserve">
|
<trans-unit id="Join group conversations" xml:space="preserve">
|
||||||
<source>Join group conversations</source>
|
<source>Join group conversations</source>
|
||||||
<target>Dołącz do grupowej rozmowy</target>
|
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Join group?" xml:space="preserve">
|
<trans-unit id="Join group?" xml:space="preserve">
|
||||||
@@ -3874,7 +3860,6 @@ To jest twój link do grupy %@!</target>
|
|||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Past member %@" xml:space="preserve">
|
<trans-unit id="Past member %@" xml:space="preserve">
|
||||||
<source>Past member %@</source>
|
<source>Past member %@</source>
|
||||||
<target>Były członek %@</target>
|
|
||||||
<note>past/unknown group member</note>
|
<note>past/unknown group member</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Paste desktop address" xml:space="preserve">
|
<trans-unit id="Paste desktop address" xml:space="preserve">
|
||||||
@@ -3889,7 +3874,6 @@ To jest twój link do grupy %@!</target>
|
|||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Paste link to connect!" xml:space="preserve">
|
<trans-unit id="Paste link to connect!" xml:space="preserve">
|
||||||
<source>Paste link to connect!</source>
|
<source>Paste link to connect!</source>
|
||||||
<target>Wklej link, aby połączyć!</target>
|
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Paste the link you received" xml:space="preserve">
|
<trans-unit id="Paste the link you received" xml:space="preserve">
|
||||||
@@ -4026,7 +4010,6 @@ Błąd: %@</target>
|
|||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Private notes" xml:space="preserve">
|
<trans-unit id="Private notes" xml:space="preserve">
|
||||||
<source>Private notes</source>
|
<source>Private notes</source>
|
||||||
<target>Prywatne notatki</target>
|
|
||||||
<note>name of notes to self</note>
|
<note>name of notes to self</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Profile and server connections" xml:space="preserve">
|
<trans-unit id="Profile and server connections" xml:space="preserve">
|
||||||
@@ -4211,7 +4194,6 @@ Błąd: %@</target>
|
|||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="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)." xml:space="preserve">
|
<trans-unit id="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)." xml:space="preserve">
|
||||||
<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>
|
<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>
|
||||||
<target>Ostania historia i ulepszony [bot adresowy](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).</target>
|
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Recipients see updates as you type them." xml:space="preserve">
|
<trans-unit id="Recipients see updates as you type them." xml:space="preserve">
|
||||||
@@ -4501,7 +4483,6 @@ Błąd: %@</target>
|
|||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Saved message" xml:space="preserve">
|
<trans-unit id="Saved message" xml:space="preserve">
|
||||||
<source>Saved message</source>
|
<source>Saved message</source>
|
||||||
<target>Zachowano wiadomość</target>
|
|
||||||
<note>message info title</note>
|
<note>message info title</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Scan QR code" xml:space="preserve">
|
<trans-unit id="Scan QR code" xml:space="preserve">
|
||||||
@@ -4536,7 +4517,6 @@ Błąd: %@</target>
|
|||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Search bar accepts invitation links." xml:space="preserve">
|
<trans-unit id="Search bar accepts invitation links." xml:space="preserve">
|
||||||
<source>Search bar accepts invitation links.</source>
|
<source>Search bar accepts invitation links.</source>
|
||||||
<target>Pasek wyszukiwania akceptuje linki zaproszenia.</target>
|
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Search or paste SimpleX link" xml:space="preserve">
|
<trans-unit id="Search or paste SimpleX link" xml:space="preserve">
|
||||||
@@ -4651,7 +4631,6 @@ Błąd: %@</target>
|
|||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Send up to 100 last messages to new members." xml:space="preserve">
|
<trans-unit id="Send up to 100 last messages to new members." xml:space="preserve">
|
||||||
<source>Send up to 100 last messages to new members.</source>
|
<source>Send up to 100 last messages to new members.</source>
|
||||||
<target>Wysyłaj do 100 ostatnich wiadomości do nowych członków.</target>
|
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Sender cancelled file transfer." xml:space="preserve">
|
<trans-unit id="Sender cancelled file transfer." xml:space="preserve">
|
||||||
@@ -5268,7 +5247,6 @@ Może się to zdarzyć z powodu jakiegoś błędu lub gdy połączenie jest skom
|
|||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="This display name is invalid. Please choose another name." xml:space="preserve">
|
<trans-unit id="This display name is invalid. Please choose another name." xml:space="preserve">
|
||||||
<source>This display name is invalid. Please choose another name.</source>
|
<source>This display name is invalid. Please choose another name.</source>
|
||||||
<target>Nazwa wyświetlana jest nieprawidłowa. Proszę wybrać inną nazwę.</target>
|
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="This group has over %lld members, delivery receipts are not sent." xml:space="preserve">
|
<trans-unit id="This group has over %lld members, delivery receipts are not sent." xml:space="preserve">
|
||||||
@@ -5375,7 +5353,6 @@ Przed włączeniem tej funkcji zostanie wyświetlony monit uwierzytelniania.</ta
|
|||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Turkish interface" xml:space="preserve">
|
<trans-unit id="Turkish interface" xml:space="preserve">
|
||||||
<source>Turkish interface</source>
|
<source>Turkish interface</source>
|
||||||
<target>Turecki interfejs</target>
|
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Turn off" xml:space="preserve">
|
<trans-unit id="Turn off" xml:space="preserve">
|
||||||
@@ -5400,7 +5377,6 @@ Przed włączeniem tej funkcji zostanie wyświetlony monit uwierzytelniania.</ta
|
|||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Unblock for all" xml:space="preserve">
|
<trans-unit id="Unblock for all" xml:space="preserve">
|
||||||
<source>Unblock for all</source>
|
<source>Unblock for all</source>
|
||||||
<target>Odblokuj dla wszystkich</target>
|
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Unblock member" xml:space="preserve">
|
<trans-unit id="Unblock member" xml:space="preserve">
|
||||||
@@ -5410,7 +5386,6 @@ Przed włączeniem tej funkcji zostanie wyświetlony monit uwierzytelniania.</ta
|
|||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Unblock member for all?" xml:space="preserve">
|
<trans-unit id="Unblock member for all?" xml:space="preserve">
|
||||||
<source>Unblock member for all?</source>
|
<source>Unblock member for all?</source>
|
||||||
<target>Odblokować członka dla wszystkich?</target>
|
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Unblock member?" xml:space="preserve">
|
<trans-unit id="Unblock member?" xml:space="preserve">
|
||||||
@@ -5512,7 +5487,6 @@ Aby się połączyć, poproś Twój kontakt o utworzenie kolejnego linku połąc
|
|||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Up to 100 last messages are sent to new members." xml:space="preserve">
|
<trans-unit id="Up to 100 last messages are sent to new members." xml:space="preserve">
|
||||||
<source>Up to 100 last messages are sent to new members.</source>
|
<source>Up to 100 last messages are sent to new members.</source>
|
||||||
<target>Do nowych członków wysyłanych jest do 100 ostatnich wiadomości.</target>
|
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Update" xml:space="preserve">
|
<trans-unit id="Update" xml:space="preserve">
|
||||||
@@ -5687,7 +5661,6 @@ Aby się połączyć, poproś Twój kontakt o utworzenie kolejnego linku połąc
|
|||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Visible history" xml:space="preserve">
|
<trans-unit id="Visible history" xml:space="preserve">
|
||||||
<source>Visible history</source>
|
<source>Visible history</source>
|
||||||
<target>Widoczna historia</target>
|
|
||||||
<note>chat feature</note>
|
<note>chat feature</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Voice messages" xml:space="preserve">
|
<trans-unit id="Voice messages" xml:space="preserve">
|
||||||
@@ -5777,7 +5750,6 @@ Aby się połączyć, poproś Twój kontakt o utworzenie kolejnego linku połąc
|
|||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="With encrypted files and media." xml:space="preserve">
|
<trans-unit id="With encrypted files and media." xml:space="preserve">
|
||||||
<source>With encrypted files and media.</source>
|
<source>With encrypted files and media.</source>
|
||||||
<target>Z zaszyfrowanymi plikami i multimediami.</target>
|
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="With optional welcome message." xml:space="preserve">
|
<trans-unit id="With optional welcome message." xml:space="preserve">
|
||||||
@@ -5787,7 +5759,6 @@ Aby się połączyć, poproś Twój kontakt o utworzenie kolejnego linku połąc
|
|||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="With reduced battery usage." xml:space="preserve">
|
<trans-unit id="With reduced battery usage." xml:space="preserve">
|
||||||
<source>With reduced battery usage.</source>
|
<source>With reduced battery usage.</source>
|
||||||
<target>Ze zmniejszonym zużyciem baterii.</target>
|
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Wrong database passphrase" xml:space="preserve">
|
<trans-unit id="Wrong database passphrase" xml:space="preserve">
|
||||||
@@ -6295,12 +6266,10 @@ Serwery SimpleX nie mogą zobaczyć Twojego profilu.</target>
|
|||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="blocked %@" xml:space="preserve">
|
<trans-unit id="blocked %@" xml:space="preserve">
|
||||||
<source>blocked %@</source>
|
<source>blocked %@</source>
|
||||||
<target>zablokowany %@</target>
|
|
||||||
<note>rcv group event chat item</note>
|
<note>rcv group event chat item</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="blocked by admin" xml:space="preserve">
|
<trans-unit id="blocked by admin" xml:space="preserve">
|
||||||
<source>blocked by admin</source>
|
<source>blocked by admin</source>
|
||||||
<target>zablokowany przez admina</target>
|
|
||||||
<note>blocked chat item</note>
|
<note>blocked chat item</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="bold" xml:space="preserve">
|
<trans-unit id="bold" xml:space="preserve">
|
||||||
@@ -6425,7 +6394,6 @@ Serwery SimpleX nie mogą zobaczyć Twojego profilu.</target>
|
|||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="contact %@ changed to %@" xml:space="preserve">
|
<trans-unit id="contact %@ changed to %@" xml:space="preserve">
|
||||||
<source>contact %1$@ changed to %2$@</source>
|
<source>contact %1$@ changed to %2$@</source>
|
||||||
<target>kontakt %1$@ zmieniony na %2$@</target>
|
|
||||||
<note>profile update event chat item</note>
|
<note>profile update event chat item</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="contact has e2e encryption" xml:space="preserve">
|
<trans-unit id="contact has e2e encryption" xml:space="preserve">
|
||||||
@@ -6700,7 +6668,6 @@ Serwery SimpleX nie mogą zobaczyć Twojego profilu.</target>
|
|||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="member %@ changed to %@" xml:space="preserve">
|
<trans-unit id="member %@ changed to %@" xml:space="preserve">
|
||||||
<source>member %1$@ changed to %2$@</source>
|
<source>member %1$@ changed to %2$@</source>
|
||||||
<target>członek %1$@ zmieniony na %2$@</target>
|
|
||||||
<note>profile update event chat item</note>
|
<note>profile update event chat item</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="member connected" xml:space="preserve">
|
<trans-unit id="member connected" xml:space="preserve">
|
||||||
@@ -6827,12 +6794,10 @@ Serwery SimpleX nie mogą zobaczyć Twojego profilu.</target>
|
|||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="removed contact address" xml:space="preserve">
|
<trans-unit id="removed contact address" xml:space="preserve">
|
||||||
<source>removed contact address</source>
|
<source>removed contact address</source>
|
||||||
<target>usunięto adres kontaktu</target>
|
|
||||||
<note>profile update event chat item</note>
|
<note>profile update event chat item</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="removed profile picture" xml:space="preserve">
|
<trans-unit id="removed profile picture" xml:space="preserve">
|
||||||
<source>removed profile picture</source>
|
<source>removed profile picture</source>
|
||||||
<target>usunięto zdjęcie profilu</target>
|
|
||||||
<note>profile update event chat item</note>
|
<note>profile update event chat item</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="removed you" xml:space="preserve">
|
<trans-unit id="removed you" xml:space="preserve">
|
||||||
@@ -6867,12 +6832,10 @@ Serwery SimpleX nie mogą zobaczyć Twojego profilu.</target>
|
|||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="set new contact address" xml:space="preserve">
|
<trans-unit id="set new contact address" xml:space="preserve">
|
||||||
<source>set new contact address</source>
|
<source>set new contact address</source>
|
||||||
<target>ustaw nowy adres kontaktu</target>
|
|
||||||
<note>profile update event chat item</note>
|
<note>profile update event chat item</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="set new profile picture" xml:space="preserve">
|
<trans-unit id="set new profile picture" xml:space="preserve">
|
||||||
<source>set new profile picture</source>
|
<source>set new profile picture</source>
|
||||||
<target>ustaw nowe zdjęcie profilu</target>
|
|
||||||
<note>profile update event chat item</note>
|
<note>profile update event chat item</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="starting…" xml:space="preserve">
|
<trans-unit id="starting…" xml:space="preserve">
|
||||||
@@ -6892,7 +6855,6 @@ Serwery SimpleX nie mogą zobaczyć Twojego profilu.</target>
|
|||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="unblocked %@" xml:space="preserve">
|
<trans-unit id="unblocked %@" xml:space="preserve">
|
||||||
<source>unblocked %@</source>
|
<source>unblocked %@</source>
|
||||||
<target>odblokowano %@</target>
|
|
||||||
<note>rcv group event chat item</note>
|
<note>rcv group event chat item</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="unknown" xml:space="preserve">
|
<trans-unit id="unknown" xml:space="preserve">
|
||||||
@@ -6902,7 +6864,6 @@ Serwery SimpleX nie mogą zobaczyć Twojego profilu.</target>
|
|||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="unknown status" xml:space="preserve">
|
<trans-unit id="unknown status" xml:space="preserve">
|
||||||
<source>unknown status</source>
|
<source>unknown status</source>
|
||||||
<target>nieznany status</target>
|
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="updated group profile" xml:space="preserve">
|
<trans-unit id="updated group profile" xml:space="preserve">
|
||||||
@@ -6912,7 +6873,6 @@ Serwery SimpleX nie mogą zobaczyć Twojego profilu.</target>
|
|||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="updated profile" xml:space="preserve">
|
<trans-unit id="updated profile" xml:space="preserve">
|
||||||
<source>updated profile</source>
|
<source>updated profile</source>
|
||||||
<target>zaktualizowano profil</target>
|
|
||||||
<note>profile update event chat item</note>
|
<note>profile update event chat item</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="v%@" xml:space="preserve">
|
<trans-unit id="v%@" xml:space="preserve">
|
||||||
@@ -6987,7 +6947,6 @@ Serwery SimpleX nie mogą zobaczyć Twojego profilu.</target>
|
|||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="you blocked %@" xml:space="preserve">
|
<trans-unit id="you blocked %@" xml:space="preserve">
|
||||||
<source>you blocked %@</source>
|
<source>you blocked %@</source>
|
||||||
<target>zablokowałeś %@</target>
|
|
||||||
<note>snd group event chat item</note>
|
<note>snd group event chat item</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="you changed address" xml:space="preserve">
|
<trans-unit id="you changed address" xml:space="preserve">
|
||||||
@@ -7032,7 +6991,6 @@ Serwery SimpleX nie mogą zobaczyć Twojego profilu.</target>
|
|||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="you unblocked %@" xml:space="preserve">
|
<trans-unit id="you unblocked %@" xml:space="preserve">
|
||||||
<source>you unblocked %@</source>
|
<source>you unblocked %@</source>
|
||||||
<target>odblokowałeś %@</target>
|
|
||||||
<note>snd group event chat item</note>
|
<note>snd group event chat item</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="you: " xml:space="preserve">
|
<trans-unit id="you: " xml:space="preserve">
|
||||||
|
|||||||
@@ -219,7 +219,6 @@
|
|||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="%lld messages blocked by admin" xml:space="preserve">
|
<trans-unit id="%lld messages blocked by admin" xml:space="preserve">
|
||||||
<source>%lld messages blocked by admin</source>
|
<source>%lld messages blocked by admin</source>
|
||||||
<target>%lld сообщений заблокировано администратором</target>
|
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="%lld messages marked deleted" xml:space="preserve">
|
<trans-unit id="%lld messages marked deleted" xml:space="preserve">
|
||||||
@@ -925,7 +924,6 @@
|
|||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Block for all" xml:space="preserve">
|
<trans-unit id="Block for all" xml:space="preserve">
|
||||||
<source>Block for all</source>
|
<source>Block for all</source>
|
||||||
<target>Заблокировать для всех</target>
|
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Block group members" xml:space="preserve">
|
<trans-unit id="Block group members" xml:space="preserve">
|
||||||
@@ -940,7 +938,6 @@
|
|||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Block member for all?" xml:space="preserve">
|
<trans-unit id="Block member for all?" xml:space="preserve">
|
||||||
<source>Block member for all?</source>
|
<source>Block member for all?</source>
|
||||||
<target>Заблокировать члена для всех?</target>
|
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Block member?" xml:space="preserve">
|
<trans-unit id="Block member?" xml:space="preserve">
|
||||||
@@ -950,7 +947,6 @@
|
|||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Blocked by admin" xml:space="preserve">
|
<trans-unit id="Blocked by admin" xml:space="preserve">
|
||||||
<source>Blocked by admin</source>
|
<source>Blocked by admin</source>
|
||||||
<target>Заблокирован администратором</target>
|
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Both you and your contact can add message reactions." xml:space="preserve">
|
<trans-unit id="Both you and your contact can add message reactions." xml:space="preserve">
|
||||||
@@ -5400,7 +5396,6 @@ You will be prompted to complete authentication before this feature is enabled.<
|
|||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Unblock for all" xml:space="preserve">
|
<trans-unit id="Unblock for all" xml:space="preserve">
|
||||||
<source>Unblock for all</source>
|
<source>Unblock for all</source>
|
||||||
<target>Разблокировать для всех</target>
|
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Unblock member" xml:space="preserve">
|
<trans-unit id="Unblock member" xml:space="preserve">
|
||||||
@@ -5410,7 +5405,6 @@ You will be prompted to complete authentication before this feature is enabled.<
|
|||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Unblock member for all?" xml:space="preserve">
|
<trans-unit id="Unblock member for all?" xml:space="preserve">
|
||||||
<source>Unblock member for all?</source>
|
<source>Unblock member for all?</source>
|
||||||
<target>Разблокировать члена для всех?</target>
|
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Unblock member?" xml:space="preserve">
|
<trans-unit id="Unblock member?" xml:space="preserve">
|
||||||
@@ -6295,12 +6289,10 @@ SimpleX серверы не могут получить доступ к Ваше
|
|||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="blocked %@" xml:space="preserve">
|
<trans-unit id="blocked %@" xml:space="preserve">
|
||||||
<source>blocked %@</source>
|
<source>blocked %@</source>
|
||||||
<target>%@ заблокирован</target>
|
|
||||||
<note>rcv group event chat item</note>
|
<note>rcv group event chat item</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="blocked by admin" xml:space="preserve">
|
<trans-unit id="blocked by admin" xml:space="preserve">
|
||||||
<source>blocked by admin</source>
|
<source>blocked by admin</source>
|
||||||
<target>заблокировано администратором</target>
|
|
||||||
<note>blocked chat item</note>
|
<note>blocked chat item</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="bold" xml:space="preserve">
|
<trans-unit id="bold" xml:space="preserve">
|
||||||
@@ -6892,7 +6884,6 @@ SimpleX серверы не могут получить доступ к Ваше
|
|||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="unblocked %@" xml:space="preserve">
|
<trans-unit id="unblocked %@" xml:space="preserve">
|
||||||
<source>unblocked %@</source>
|
<source>unblocked %@</source>
|
||||||
<target>%@ разблокирован</target>
|
|
||||||
<note>rcv group event chat item</note>
|
<note>rcv group event chat item</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="unknown" xml:space="preserve">
|
<trans-unit id="unknown" xml:space="preserve">
|
||||||
@@ -6987,7 +6978,6 @@ SimpleX серверы не могут получить доступ к Ваше
|
|||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="you blocked %@" xml:space="preserve">
|
<trans-unit id="you blocked %@" xml:space="preserve">
|
||||||
<source>you blocked %@</source>
|
<source>you blocked %@</source>
|
||||||
<target>Вы заблокировали %@</target>
|
|
||||||
<note>snd group event chat item</note>
|
<note>snd group event chat item</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="you changed address" xml:space="preserve">
|
<trans-unit id="you changed address" xml:space="preserve">
|
||||||
@@ -7032,7 +7022,6 @@ SimpleX серверы не могут получить доступ к Ваше
|
|||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="you unblocked %@" xml:space="preserve">
|
<trans-unit id="you unblocked %@" xml:space="preserve">
|
||||||
<source>you unblocked %@</source>
|
<source>you unblocked %@</source>
|
||||||
<target>Вы разблокировали %@</target>
|
|
||||||
<note>snd group event chat item</note>
|
<note>snd group event chat item</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="you: " xml:space="preserve">
|
<trans-unit id="you: " xml:space="preserve">
|
||||||
|
|||||||
@@ -14,17 +14,17 @@
|
|||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id=" " xml:space="preserve">
|
<trans-unit id=" " xml:space="preserve">
|
||||||
<source> </source>
|
<source> </source>
|
||||||
<target> </target>
|
<target> . </target>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id=" " xml:space="preserve">
|
<trans-unit id=" " xml:space="preserve">
|
||||||
<source> </source>
|
<source> </source>
|
||||||
<target> </target>
|
<target> </target>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id=" " xml:space="preserve">
|
<trans-unit id=" " xml:space="preserve">
|
||||||
<source> </source>
|
<source> </source>
|
||||||
<target> </target>
|
<target> . </target>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id=" (" xml:space="preserve">
|
<trans-unit id=" (" xml:space="preserve">
|
||||||
|
|||||||
@@ -453,6 +453,7 @@ var receiverStarted = false
|
|||||||
let startLock = DispatchSemaphore(value: 1)
|
let startLock = DispatchSemaphore(value: 1)
|
||||||
let suspendLock = DispatchSemaphore(value: 1)
|
let suspendLock = DispatchSemaphore(value: 1)
|
||||||
var networkConfig: NetCfg = getNetCfg()
|
var networkConfig: NetCfg = getNetCfg()
|
||||||
|
let xftpConfig: XFTPFileConfig? = getXFTPCfg()
|
||||||
|
|
||||||
// startChat uses semaphore startLock to ensure that only one didReceive thread can start chat controller
|
// 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
|
// Subsequent calls to didReceive will be waiting on semaphore and won't start chat again, as it will be .active
|
||||||
@@ -498,6 +499,7 @@ func doStartChat() -> DBMigrationResult? {
|
|||||||
try setNetworkConfig(networkConfig)
|
try setNetworkConfig(networkConfig)
|
||||||
try apiSetTempFolder(tempFolder: getTempFilesDirectory().path)
|
try apiSetTempFolder(tempFolder: getTempFilesDirectory().path)
|
||||||
try apiSetFilesFolder(filesFolder: getAppFilesDirectory().path)
|
try apiSetFilesFolder(filesFolder: getAppFilesDirectory().path)
|
||||||
|
try setXFTPConfig(xftpConfig)
|
||||||
try apiSetEncryptLocalFiles(privacyEncryptLocalFilesGroupDefault.get())
|
try apiSetEncryptLocalFiles(privacyEncryptLocalFilesGroupDefault.get())
|
||||||
// prevent suspension while starting chat
|
// prevent suspension while starting chat
|
||||||
suspendLock.wait()
|
suspendLock.wait()
|
||||||
@@ -731,6 +733,12 @@ func apiSetFilesFolder(filesFolder: String) throws {
|
|||||||
throw r
|
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 {
|
func apiSetEncryptLocalFiles(_ enable: Bool) throws {
|
||||||
let r = sendSimpleXCmd(.apiSetEncryptLocalFiles(enable: enable))
|
let r = sendSimpleXCmd(.apiSetEncryptLocalFiles(enable: enable))
|
||||||
if case .cmdOk = r { return }
|
if case .cmdOk = r { return }
|
||||||
|
|||||||
@@ -61,11 +61,6 @@
|
|||||||
5C7505A527B679EE00BE3227 /* NavLinkPlain.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C7505A427B679EE00BE3227 /* NavLinkPlain.swift */; };
|
5C7505A527B679EE00BE3227 /* NavLinkPlain.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C7505A427B679EE00BE3227 /* NavLinkPlain.swift */; };
|
||||||
5C7505A827B6D34800BE3227 /* ChatInfoToolbar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C7505A727B6D34800BE3227 /* ChatInfoToolbar.swift */; };
|
5C7505A827B6D34800BE3227 /* ChatInfoToolbar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C7505A727B6D34800BE3227 /* ChatInfoToolbar.swift */; };
|
||||||
5C764E89279CBCB3000C6508 /* ChatModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C764E88279CBCB3000C6508 /* ChatModel.swift */; };
|
5C764E89279CBCB3000C6508 /* ChatModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C764E88279CBCB3000C6508 /* ChatModel.swift */; };
|
||||||
5C777BD82B99B38B00C72EFF /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C777BD32B99B38B00C72EFF /* libgmp.a */; };
|
|
||||||
5C777BD92B99B38B00C72EFF /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C777BD42B99B38B00C72EFF /* libgmpxx.a */; };
|
|
||||||
5C777BDA2B99B38B00C72EFF /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C777BD52B99B38B00C72EFF /* libffi.a */; };
|
|
||||||
5C777BDB2B99B38B00C72EFF /* libHSsimplex-chat-5.5.6.0-AiwFoGVZWFALIHlLc8SJrg-ghc9.6.3.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C777BD62B99B38B00C72EFF /* libHSsimplex-chat-5.5.6.0-AiwFoGVZWFALIHlLc8SJrg-ghc9.6.3.a */; };
|
|
||||||
5C777BDC2B99B38B00C72EFF /* libHSsimplex-chat-5.5.6.0-AiwFoGVZWFALIHlLc8SJrg.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C777BD72B99B38B00C72EFF /* libHSsimplex-chat-5.5.6.0-AiwFoGVZWFALIHlLc8SJrg.a */; };
|
|
||||||
5C8F01CD27A6F0D8007D2C8D /* CodeScanner in Frameworks */ = {isa = PBXBuildFile; productRef = 5C8F01CC27A6F0D8007D2C8D /* CodeScanner */; };
|
5C8F01CD27A6F0D8007D2C8D /* CodeScanner in Frameworks */ = {isa = PBXBuildFile; productRef = 5C8F01CC27A6F0D8007D2C8D /* CodeScanner */; };
|
||||||
5C93292F29239A170090FFF9 /* ProtocolServersView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C93292E29239A170090FFF9 /* ProtocolServersView.swift */; };
|
5C93292F29239A170090FFF9 /* ProtocolServersView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C93292E29239A170090FFF9 /* ProtocolServersView.swift */; };
|
||||||
5C93293129239BED0090FFF9 /* ProtocolServerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C93293029239BED0090FFF9 /* ProtocolServerView.swift */; };
|
5C93293129239BED0090FFF9 /* ProtocolServerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C93293029239BED0090FFF9 /* ProtocolServerView.swift */; };
|
||||||
@@ -117,6 +112,11 @@
|
|||||||
5CC2C0FF2809BF11000C35E3 /* SimpleX--iOS--InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 5CC2C0FD2809BF11000C35E3 /* SimpleX--iOS--InfoPlist.strings */; };
|
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 */; };
|
5CC868F329EB540C0017BBFD /* CIRcvDecryptionError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CC868F229EB540C0017BBFD /* CIRcvDecryptionError.swift */; };
|
||||||
5CCB939C297EFCB100399E78 /* NavStackCompat.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CCB939B297EFCB100399E78 /* NavStackCompat.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, ); }; };
|
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 */; };
|
5CD67B902B0E858A00C510B1 /* hs_init.c in Sources */ = {isa = PBXBuildFile; fileRef = 5CD67B8E2B0E858A00C510B1 /* hs_init.c */; };
|
||||||
5CDCAD482818589900503DA2 /* NotificationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CDCAD472818589900503DA2 /* NotificationService.swift */; };
|
5CDCAD482818589900503DA2 /* NotificationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CDCAD472818589900503DA2 /* NotificationService.swift */; };
|
||||||
@@ -325,11 +325,6 @@
|
|||||||
5C7505A427B679EE00BE3227 /* NavLinkPlain.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavLinkPlain.swift; sourceTree = "<group>"; };
|
5C7505A427B679EE00BE3227 /* NavLinkPlain.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavLinkPlain.swift; sourceTree = "<group>"; };
|
||||||
5C7505A727B6D34800BE3227 /* ChatInfoToolbar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatInfoToolbar.swift; sourceTree = "<group>"; };
|
5C7505A727B6D34800BE3227 /* ChatInfoToolbar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatInfoToolbar.swift; sourceTree = "<group>"; };
|
||||||
5C764E88279CBCB3000C6508 /* ChatModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatModel.swift; sourceTree = "<group>"; };
|
5C764E88279CBCB3000C6508 /* ChatModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatModel.swift; sourceTree = "<group>"; };
|
||||||
5C777BD32B99B38B00C72EFF /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = "<group>"; };
|
|
||||||
5C777BD42B99B38B00C72EFF /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = "<group>"; };
|
|
||||||
5C777BD52B99B38B00C72EFF /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = "<group>"; };
|
|
||||||
5C777BD62B99B38B00C72EFF /* libHSsimplex-chat-5.5.6.0-AiwFoGVZWFALIHlLc8SJrg-ghc9.6.3.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-5.5.6.0-AiwFoGVZWFALIHlLc8SJrg-ghc9.6.3.a"; sourceTree = "<group>"; };
|
|
||||||
5C777BD72B99B38B00C72EFF /* libHSsimplex-chat-5.5.6.0-AiwFoGVZWFALIHlLc8SJrg.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-5.5.6.0-AiwFoGVZWFALIHlLc8SJrg.a"; sourceTree = "<group>"; };
|
|
||||||
5C84FE9129A216C800D95B1A /* nl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = nl; path = nl.lproj/Localizable.strings; sourceTree = "<group>"; };
|
5C84FE9129A216C800D95B1A /* nl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = nl; path = nl.lproj/Localizable.strings; sourceTree = "<group>"; };
|
||||||
5C84FE9329A2179C00D95B1A /* nl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = nl; path = "nl.lproj/SimpleX--iOS--InfoPlist.strings"; sourceTree = "<group>"; };
|
5C84FE9329A2179C00D95B1A /* nl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = nl; path = "nl.lproj/SimpleX--iOS--InfoPlist.strings"; sourceTree = "<group>"; };
|
||||||
5C84FE9429A2179C00D95B1A /* nl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = nl; path = nl.lproj/InfoPlist.strings; sourceTree = "<group>"; };
|
5C84FE9429A2179C00D95B1A /* nl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = nl; path = nl.lproj/InfoPlist.strings; sourceTree = "<group>"; };
|
||||||
@@ -403,6 +398,11 @@
|
|||||||
5CC2C0FE2809BF11000C35E3 /* ru */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ru; path = "ru.lproj/SimpleX--iOS--InfoPlist.strings"; sourceTree = "<group>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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; };
|
5CDCAD452818589900503DA2 /* SimpleX NSE.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = "SimpleX NSE.appex"; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
@@ -514,13 +514,13 @@
|
|||||||
isa = PBXFrameworksBuildPhase;
|
isa = PBXFrameworksBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
5C777BD92B99B38B00C72EFF /* libgmpxx.a in Frameworks */,
|
5CCD2C462B5C800E00F76440 /* libgmpxx.a in Frameworks */,
|
||||||
5C777BDB2B99B38B00C72EFF /* libHSsimplex-chat-5.5.6.0-AiwFoGVZWFALIHlLc8SJrg-ghc9.6.3.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 */,
|
||||||
5CE2BA93284534B000EC33A6 /* libiconv.tbd in Frameworks */,
|
5CE2BA93284534B000EC33A6 /* libiconv.tbd in Frameworks */,
|
||||||
5C777BD82B99B38B00C72EFF /* libgmp.a in Frameworks */,
|
5CCD2C4A2B5C800E00F76440 /* libffi.a in Frameworks */,
|
||||||
5C777BDA2B99B38B00C72EFF /* libffi.a in Frameworks */,
|
5CCD2C472B5C800E00F76440 /* libgmp.a in Frameworks */,
|
||||||
5CE2BA94284534BB00EC33A6 /* libz.tbd in Frameworks */,
|
5CE2BA94284534BB00EC33A6 /* libz.tbd in Frameworks */,
|
||||||
5C777BDC2B99B38B00C72EFF /* libHSsimplex-chat-5.5.6.0-AiwFoGVZWFALIHlLc8SJrg.a in Frameworks */,
|
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
@@ -582,11 +582,11 @@
|
|||||||
5C764E5C279C70B7000C6508 /* Libraries */ = {
|
5C764E5C279C70B7000C6508 /* Libraries */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
5C777BD52B99B38B00C72EFF /* libffi.a */,
|
5CCD2C452B5C800E00F76440 /* libffi.a */,
|
||||||
5C777BD32B99B38B00C72EFF /* libgmp.a */,
|
5CCD2C422B5C800E00F76440 /* libgmp.a */,
|
||||||
5C777BD42B99B38B00C72EFF /* libgmpxx.a */,
|
5CCD2C412B5C800E00F76440 /* libgmpxx.a */,
|
||||||
5C777BD62B99B38B00C72EFF /* libHSsimplex-chat-5.5.6.0-AiwFoGVZWFALIHlLc8SJrg-ghc9.6.3.a */,
|
5CCD2C432B5C800E00F76440 /* libHSsimplex-chat-5.5.0.3-3tzCJyUgrnK8CcHdWjtxcl-ghc9.6.3.a */,
|
||||||
5C777BD72B99B38B00C72EFF /* libHSsimplex-chat-5.5.6.0-AiwFoGVZWFALIHlLc8SJrg.a */,
|
5CCD2C442B5C800E00F76440 /* libHSsimplex-chat-5.5.0.3-3tzCJyUgrnK8CcHdWjtxcl.a */,
|
||||||
);
|
);
|
||||||
path = Libraries;
|
path = Libraries;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@@ -1509,7 +1509,7 @@
|
|||||||
CLANG_ENABLE_MODULES = YES;
|
CLANG_ENABLE_MODULES = YES;
|
||||||
CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements";
|
CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 201;
|
CURRENT_PROJECT_VERSION = 191;
|
||||||
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
ENABLE_PREVIEWS = YES;
|
ENABLE_PREVIEWS = YES;
|
||||||
@@ -1531,7 +1531,7 @@
|
|||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 5.5.6;
|
MARKETING_VERSION = 5.5;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.app;
|
PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.app;
|
||||||
PRODUCT_NAME = SimpleX;
|
PRODUCT_NAME = SimpleX;
|
||||||
SDKROOT = iphoneos;
|
SDKROOT = iphoneos;
|
||||||
@@ -1552,7 +1552,7 @@
|
|||||||
CLANG_ENABLE_MODULES = YES;
|
CLANG_ENABLE_MODULES = YES;
|
||||||
CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements";
|
CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 201;
|
CURRENT_PROJECT_VERSION = 191;
|
||||||
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
ENABLE_PREVIEWS = YES;
|
ENABLE_PREVIEWS = YES;
|
||||||
@@ -1574,7 +1574,7 @@
|
|||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 5.5.6;
|
MARKETING_VERSION = 5.5;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.app;
|
PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.app;
|
||||||
PRODUCT_NAME = SimpleX;
|
PRODUCT_NAME = SimpleX;
|
||||||
SDKROOT = iphoneos;
|
SDKROOT = iphoneos;
|
||||||
@@ -1633,7 +1633,7 @@
|
|||||||
CODE_SIGN_ENTITLEMENTS = "SimpleX NSE/SimpleX NSE.entitlements";
|
CODE_SIGN_ENTITLEMENTS = "SimpleX NSE/SimpleX NSE.entitlements";
|
||||||
CODE_SIGN_IDENTITY = "Apple Development";
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 201;
|
CURRENT_PROJECT_VERSION = 191;
|
||||||
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
@@ -1646,7 +1646,7 @@
|
|||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
"@executable_path/../../Frameworks",
|
"@executable_path/../../Frameworks",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 5.5.6;
|
MARKETING_VERSION = 5.5;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.app.SimpleX-NSE";
|
PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.app.SimpleX-NSE";
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||||
@@ -1665,7 +1665,7 @@
|
|||||||
CODE_SIGN_ENTITLEMENTS = "SimpleX NSE/SimpleX NSE.entitlements";
|
CODE_SIGN_ENTITLEMENTS = "SimpleX NSE/SimpleX NSE.entitlements";
|
||||||
CODE_SIGN_IDENTITY = "Apple Development";
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 201;
|
CURRENT_PROJECT_VERSION = 191;
|
||||||
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
@@ -1678,7 +1678,7 @@
|
|||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
"@executable_path/../../Frameworks",
|
"@executable_path/../../Frameworks",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 5.5.6;
|
MARKETING_VERSION = 5.5;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.app.SimpleX-NSE";
|
PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.app.SimpleX-NSE";
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||||
@@ -1697,7 +1697,7 @@
|
|||||||
APPLICATION_EXTENSION_API_ONLY = YES;
|
APPLICATION_EXTENSION_API_ONLY = YES;
|
||||||
CLANG_ENABLE_MODULES = YES;
|
CLANG_ENABLE_MODULES = YES;
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 201;
|
CURRENT_PROJECT_VERSION = 191;
|
||||||
DEFINES_MODULE = YES;
|
DEFINES_MODULE = YES;
|
||||||
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
||||||
DYLIB_COMPATIBILITY_VERSION = 1;
|
DYLIB_COMPATIBILITY_VERSION = 1;
|
||||||
@@ -1721,7 +1721,7 @@
|
|||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"$(PROJECT_DIR)/Libraries/sim",
|
"$(PROJECT_DIR)/Libraries/sim",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 5.5.6;
|
MARKETING_VERSION = 5.5;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.SimpleXChat;
|
PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.SimpleXChat;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
|
PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
|
||||||
SDKROOT = iphoneos;
|
SDKROOT = iphoneos;
|
||||||
@@ -1743,7 +1743,7 @@
|
|||||||
APPLICATION_EXTENSION_API_ONLY = YES;
|
APPLICATION_EXTENSION_API_ONLY = YES;
|
||||||
CLANG_ENABLE_MODULES = YES;
|
CLANG_ENABLE_MODULES = YES;
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 201;
|
CURRENT_PROJECT_VERSION = 191;
|
||||||
DEFINES_MODULE = YES;
|
DEFINES_MODULE = YES;
|
||||||
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
||||||
DYLIB_COMPATIBILITY_VERSION = 1;
|
DYLIB_COMPATIBILITY_VERSION = 1;
|
||||||
@@ -1767,7 +1767,7 @@
|
|||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"$(PROJECT_DIR)/Libraries/sim",
|
"$(PROJECT_DIR)/Libraries/sim",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 5.5.6;
|
MARKETING_VERSION = 5.5;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.SimpleXChat;
|
PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.SimpleXChat;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
|
PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
|
||||||
SDKROOT = iphoneos;
|
SDKROOT = iphoneos;
|
||||||
|
|||||||
@@ -105,11 +105,6 @@ public func parseSimpleXMarkdown(_ s: String) -> [FormattedText]? {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
public func chatJsonLength(_ s: String) -> Int {
|
|
||||||
var c = s.cString(using: .utf8)!
|
|
||||||
return Int(chat_json_length(&c))
|
|
||||||
}
|
|
||||||
|
|
||||||
struct ParsedMarkdown: Decodable {
|
struct ParsedMarkdown: Decodable {
|
||||||
var formattedText: [FormattedText]?
|
var formattedText: [FormattedText]?
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,9 +31,8 @@ public enum ChatCommand {
|
|||||||
case apiSuspendChat(timeoutMicroseconds: Int)
|
case apiSuspendChat(timeoutMicroseconds: Int)
|
||||||
case setTempFolder(tempFolder: String)
|
case setTempFolder(tempFolder: String)
|
||||||
case setFilesFolder(filesFolder: String)
|
case setFilesFolder(filesFolder: String)
|
||||||
|
case apiSetXFTPConfig(config: XFTPFileConfig?)
|
||||||
case apiSetEncryptLocalFiles(enable: Bool)
|
case apiSetEncryptLocalFiles(enable: Bool)
|
||||||
case apiSetPQEnabled(enable: Bool)
|
|
||||||
case apiAllowContactPQ(contactId: Int64)
|
|
||||||
case apiExportArchive(config: ArchiveConfig)
|
case apiExportArchive(config: ArchiveConfig)
|
||||||
case apiImportArchive(config: ArchiveConfig)
|
case apiImportArchive(config: ArchiveConfig)
|
||||||
case apiDeleteStorage
|
case apiDeleteStorage
|
||||||
@@ -163,9 +162,12 @@ public enum ChatCommand {
|
|||||||
case let .apiSuspendChat(timeoutMicroseconds): return "/_app suspend \(timeoutMicroseconds)"
|
case let .apiSuspendChat(timeoutMicroseconds): return "/_app suspend \(timeoutMicroseconds)"
|
||||||
case let .setTempFolder(tempFolder): return "/_temp_folder \(tempFolder)"
|
case let .setTempFolder(tempFolder): return "/_temp_folder \(tempFolder)"
|
||||||
case let .setFilesFolder(filesFolder): return "/_files_folder \(filesFolder)"
|
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 .apiSetEncryptLocalFiles(enable): return "/_files_encrypt \(onOff(enable))"
|
||||||
case let .apiSetPQEnabled(enable): return "/_pq \(onOff(enable))"
|
|
||||||
case let .apiAllowContactPQ(contactId): return "/_pq allow \(contactId)"
|
|
||||||
case let .apiExportArchive(cfg): return "/_db export \(encodeJSON(cfg))"
|
case let .apiExportArchive(cfg): return "/_db export \(encodeJSON(cfg))"
|
||||||
case let .apiImportArchive(cfg): return "/_db import \(encodeJSON(cfg))"
|
case let .apiImportArchive(cfg): return "/_db import \(encodeJSON(cfg))"
|
||||||
case .apiDeleteStorage: return "/_db delete"
|
case .apiDeleteStorage: return "/_db delete"
|
||||||
@@ -309,9 +311,8 @@ public enum ChatCommand {
|
|||||||
case .apiSuspendChat: return "apiSuspendChat"
|
case .apiSuspendChat: return "apiSuspendChat"
|
||||||
case .setTempFolder: return "setTempFolder"
|
case .setTempFolder: return "setTempFolder"
|
||||||
case .setFilesFolder: return "setFilesFolder"
|
case .setFilesFolder: return "setFilesFolder"
|
||||||
|
case .apiSetXFTPConfig: return "apiSetXFTPConfig"
|
||||||
case .apiSetEncryptLocalFiles: return "apiSetEncryptLocalFiles"
|
case .apiSetEncryptLocalFiles: return "apiSetEncryptLocalFiles"
|
||||||
case .apiSetPQEnabled: return "apiSetPQEnabled"
|
|
||||||
case .apiAllowContactPQ: return "apiAllowContactPQ"
|
|
||||||
case .apiExportArchive: return "apiExportArchive"
|
case .apiExportArchive: return "apiExportArchive"
|
||||||
case .apiImportArchive: return "apiImportArchive"
|
case .apiImportArchive: return "apiImportArchive"
|
||||||
case .apiDeleteStorage: return "apiDeleteStorage"
|
case .apiDeleteStorage: return "apiDeleteStorage"
|
||||||
@@ -612,7 +613,7 @@ public enum ChatResponse: Decodable, Error {
|
|||||||
case callEnded(user: UserRef, contact: Contact)
|
case callEnded(user: UserRef, contact: Contact)
|
||||||
case callInvitations(callInvitations: [RcvCallInvitation])
|
case callInvitations(callInvitations: [RcvCallInvitation])
|
||||||
case ntfTokenStatus(status: NtfTknStatus)
|
case ntfTokenStatus(status: NtfTknStatus)
|
||||||
case ntfToken(token: DeviceToken, status: NtfTknStatus, ntfMode: NotificationsMode, ntfServer: String)
|
case ntfToken(token: DeviceToken, status: NtfTknStatus, ntfMode: NotificationsMode)
|
||||||
case ntfMessages(user_: User?, connEntity_: ConnectionEntity?, msgTs: Date?, ntfMessages: [NtfMsgInfo])
|
case ntfMessages(user_: User?, connEntity_: ConnectionEntity?, msgTs: Date?, ntfMessages: [NtfMsgInfo])
|
||||||
case ntfMessage(user: UserRef, connEntity: ConnectionEntity, ntfMessage: NtfMsgInfo)
|
case ntfMessage(user: UserRef, connEntity: ConnectionEntity, ntfMessage: NtfMsgInfo)
|
||||||
case contactConnectionDeleted(user: UserRef, connection: PendingContactConnection)
|
case contactConnectionDeleted(user: UserRef, connection: PendingContactConnection)
|
||||||
@@ -623,9 +624,6 @@ public enum ChatResponse: Decodable, Error {
|
|||||||
case remoteCtrlSessionCode(remoteCtrl_: RemoteCtrlInfo?, sessionCode: String)
|
case remoteCtrlSessionCode(remoteCtrl_: RemoteCtrlInfo?, sessionCode: String)
|
||||||
case remoteCtrlConnected(remoteCtrl: RemoteCtrlInfo)
|
case remoteCtrlConnected(remoteCtrl: RemoteCtrlInfo)
|
||||||
case remoteCtrlStopped(rcsState: RemoteCtrlSessionState, rcStopReason: RemoteCtrlStopReason)
|
case remoteCtrlStopped(rcsState: RemoteCtrlSessionState, rcStopReason: RemoteCtrlStopReason)
|
||||||
// pq
|
|
||||||
case contactPQAllowed(user: UserRef, contact: Contact)
|
|
||||||
case contactPQEnabled(user: UserRef, contact: Contact, pqEnabled: Bool)
|
|
||||||
// misc
|
// misc
|
||||||
case versionInfo(versionInfo: CoreVersionInfo, chatMigrations: [UpMigration], agentMigrations: [UpMigration])
|
case versionInfo(versionInfo: CoreVersionInfo, chatMigrations: [UpMigration], agentMigrations: [UpMigration])
|
||||||
case cmdOk(user: UserRef?)
|
case cmdOk(user: UserRef?)
|
||||||
@@ -774,8 +772,6 @@ public enum ChatResponse: Decodable, Error {
|
|||||||
case .remoteCtrlSessionCode: return "remoteCtrlSessionCode"
|
case .remoteCtrlSessionCode: return "remoteCtrlSessionCode"
|
||||||
case .remoteCtrlConnected: return "remoteCtrlConnected"
|
case .remoteCtrlConnected: return "remoteCtrlConnected"
|
||||||
case .remoteCtrlStopped: return "remoteCtrlStopped"
|
case .remoteCtrlStopped: return "remoteCtrlStopped"
|
||||||
case .contactPQAllowed: return "contactPQAllowed"
|
|
||||||
case .contactPQEnabled: return "contactPQAllowed"
|
|
||||||
case .versionInfo: return "versionInfo"
|
case .versionInfo: return "versionInfo"
|
||||||
case .cmdOk: return "cmdOk"
|
case .cmdOk: return "cmdOk"
|
||||||
case .chatCmdError: return "chatCmdError"
|
case .chatCmdError: return "chatCmdError"
|
||||||
@@ -916,7 +912,7 @@ public enum ChatResponse: Decodable, Error {
|
|||||||
case let .callEnded(u, contact): return withUser(u, "contact: \(contact.id)")
|
case let .callEnded(u, contact): return withUser(u, "contact: \(contact.id)")
|
||||||
case let .callInvitations(invs): return String(describing: invs)
|
case let .callInvitations(invs): return String(describing: invs)
|
||||||
case let .ntfTokenStatus(status): return String(describing: status)
|
case let .ntfTokenStatus(status): return String(describing: status)
|
||||||
case let .ntfToken(token, status, ntfMode, ntfServer): return "token: \(token)\nstatus: \(status.rawValue)\nntfMode: \(ntfMode.rawValue)\nntfServer: \(ntfServer)"
|
case let .ntfToken(token, status, ntfMode): return "token: \(token)\nstatus: \(status.rawValue)\nntfMode: \(ntfMode.rawValue)"
|
||||||
case let .ntfMessages(u, connEntity, msgTs, ntfMessages): return withUser(u, "connEntity: \(String(describing: connEntity))\nmsgTs: \(String(describing: msgTs))\nntfMessages: \(String(describing: ntfMessages))")
|
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 .ntfMessage(u, connEntity, ntfMessage): return withUser(u, "connEntity: \(String(describing: connEntity))\nntfMessage: \(String(describing: ntfMessage))")
|
||||||
case let .contactConnectionDeleted(u, connection): return withUser(u, String(describing: connection))
|
case let .contactConnectionDeleted(u, connection): return withUser(u, String(describing: connection))
|
||||||
@@ -926,8 +922,6 @@ public enum ChatResponse: Decodable, Error {
|
|||||||
case let .remoteCtrlSessionCode(remoteCtrl_, sessionCode): return "remoteCtrl_:\n\(String(describing: remoteCtrl_))\nsessionCode: \(sessionCode)"
|
case let .remoteCtrlSessionCode(remoteCtrl_, sessionCode): return "remoteCtrl_:\n\(String(describing: remoteCtrl_))\nsessionCode: \(sessionCode)"
|
||||||
case let .remoteCtrlConnected(remoteCtrl): return String(describing: remoteCtrl)
|
case let .remoteCtrlConnected(remoteCtrl): return String(describing: remoteCtrl)
|
||||||
case .remoteCtrlStopped: return noDetails
|
case .remoteCtrlStopped: return noDetails
|
||||||
case let .contactPQAllowed(u, contact): return withUser(u, "contact: \(String(describing: contact))")
|
|
||||||
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 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 .cmdOk: return noDetails
|
||||||
case let .chatCmdError(u, chatError): return withUser(u, String(describing: chatError))
|
case let .chatCmdError(u, chatError): return withUser(u, String(describing: chatError))
|
||||||
@@ -1011,6 +1005,10 @@ struct ComposedMessage: Encodable {
|
|||||||
var msgContent: MsgContent
|
var msgContent: MsgContent
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public struct XFTPFileConfig: Encodable {
|
||||||
|
var minFileSize: Int64
|
||||||
|
}
|
||||||
|
|
||||||
public struct ArchiveConfig: Encodable {
|
public struct ArchiveConfig: Encodable {
|
||||||
var archivePath: String
|
var archivePath: String
|
||||||
var disableCompression: Bool?
|
var disableCompression: Bool?
|
||||||
|
|||||||
@@ -39,7 +39,6 @@ let GROUP_DEFAULT_STORE_DB_PASSPHRASE = "storeDBPassphrase"
|
|||||||
let GROUP_DEFAULT_INITIAL_RANDOM_DB_PASSPHRASE = "initialRandomDBPassphrase"
|
let GROUP_DEFAULT_INITIAL_RANDOM_DB_PASSPHRASE = "initialRandomDBPassphrase"
|
||||||
public let GROUP_DEFAULT_CONFIRM_DB_UPGRADES = "confirmDBUpgrades"
|
public let GROUP_DEFAULT_CONFIRM_DB_UPGRADES = "confirmDBUpgrades"
|
||||||
public let GROUP_DEFAULT_CALL_KIT_ENABLED = "callKitEnabled"
|
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"
|
public let APP_GROUP_NAME = "group.chat.simplex.app"
|
||||||
|
|
||||||
@@ -68,7 +67,6 @@ public func registerGroupDefaults() {
|
|||||||
GROUP_DEFAULT_PRIVACY_ENCRYPT_LOCAL_FILES: true,
|
GROUP_DEFAULT_PRIVACY_ENCRYPT_LOCAL_FILES: true,
|
||||||
GROUP_DEFAULT_CONFIRM_DB_UPGRADES: false,
|
GROUP_DEFAULT_CONFIRM_DB_UPGRADES: false,
|
||||||
GROUP_DEFAULT_CALL_KIT_ENABLED: true,
|
GROUP_DEFAULT_CALL_KIT_ENABLED: true,
|
||||||
GROUP_DEFAULT_PQ_EXPERIMENTAL_ENABLED: false,
|
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -195,8 +193,6 @@ public let confirmDBUpgradesGroupDefault = BoolDefault(defaults: groupDefaults,
|
|||||||
|
|
||||||
public let callKitEnabledGroupDefault = BoolDefault(defaults: groupDefaults, forKey: GROUP_DEFAULT_CALL_KIT_ENABLED)
|
public let callKitEnabledGroupDefault = BoolDefault(defaults: groupDefaults, forKey: GROUP_DEFAULT_CALL_KIT_ENABLED)
|
||||||
|
|
||||||
public let pqExperimentalEnabledDefault = BoolDefault(defaults: groupDefaults, forKey: GROUP_DEFAULT_PRIVACY_ENCRYPT_LOCAL_FILES)
|
|
||||||
|
|
||||||
public class DateDefault {
|
public class DateDefault {
|
||||||
var defaults: UserDefaults
|
var defaults: UserDefaults
|
||||||
var key: String
|
var key: String
|
||||||
@@ -269,6 +265,10 @@ public class Default<T> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public func getXFTPCfg() -> XFTPFileConfig {
|
||||||
|
return XFTPFileConfig(minFileSize: 0)
|
||||||
|
}
|
||||||
|
|
||||||
public func getNetCfg() -> NetCfg {
|
public func getNetCfg() -> NetCfg {
|
||||||
let onionHosts = networkUseOnionHostsGroupDefault.get()
|
let onionHosts = networkUseOnionHostsGroupDefault.get()
|
||||||
let (hostMode, requiredHostMode) = onionHosts.hostMode
|
let (hostMode, requiredHostMode) = onionHosts.hostMode
|
||||||
|
|||||||
@@ -1367,17 +1367,6 @@ public enum ChatInfo: Identifiable, Decodable, NamedChat {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public var chatTs: Date {
|
|
||||||
switch self {
|
|
||||||
case let .direct(contact): return contact.chatTs ?? contact.updatedAt
|
|
||||||
case let .group(groupInfo): return groupInfo.chatTs ?? groupInfo.updatedAt
|
|
||||||
case let .local(noteFolder): return noteFolder.chatTs
|
|
||||||
case let .contactRequest(contactRequest): return contactRequest.updatedAt
|
|
||||||
case let .contactConnection(contactConnection): return contactConnection.updatedAt
|
|
||||||
case .invalidJSON: return .now
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public struct SampleData {
|
public struct SampleData {
|
||||||
public var direct: ChatInfo
|
public var direct: ChatInfo
|
||||||
public var group: ChatInfo
|
public var group: ChatInfo
|
||||||
@@ -1436,7 +1425,6 @@ public struct Contact: Identifiable, Decodable, NamedChat {
|
|||||||
public var mergedPreferences: ContactUserPreferences
|
public var mergedPreferences: ContactUserPreferences
|
||||||
var createdAt: Date
|
var createdAt: Date
|
||||||
var updatedAt: Date
|
var updatedAt: Date
|
||||||
var chatTs: Date?
|
|
||||||
var contactGroupMemberId: Int64?
|
var contactGroupMemberId: Int64?
|
||||||
var contactGrpInvSent: Bool
|
var contactGrpInvSent: Bool
|
||||||
|
|
||||||
@@ -1532,30 +1520,22 @@ public struct Connection: Decodable {
|
|||||||
public var viaGroupLink: Bool
|
public var viaGroupLink: Bool
|
||||||
public var customUserProfileId: Int64?
|
public var customUserProfileId: Int64?
|
||||||
public var connectionCode: SecurityCode?
|
public var connectionCode: SecurityCode?
|
||||||
public var enablePQ: Bool
|
|
||||||
public var pqSndEnabled: Bool?
|
|
||||||
public var pqRcvEnabled: Bool?
|
|
||||||
|
|
||||||
public var connectionStats: ConnectionStats? = nil
|
public var connectionStats: ConnectionStats? = nil
|
||||||
|
|
||||||
private enum CodingKeys: String, CodingKey {
|
private enum CodingKeys: String, CodingKey {
|
||||||
case connId, agentConnId, peerChatVRange, connStatus, connLevel, viaGroupLink, customUserProfileId, connectionCode, enablePQ, pqSndEnabled, pqRcvEnabled
|
case connId, agentConnId, peerChatVRange, connStatus, connLevel, viaGroupLink, customUserProfileId, connectionCode
|
||||||
}
|
}
|
||||||
|
|
||||||
public var id: ChatId { get { ":\(connId)" } }
|
public var id: ChatId { get { ":\(connId)" } }
|
||||||
|
|
||||||
public var connPQEnabled: Bool {
|
|
||||||
pqSndEnabled == true && pqRcvEnabled == true
|
|
||||||
}
|
|
||||||
|
|
||||||
static let sampleData = Connection(
|
static let sampleData = Connection(
|
||||||
connId: 1,
|
connId: 1,
|
||||||
agentConnId: "abc",
|
agentConnId: "abc",
|
||||||
peerChatVRange: VersionRange(minVersion: 1, maxVersion: 1),
|
peerChatVRange: VersionRange(minVersion: 1, maxVersion: 1),
|
||||||
connStatus: .ready,
|
connStatus: .ready,
|
||||||
connLevel: 0,
|
connLevel: 0,
|
||||||
viaGroupLink: false,
|
viaGroupLink: false
|
||||||
enablePQ: false
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1764,7 +1744,6 @@ public struct GroupInfo: Identifiable, Decodable, NamedChat {
|
|||||||
public var chatSettings: ChatSettings
|
public var chatSettings: ChatSettings
|
||||||
var createdAt: Date
|
var createdAt: Date
|
||||||
var updatedAt: Date
|
var updatedAt: Date
|
||||||
var chatTs: Date?
|
|
||||||
|
|
||||||
public var id: ChatId { get { "#\(groupId)" } }
|
public var id: ChatId { get { "#\(groupId)" } }
|
||||||
public var apiId: Int64 { get { groupId } }
|
public var apiId: Int64 { get { groupId } }
|
||||||
@@ -2070,7 +2049,6 @@ public struct NoteFolder: Identifiable, Decodable, NamedChat {
|
|||||||
public var unread: Bool
|
public var unread: Bool
|
||||||
var createdAt: Date
|
var createdAt: Date
|
||||||
public var updatedAt: Date
|
public var updatedAt: Date
|
||||||
var chatTs: Date
|
|
||||||
|
|
||||||
public var id: ChatId { get { "*\(noteFolderId)" } }
|
public var id: ChatId { get { "*\(noteFolderId)" } }
|
||||||
public var apiId: Int64 { get { noteFolderId } }
|
public var apiId: Int64 { get { noteFolderId } }
|
||||||
@@ -2092,8 +2070,7 @@ public struct NoteFolder: Identifiable, Decodable, NamedChat {
|
|||||||
favorite: false,
|
favorite: false,
|
||||||
unread: false,
|
unread: false,
|
||||||
createdAt: .now,
|
createdAt: .now,
|
||||||
updatedAt: .now,
|
updatedAt: .now
|
||||||
chatTs: .now
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2276,7 +2253,7 @@ public struct ChatItem: Identifiable, Decodable {
|
|||||||
case .rcvDirectEvent(rcvDirectEvent: let rcvDirectEvent):
|
case .rcvDirectEvent(rcvDirectEvent: let rcvDirectEvent):
|
||||||
switch rcvDirectEvent {
|
switch rcvDirectEvent {
|
||||||
case .contactDeleted: return false
|
case .contactDeleted: return false
|
||||||
case .profileUpdated: return false
|
case .profileUpdated: return true
|
||||||
}
|
}
|
||||||
case .rcvGroupEvent(rcvGroupEvent: let rcvGroupEvent):
|
case .rcvGroupEvent(rcvGroupEvent: let rcvGroupEvent):
|
||||||
switch rcvGroupEvent {
|
switch rcvGroupEvent {
|
||||||
@@ -2308,10 +2285,6 @@ public struct ChatItem: Identifiable, Decodable {
|
|||||||
case .sndModerated: return false
|
case .sndModerated: return false
|
||||||
case .rcvModerated: return false
|
case .rcvModerated: return false
|
||||||
case .rcvBlocked: return false
|
case .rcvBlocked: return false
|
||||||
case .sndDirectE2EEInfo: return false
|
|
||||||
case .rcvDirectE2EEInfo: return false
|
|
||||||
case .sndGroupE2EEInfo: return false
|
|
||||||
case .rcvGroupE2EEInfo: return false
|
|
||||||
case .invalidJSON: return false
|
case .invalidJSON: return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2747,10 +2720,6 @@ public enum CIContent: Decodable, ItemContent {
|
|||||||
case sndModerated
|
case sndModerated
|
||||||
case rcvModerated
|
case rcvModerated
|
||||||
case rcvBlocked
|
case rcvBlocked
|
||||||
case sndDirectE2EEInfo(e2eeInfo: E2EEInfo)
|
|
||||||
case rcvDirectE2EEInfo(e2eeInfo: E2EEInfo)
|
|
||||||
case sndGroupE2EEInfo(e2eeInfo: E2EEInfo)
|
|
||||||
case rcvGroupE2EEInfo(e2eeInfo: E2EEInfo)
|
|
||||||
case invalidJSON(json: String)
|
case invalidJSON(json: String)
|
||||||
|
|
||||||
public var text: String {
|
public var text: String {
|
||||||
@@ -2782,25 +2751,11 @@ public enum CIContent: Decodable, ItemContent {
|
|||||||
case .sndModerated: return NSLocalizedString("moderated", comment: "moderated chat item")
|
case .sndModerated: return NSLocalizedString("moderated", comment: "moderated chat item")
|
||||||
case .rcvModerated: return NSLocalizedString("moderated", comment: "moderated chat item")
|
case .rcvModerated: return NSLocalizedString("moderated", comment: "moderated chat item")
|
||||||
case .rcvBlocked: return NSLocalizedString("blocked by admin", comment: "blocked chat item")
|
case .rcvBlocked: return NSLocalizedString("blocked by admin", comment: "blocked chat item")
|
||||||
case let .sndDirectE2EEInfo(e2eeInfo): return directE2EEInfoToText(e2eeInfo)
|
|
||||||
case let .rcvDirectE2EEInfo(e2eeInfo): return directE2EEInfoToText(e2eeInfo)
|
|
||||||
case .sndGroupE2EEInfo: return e2eeInfoNoPQText
|
|
||||||
case .rcvGroupE2EEInfo: return e2eeInfoNoPQText
|
|
||||||
case .invalidJSON: return NSLocalizedString("invalid data", comment: "invalid chat item")
|
case .invalidJSON: return NSLocalizedString("invalid data", comment: "invalid chat item")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func directE2EEInfoToText(_ e2eeInfo: E2EEInfo) -> String {
|
|
||||||
e2eeInfo.pqEnabled
|
|
||||||
? NSLocalizedString("This conversation is protected by quantum resistant end-to-end encryption. It has perfect forward secrecy, repudiation and quantum resistant break-in recovery.", comment: "E2EE info chat item")
|
|
||||||
: e2eeInfoNoPQText
|
|
||||||
}
|
|
||||||
|
|
||||||
private var e2eeInfoNoPQText: String {
|
|
||||||
NSLocalizedString("This conversation is protected by end-to-end encryption with perfect forward secrecy, repudiation and break-in recovery.", comment: "E2EE info chat item")
|
|
||||||
}
|
|
||||||
|
|
||||||
static func featureText(_ feature: Feature, _ enabled: String, _ param: Int?) -> String {
|
static func featureText(_ feature: Feature, _ enabled: String, _ param: Int?) -> String {
|
||||||
feature.hasParam
|
feature.hasParam
|
||||||
? "\(feature.text): \(timeText(param))"
|
? "\(feature.text): \(timeText(param))"
|
||||||
@@ -3487,10 +3442,6 @@ public enum CIGroupInvitationStatus: String, Decodable {
|
|||||||
case expired
|
case expired
|
||||||
}
|
}
|
||||||
|
|
||||||
public struct E2EEInfo: Decodable {
|
|
||||||
public var pqEnabled: Bool
|
|
||||||
}
|
|
||||||
|
|
||||||
public enum RcvDirectEvent: Decodable {
|
public enum RcvDirectEvent: Decodable {
|
||||||
case contactDeleted
|
case contactDeleted
|
||||||
case profileUpdated(fromProfile: Profile, toProfile: Profile)
|
case profileUpdated(fromProfile: Profile, toProfile: Profile)
|
||||||
@@ -3608,7 +3559,6 @@ public enum RcvConnEvent: Decodable {
|
|||||||
case switchQueue(phase: SwitchPhase)
|
case switchQueue(phase: SwitchPhase)
|
||||||
case ratchetSync(syncStatus: RatchetSyncState)
|
case ratchetSync(syncStatus: RatchetSyncState)
|
||||||
case verificationCodeReset
|
case verificationCodeReset
|
||||||
case pqEnabled(enabled: Bool)
|
|
||||||
|
|
||||||
var text: String {
|
var text: String {
|
||||||
switch self {
|
switch self {
|
||||||
@@ -3621,12 +3571,6 @@ public enum RcvConnEvent: Decodable {
|
|||||||
return ratchetSyncStatusToText(syncStatus)
|
return ratchetSyncStatusToText(syncStatus)
|
||||||
case .verificationCodeReset:
|
case .verificationCodeReset:
|
||||||
return NSLocalizedString("security code changed", comment: "chat item text")
|
return NSLocalizedString("security code changed", comment: "chat item text")
|
||||||
case let .pqEnabled(enabled):
|
|
||||||
if enabled {
|
|
||||||
return NSLocalizedString("enabled post-quantum encryption", comment: "chat item text")
|
|
||||||
} else {
|
|
||||||
return NSLocalizedString("disabled post-quantum encryption", comment: "chat item text")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -3644,7 +3588,6 @@ func ratchetSyncStatusToText(_ ratchetSyncStatus: RatchetSyncState) -> String {
|
|||||||
public enum SndConnEvent: Decodable {
|
public enum SndConnEvent: Decodable {
|
||||||
case switchQueue(phase: SwitchPhase, member: GroupMemberRef?)
|
case switchQueue(phase: SwitchPhase, member: GroupMemberRef?)
|
||||||
case ratchetSync(syncStatus: RatchetSyncState, member: GroupMemberRef?)
|
case ratchetSync(syncStatus: RatchetSyncState, member: GroupMemberRef?)
|
||||||
case pqEnabled(enabled: Bool)
|
|
||||||
|
|
||||||
var text: String {
|
var text: String {
|
||||||
switch self {
|
switch self {
|
||||||
@@ -3668,12 +3611,6 @@ public enum SndConnEvent: Decodable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
return ratchetSyncStatusToText(syncStatus)
|
return ratchetSyncStatusToText(syncStatus)
|
||||||
case let .pqEnabled(enabled):
|
|
||||||
if enabled {
|
|
||||||
return NSLocalizedString("enabled post-quantum encryption", comment: "chat item text")
|
|
||||||
} else {
|
|
||||||
return NSLocalizedString("disabled post-quantum encryption", comment: "chat item text")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,7 +25,6 @@ extern char *chat_parse_markdown(char *str);
|
|||||||
extern char *chat_parse_server(char *str);
|
extern char *chat_parse_server(char *str);
|
||||||
extern char *chat_password_hash(char *pwd, char *salt);
|
extern char *chat_password_hash(char *pwd, char *salt);
|
||||||
extern char *chat_valid_name(char *name);
|
extern char *chat_valid_name(char *name);
|
||||||
extern int chat_json_length(char *str);
|
|
||||||
extern char *chat_encrypt_media(chat_ctrl ctl, char *key, char *frame, int len);
|
extern char *chat_encrypt_media(chat_ctrl ctl, char *key, char *frame, int len);
|
||||||
extern char *chat_decrypt_media(char *key, char *frame, int len);
|
extern char *chat_decrypt_media(char *key, char *frame, int len);
|
||||||
|
|
||||||
|
|||||||
@@ -202,9 +202,6 @@
|
|||||||
/* No comment provided by engineer. */
|
/* No comment provided by engineer. */
|
||||||
"%lld messages blocked" = "%lld Nachrichten blockiert";
|
"%lld messages blocked" = "%lld Nachrichten blockiert";
|
||||||
|
|
||||||
/* No comment provided by engineer. */
|
|
||||||
"%lld messages blocked by admin" = "%lld Nachrichten wurden vom Administrator blockiert";
|
|
||||||
|
|
||||||
/* No comment provided by engineer. */
|
/* No comment provided by engineer. */
|
||||||
"%lld messages marked deleted" = "%lld Nachrichten als gelöscht markiert";
|
"%lld messages marked deleted" = "%lld Nachrichten als gelöscht markiert";
|
||||||
|
|
||||||
@@ -401,14 +398,11 @@
|
|||||||
/* No comment provided by engineer. */
|
/* No comment provided by engineer. */
|
||||||
"All group members will remain connected." = "Alle Gruppenmitglieder bleiben verbunden.";
|
"All group members will remain connected." = "Alle Gruppenmitglieder bleiben verbunden.";
|
||||||
|
|
||||||
/* No comment provided by engineer. */
|
|
||||||
"All messages will be deleted - this cannot be undone!" = "Es werden alle Nachrichten gelöscht. Dieser Vorgang kann nicht rückgängig gemacht werden!";
|
|
||||||
|
|
||||||
/* No comment provided by engineer. */
|
/* No comment provided by engineer. */
|
||||||
"All messages will be deleted - this cannot be undone! The messages will be deleted ONLY for you." = "Alle Nachrichten werden gelöscht - dies kann nicht rückgängig gemacht werden! Die Nachrichten werden NUR bei Ihnen gelöscht.";
|
"All messages will be deleted - this cannot be undone! The messages will be deleted ONLY for you." = "Alle Nachrichten werden gelöscht - dies kann nicht rückgängig gemacht werden! Die Nachrichten werden NUR bei Ihnen gelöscht.";
|
||||||
|
|
||||||
/* No comment provided by engineer. */
|
/* No comment provided by engineer. */
|
||||||
"All new messages from %@ will be hidden!" = "Von %@ werden alle neuen Nachrichten ausgeblendet!";
|
"All new messages from %@ will be hidden!" = "Alle neuen Nachrichten von %@ werden verborgen!";
|
||||||
|
|
||||||
/* No comment provided by engineer. */
|
/* No comment provided by engineer. */
|
||||||
"All your contacts will remain connected." = "Alle Ihre Kontakte bleiben verbunden.";
|
"All your contacts will remain connected." = "Alle Ihre Kontakte bleiben verbunden.";
|
||||||
@@ -587,32 +581,17 @@
|
|||||||
/* No comment provided by engineer. */
|
/* No comment provided by engineer. */
|
||||||
"Block" = "Blockieren";
|
"Block" = "Blockieren";
|
||||||
|
|
||||||
/* No comment provided by engineer. */
|
|
||||||
"Block for all" = "Für Alle blockieren";
|
|
||||||
|
|
||||||
/* No comment provided by engineer. */
|
/* No comment provided by engineer. */
|
||||||
"Block group members" = "Gruppenmitglieder blockieren";
|
"Block group members" = "Gruppenmitglieder blockieren";
|
||||||
|
|
||||||
/* No comment provided by engineer. */
|
/* No comment provided by engineer. */
|
||||||
"Block member" = "Mitglied blockieren";
|
"Block member" = "Mitglied blockieren";
|
||||||
|
|
||||||
/* No comment provided by engineer. */
|
|
||||||
"Block member for all?" = "Mitglied für Alle blockieren?";
|
|
||||||
|
|
||||||
/* No comment provided by engineer. */
|
/* No comment provided by engineer. */
|
||||||
"Block member?" = "Mitglied blockieren?";
|
"Block member?" = "Mitglied blockieren?";
|
||||||
|
|
||||||
/* No comment provided by engineer. */
|
/* No comment provided by engineer. */
|
||||||
"blocked" = "Blockiert";
|
"blocked" = "blockiert";
|
||||||
|
|
||||||
/* rcv group event chat item */
|
|
||||||
"blocked %@" = "%@ wurde blockiert";
|
|
||||||
|
|
||||||
/* blocked chat item */
|
|
||||||
"blocked by admin" = "wurde vom Administrator blockiert";
|
|
||||||
|
|
||||||
/* No comment provided by engineer. */
|
|
||||||
"Blocked by admin" = "wurde vom Administrator blockiert";
|
|
||||||
|
|
||||||
/* No comment provided by engineer. */
|
/* No comment provided by engineer. */
|
||||||
"bold" = "fett";
|
"bold" = "fett";
|
||||||
@@ -771,9 +750,6 @@
|
|||||||
/* No comment provided by engineer. */
|
/* No comment provided by engineer. */
|
||||||
"Clear conversation?" = "Unterhaltung löschen?";
|
"Clear conversation?" = "Unterhaltung löschen?";
|
||||||
|
|
||||||
/* No comment provided by engineer. */
|
|
||||||
"Clear private notes?" = "Private Notizen löschen?";
|
|
||||||
|
|
||||||
/* No comment provided by engineer. */
|
/* No comment provided by engineer. */
|
||||||
"Clear verification" = "Überprüfung zurücknehmen";
|
"Clear verification" = "Überprüfung zurücknehmen";
|
||||||
|
|
||||||
@@ -912,9 +888,6 @@
|
|||||||
/* connection information */
|
/* connection information */
|
||||||
"connection:%@" = "Verbindung:%@";
|
"connection:%@" = "Verbindung:%@";
|
||||||
|
|
||||||
/* profile update event chat item */
|
|
||||||
"contact %@ changed to %@" = "Der Kontaktname %1$@ wurde auf %2$@ geändert";
|
|
||||||
|
|
||||||
/* No comment provided by engineer. */
|
/* No comment provided by engineer. */
|
||||||
"Contact allows" = "Der Kontakt erlaubt";
|
"Contact allows" = "Der Kontakt erlaubt";
|
||||||
|
|
||||||
@@ -999,12 +972,6 @@
|
|||||||
/* No comment provided by engineer. */
|
/* No comment provided by engineer. */
|
||||||
"Create your profile" = "Erstellen Sie Ihr Profil";
|
"Create your profile" = "Erstellen Sie Ihr Profil";
|
||||||
|
|
||||||
/* No comment provided by engineer. */
|
|
||||||
"Created at" = "Erstellt um";
|
|
||||||
|
|
||||||
/* copied message info */
|
|
||||||
"Created at: %@" = "Erstellt um: %@";
|
|
||||||
|
|
||||||
/* No comment provided by engineer. */
|
/* No comment provided by engineer. */
|
||||||
"Created on %@" = "Erstellt am %@";
|
"Created on %@" = "Erstellt am %@";
|
||||||
|
|
||||||
@@ -1557,9 +1524,6 @@
|
|||||||
/* No comment provided by engineer. */
|
/* No comment provided by engineer. */
|
||||||
"Error creating member contact" = "Fehler beim Anlegen eines Mitglied-Kontaktes";
|
"Error creating member contact" = "Fehler beim Anlegen eines Mitglied-Kontaktes";
|
||||||
|
|
||||||
/* No comment provided by engineer. */
|
|
||||||
"Error creating message" = "Fehler beim Erstellen der Nachricht";
|
|
||||||
|
|
||||||
/* No comment provided by engineer. */
|
/* No comment provided by engineer. */
|
||||||
"Error creating profile!" = "Fehler beim Erstellen des Profils!";
|
"Error creating profile!" = "Fehler beim Erstellen des Profils!";
|
||||||
|
|
||||||
@@ -1974,9 +1938,6 @@
|
|||||||
/* No comment provided by engineer. */
|
/* No comment provided by engineer. */
|
||||||
"Import database" = "Datenbank importieren";
|
"Import database" = "Datenbank importieren";
|
||||||
|
|
||||||
/* No comment provided by engineer. */
|
|
||||||
"Improved message delivery" = "Verbesserte Zustellung von Nachrichten";
|
|
||||||
|
|
||||||
/* No comment provided by engineer. */
|
/* No comment provided by engineer. */
|
||||||
"Improved privacy and security" = "Verbesserte Privatsphäre und Sicherheit";
|
"Improved privacy and security" = "Verbesserte Privatsphäre und Sicherheit";
|
||||||
|
|
||||||
@@ -2154,9 +2115,6 @@
|
|||||||
/* No comment provided by engineer. */
|
/* No comment provided by engineer. */
|
||||||
"Join group" = "Treten Sie der Gruppe bei";
|
"Join group" = "Treten Sie der Gruppe bei";
|
||||||
|
|
||||||
/* No comment provided by engineer. */
|
|
||||||
"Join group conversations" = "Gruppenunterhaltungen beitreten";
|
|
||||||
|
|
||||||
/* No comment provided by engineer. */
|
/* No comment provided by engineer. */
|
||||||
"Join group?" = "Der Gruppe beitreten?";
|
"Join group?" = "Der Gruppe beitreten?";
|
||||||
|
|
||||||
@@ -2292,9 +2250,6 @@
|
|||||||
/* No comment provided by engineer. */
|
/* No comment provided by engineer. */
|
||||||
"Member" = "Mitglied";
|
"Member" = "Mitglied";
|
||||||
|
|
||||||
/* profile update event chat item */
|
|
||||||
"member %@ changed to %@" = "Der Mitgliedsname %1$@ wurde auf %2$@ geändert";
|
|
||||||
|
|
||||||
/* rcv group event chat item */
|
/* rcv group event chat item */
|
||||||
"member connected" = "ist der Gruppe beigetreten";
|
"member connected" = "ist der Gruppe beigetreten";
|
||||||
|
|
||||||
@@ -2639,18 +2594,12 @@
|
|||||||
/* No comment provided by engineer. */
|
/* No comment provided by engineer. */
|
||||||
"Password to show" = "Passwort anzeigen";
|
"Password to show" = "Passwort anzeigen";
|
||||||
|
|
||||||
/* past/unknown group member */
|
|
||||||
"Past member %@" = "Ehemaliges Mitglied %@";
|
|
||||||
|
|
||||||
/* No comment provided by engineer. */
|
/* No comment provided by engineer. */
|
||||||
"Paste desktop address" = "Desktop-Adresse einfügen";
|
"Paste desktop address" = "Desktop-Adresse einfügen";
|
||||||
|
|
||||||
/* No comment provided by engineer. */
|
/* No comment provided by engineer. */
|
||||||
"Paste image" = "Bild einfügen";
|
"Paste image" = "Bild einfügen";
|
||||||
|
|
||||||
/* No comment provided by engineer. */
|
|
||||||
"Paste link to connect!" = "Zum Verbinden den Link einfügen!";
|
|
||||||
|
|
||||||
/* No comment provided by engineer. */
|
/* No comment provided by engineer. */
|
||||||
"Paste the link you received" = "Fügen Sie den erhaltenen Link ein";
|
"Paste the link you received" = "Fügen Sie den erhaltenen Link ein";
|
||||||
|
|
||||||
@@ -2738,9 +2687,6 @@
|
|||||||
/* No comment provided by engineer. */
|
/* No comment provided by engineer. */
|
||||||
"Private filenames" = "Neutrale Dateinamen";
|
"Private filenames" = "Neutrale Dateinamen";
|
||||||
|
|
||||||
/* name of notes to self */
|
|
||||||
"Private notes" = "Private Notizen";
|
|
||||||
|
|
||||||
/* No comment provided by engineer. */
|
/* No comment provided by engineer. */
|
||||||
"Profile and server connections" = "Profil und Serververbindungen";
|
"Profile and server connections" = "Profil und Serververbindungen";
|
||||||
|
|
||||||
@@ -2855,9 +2801,6 @@
|
|||||||
/* No comment provided by engineer. */
|
/* No comment provided by engineer. */
|
||||||
"Receiving via" = "Empfangen über";
|
"Receiving via" = "Empfangen über";
|
||||||
|
|
||||||
/* No comment provided by engineer. */
|
|
||||||
"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)." = "Aktueller Nachrichtenverlauf und verbesserter [Gruppenverzeichnis-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).";
|
|
||||||
|
|
||||||
/* No comment provided by engineer. */
|
/* No comment provided by engineer. */
|
||||||
"Recipients see updates as you type them." = "Die Empfänger sehen Nachrichtenaktualisierungen, während Sie sie eingeben.";
|
"Recipients see updates as you type them." = "Die Empfänger sehen Nachrichtenaktualisierungen, während Sie sie eingeben.";
|
||||||
|
|
||||||
@@ -2912,12 +2855,6 @@
|
|||||||
/* rcv group event chat item */
|
/* rcv group event chat item */
|
||||||
"removed %@" = "hat %@ aus der Gruppe entfernt";
|
"removed %@" = "hat %@ aus der Gruppe entfernt";
|
||||||
|
|
||||||
/* profile update event chat item */
|
|
||||||
"removed contact address" = "Kontaktadresse wurde entfernt";
|
|
||||||
|
|
||||||
/* profile update event chat item */
|
|
||||||
"removed profile picture" = "Profil-Bild wurde entfernt";
|
|
||||||
|
|
||||||
/* rcv group event chat item */
|
/* rcv group event chat item */
|
||||||
"removed you" = "hat Sie aus der Gruppe entfernt";
|
"removed you" = "hat Sie aus der Gruppe entfernt";
|
||||||
|
|
||||||
@@ -3041,9 +2978,6 @@
|
|||||||
/* No comment provided by engineer. */
|
/* No comment provided by engineer. */
|
||||||
"Save welcome message?" = "Begrüßungsmeldung speichern?";
|
"Save welcome message?" = "Begrüßungsmeldung speichern?";
|
||||||
|
|
||||||
/* message info title */
|
|
||||||
"Saved message" = "Gespeicherte Nachricht";
|
|
||||||
|
|
||||||
/* No comment provided by engineer. */
|
/* No comment provided by engineer. */
|
||||||
"Saved WebRTC ICE servers will be removed" = "Gespeicherte WebRTC ICE-Server werden entfernt";
|
"Saved WebRTC ICE servers will be removed" = "Gespeicherte WebRTC ICE-Server werden entfernt";
|
||||||
|
|
||||||
@@ -3065,9 +2999,6 @@
|
|||||||
/* No comment provided by engineer. */
|
/* No comment provided by engineer. */
|
||||||
"Search" = "Suche";
|
"Search" = "Suche";
|
||||||
|
|
||||||
/* No comment provided by engineer. */
|
|
||||||
"Search bar accepts invitation links." = "Von der Suchleiste werden Einladungslinks akzeptiert.";
|
|
||||||
|
|
||||||
/* No comment provided by engineer. */
|
/* No comment provided by engineer. */
|
||||||
"Search or paste SimpleX link" = "Suchen oder fügen Sie den SimpleX-Link ein";
|
"Search or paste SimpleX link" = "Suchen oder fügen Sie den SimpleX-Link ein";
|
||||||
|
|
||||||
@@ -3224,12 +3155,6 @@
|
|||||||
/* No comment provided by engineer. */
|
/* No comment provided by engineer. */
|
||||||
"Set it instead of system authentication." = "Anstelle der System-Authentifizierung festlegen.";
|
"Set it instead of system authentication." = "Anstelle der System-Authentifizierung festlegen.";
|
||||||
|
|
||||||
/* profile update event chat item */
|
|
||||||
"set new contact address" = "Neue Kontaktadresse wurde festgelegt";
|
|
||||||
|
|
||||||
/* profile update event chat item */
|
|
||||||
"set new profile picture" = "Neues Profil-Bild wurde festgelegt";
|
|
||||||
|
|
||||||
/* No comment provided by engineer. */
|
/* No comment provided by engineer. */
|
||||||
"Set passcode" = "Zugangscode einstellen";
|
"Set passcode" = "Zugangscode einstellen";
|
||||||
|
|
||||||
@@ -3599,9 +3524,6 @@
|
|||||||
/* No comment provided by engineer. */
|
/* No comment provided by engineer. */
|
||||||
"Trying to connect to the server used to receive messages from this contact." = "Versuche die Verbindung mit dem Server aufzunehmen, der für den Empfang von Nachrichten mit diesem Kontakt genutzt wird.";
|
"Trying to connect to the server used to receive messages from this contact." = "Versuche die Verbindung mit dem Server aufzunehmen, der für den Empfang von Nachrichten mit diesem Kontakt genutzt wird.";
|
||||||
|
|
||||||
/* No comment provided by engineer. */
|
|
||||||
"Turkish interface" = "Türkische Bedienoberfläche";
|
|
||||||
|
|
||||||
/* No comment provided by engineer. */
|
/* No comment provided by engineer. */
|
||||||
"Turn off" = "Abschalten";
|
"Turn off" = "Abschalten";
|
||||||
|
|
||||||
@@ -3614,21 +3536,12 @@
|
|||||||
/* No comment provided by engineer. */
|
/* No comment provided by engineer. */
|
||||||
"Unblock" = "Freigeben";
|
"Unblock" = "Freigeben";
|
||||||
|
|
||||||
/* No comment provided by engineer. */
|
|
||||||
"Unblock for all" = "Für Alle freigeben";
|
|
||||||
|
|
||||||
/* No comment provided by engineer. */
|
/* No comment provided by engineer. */
|
||||||
"Unblock member" = "Mitglied freigeben";
|
"Unblock member" = "Mitglied freigeben";
|
||||||
|
|
||||||
/* No comment provided by engineer. */
|
|
||||||
"Unblock member for all?" = "Mitglied für Alle freigeben?";
|
|
||||||
|
|
||||||
/* No comment provided by engineer. */
|
/* No comment provided by engineer. */
|
||||||
"Unblock member?" = "Mitglied freigeben?";
|
"Unblock member?" = "Mitglied freigeben?";
|
||||||
|
|
||||||
/* rcv group event chat item */
|
|
||||||
"unblocked %@" = "%@ wurde freigegeben";
|
|
||||||
|
|
||||||
/* item status description */
|
/* item status description */
|
||||||
"Unexpected error: %@" = "Unerwarteter Fehler: %@";
|
"Unexpected error: %@" = "Unerwarteter Fehler: %@";
|
||||||
|
|
||||||
@@ -3662,9 +3575,6 @@
|
|||||||
/* No comment provided by engineer. */
|
/* No comment provided by engineer. */
|
||||||
"Unknown error" = "Unbekannter Fehler";
|
"Unknown error" = "Unbekannter Fehler";
|
||||||
|
|
||||||
/* No comment provided by engineer. */
|
|
||||||
"unknown status" = "unbekannter Gruppenmitglieds-Status";
|
|
||||||
|
|
||||||
/* No comment provided by engineer. */
|
/* No comment provided by engineer. */
|
||||||
"Unless you use iOS call interface, enable Do Not Disturb mode to avoid interruptions." = "Aktivieren Sie den Modus \"Bitte nicht stören\", um Unterbrechungen zu vermeiden, es sei denn, Sie verwenden die iOS Anrufschnittstelle.";
|
"Unless you use iOS call interface, enable Do Not Disturb mode to avoid interruptions." = "Aktivieren Sie den Modus \"Bitte nicht stören\", um Unterbrechungen zu vermeiden, es sei denn, Sie verwenden die iOS Anrufschnittstelle.";
|
||||||
|
|
||||||
@@ -3710,9 +3620,6 @@
|
|||||||
/* rcv group event chat item */
|
/* rcv group event chat item */
|
||||||
"updated group profile" = "Aktualisiertes Gruppenprofil";
|
"updated group profile" = "Aktualisiertes Gruppenprofil";
|
||||||
|
|
||||||
/* profile update event chat item */
|
|
||||||
"updated profile" = "Das Profil wurde aktualisiert";
|
|
||||||
|
|
||||||
/* No comment provided by engineer. */
|
/* No comment provided by engineer. */
|
||||||
"Updating settings will re-connect the client to all servers." = "Die Aktualisierung der Einstellungen wird den Client wieder mit allen Servern verbinden.";
|
"Updating settings will re-connect the client to all servers." = "Die Aktualisierung der Einstellungen wird den Client wieder mit allen Servern verbinden.";
|
||||||
|
|
||||||
@@ -3887,15 +3794,9 @@
|
|||||||
/* No comment provided by engineer. */
|
/* No comment provided by engineer. */
|
||||||
"When you share an incognito profile with somebody, this profile will be used for the groups they invite you to." = "Wenn Sie ein Inkognito-Profil mit Jemandem teilen, wird dieses Profil auch für die Gruppen verwendet, für die Sie von diesem Kontakt eingeladen werden.";
|
"When you share an incognito profile with somebody, this profile will be used for the groups they invite you to." = "Wenn Sie ein Inkognito-Profil mit Jemandem teilen, wird dieses Profil auch für die Gruppen verwendet, für die Sie von diesem Kontakt eingeladen werden.";
|
||||||
|
|
||||||
/* No comment provided by engineer. */
|
|
||||||
"With encrypted files and media." = "Mit verschlüsselten Dateien und Medien.";
|
|
||||||
|
|
||||||
/* No comment provided by engineer. */
|
/* No comment provided by engineer. */
|
||||||
"With optional welcome message." = "Mit optionaler Begrüßungsmeldung.";
|
"With optional welcome message." = "Mit optionaler Begrüßungsmeldung.";
|
||||||
|
|
||||||
/* No comment provided by engineer. */
|
|
||||||
"With reduced battery usage." = "Mit reduziertem Akkuverbrauch.";
|
|
||||||
|
|
||||||
/* No comment provided by engineer. */
|
/* No comment provided by engineer. */
|
||||||
"Wrong database passphrase" = "Falsches Datenbank-Passwort";
|
"Wrong database passphrase" = "Falsches Datenbank-Passwort";
|
||||||
|
|
||||||
@@ -3956,9 +3857,6 @@
|
|||||||
/* No comment provided by engineer. */
|
/* No comment provided by engineer. */
|
||||||
"you are observer" = "Sie sind Beobachter";
|
"you are observer" = "Sie sind Beobachter";
|
||||||
|
|
||||||
/* snd group event chat item */
|
|
||||||
"you blocked %@" = "Sie haben %@ blockiert";
|
|
||||||
|
|
||||||
/* No comment provided by engineer. */
|
/* No comment provided by engineer. */
|
||||||
"You can accept calls from lock screen, without device and app authentication." = "Sie können Anrufe ohne Geräte- und App-Authentifizierung vom Sperrbildschirm aus annehmen.";
|
"You can accept calls from lock screen, without device and app authentication." = "Sie können Anrufe ohne Geräte- und App-Authentifizierung vom Sperrbildschirm aus annehmen.";
|
||||||
|
|
||||||
@@ -4070,9 +3968,6 @@
|
|||||||
/* chat list item description */
|
/* chat list item description */
|
||||||
"you shared one-time link incognito" = "Sie haben Inkognito einen Einmal-Link geteilt";
|
"you shared one-time link incognito" = "Sie haben Inkognito einen Einmal-Link geteilt";
|
||||||
|
|
||||||
/* snd group event chat item */
|
|
||||||
"you unblocked %@" = "Sie haben %@ freigegeben";
|
|
||||||
|
|
||||||
/* No comment provided by engineer. */
|
/* No comment provided by engineer. */
|
||||||
"You will be connected to group when the group host's device is online, please wait or check later!" = "Sie werden mit der Gruppe verbunden, sobald das Endgerät des Gruppen-Hosts online ist. Bitte warten oder schauen Sie später nochmal nach!";
|
"You will be connected to group when the group host's device is online, please wait or check later!" = "Sie werden mit der Gruppe verbunden, sobald das Endgerät des Gruppen-Hosts online ist. Bitte warten oder schauen Sie später nochmal nach!";
|
||||||
|
|
||||||
|
|||||||
@@ -202,9 +202,6 @@
|
|||||||
/* No comment provided by engineer. */
|
/* No comment provided by engineer. */
|
||||||
"%lld messages blocked" = "%lld messaggi bloccati";
|
"%lld messages blocked" = "%lld messaggi bloccati";
|
||||||
|
|
||||||
/* No comment provided by engineer. */
|
|
||||||
"%lld messages blocked by admin" = "%lld messaggi bloccati dall'amministratore";
|
|
||||||
|
|
||||||
/* No comment provided by engineer. */
|
/* No comment provided by engineer. */
|
||||||
"%lld messages marked deleted" = "%lld messaggi contrassegnati eliminati";
|
"%lld messages marked deleted" = "%lld messaggi contrassegnati eliminati";
|
||||||
|
|
||||||
@@ -587,33 +584,18 @@
|
|||||||
/* No comment provided by engineer. */
|
/* No comment provided by engineer. */
|
||||||
"Block" = "Blocca";
|
"Block" = "Blocca";
|
||||||
|
|
||||||
/* No comment provided by engineer. */
|
|
||||||
"Block for all" = "Blocca per tutti";
|
|
||||||
|
|
||||||
/* No comment provided by engineer. */
|
/* No comment provided by engineer. */
|
||||||
"Block group members" = "Blocca i membri dei gruppi";
|
"Block group members" = "Blocca i membri dei gruppi";
|
||||||
|
|
||||||
/* No comment provided by engineer. */
|
/* No comment provided by engineer. */
|
||||||
"Block member" = "Blocca membro";
|
"Block member" = "Blocca membro";
|
||||||
|
|
||||||
/* No comment provided by engineer. */
|
|
||||||
"Block member for all?" = "Bloccare il membro per tutti?";
|
|
||||||
|
|
||||||
/* No comment provided by engineer. */
|
/* No comment provided by engineer. */
|
||||||
"Block member?" = "Bloccare il membro?";
|
"Block member?" = "Bloccare il membro?";
|
||||||
|
|
||||||
/* No comment provided by engineer. */
|
/* No comment provided by engineer. */
|
||||||
"blocked" = "bloccato";
|
"blocked" = "bloccato";
|
||||||
|
|
||||||
/* rcv group event chat item */
|
|
||||||
"blocked %@" = "ha bloccato %@";
|
|
||||||
|
|
||||||
/* blocked chat item */
|
|
||||||
"blocked by admin" = "bloccato dall'amministratore";
|
|
||||||
|
|
||||||
/* No comment provided by engineer. */
|
|
||||||
"Blocked by admin" = "Bloccato dall'amministratore";
|
|
||||||
|
|
||||||
/* No comment provided by engineer. */
|
/* No comment provided by engineer. */
|
||||||
"bold" = "grassetto";
|
"bold" = "grassetto";
|
||||||
|
|
||||||
@@ -3614,21 +3596,12 @@
|
|||||||
/* No comment provided by engineer. */
|
/* No comment provided by engineer. */
|
||||||
"Unblock" = "Sblocca";
|
"Unblock" = "Sblocca";
|
||||||
|
|
||||||
/* No comment provided by engineer. */
|
|
||||||
"Unblock for all" = "Sblocca per tutti";
|
|
||||||
|
|
||||||
/* No comment provided by engineer. */
|
/* No comment provided by engineer. */
|
||||||
"Unblock member" = "Sblocca membro";
|
"Unblock member" = "Sblocca membro";
|
||||||
|
|
||||||
/* No comment provided by engineer. */
|
|
||||||
"Unblock member for all?" = "Sbloccare il membro per tutti?";
|
|
||||||
|
|
||||||
/* No comment provided by engineer. */
|
/* No comment provided by engineer. */
|
||||||
"Unblock member?" = "Sbloccare il membro?";
|
"Unblock member?" = "Sbloccare il membro?";
|
||||||
|
|
||||||
/* rcv group event chat item */
|
|
||||||
"unblocked %@" = "ha sbloccato %@";
|
|
||||||
|
|
||||||
/* item status description */
|
/* item status description */
|
||||||
"Unexpected error: %@" = "Errore imprevisto: % @";
|
"Unexpected error: %@" = "Errore imprevisto: % @";
|
||||||
|
|
||||||
@@ -3956,9 +3929,6 @@
|
|||||||
/* No comment provided by engineer. */
|
/* No comment provided by engineer. */
|
||||||
"you are observer" = "sei un osservatore";
|
"you are observer" = "sei un osservatore";
|
||||||
|
|
||||||
/* snd group event chat item */
|
|
||||||
"you blocked %@" = "hai bloccato %@";
|
|
||||||
|
|
||||||
/* No comment provided by engineer. */
|
/* No comment provided by engineer. */
|
||||||
"You can accept calls from lock screen, without device and app authentication." = "Puoi accettare chiamate dalla schermata di blocco, senza l'autenticazione del dispositivo e dell'app.";
|
"You can accept calls from lock screen, without device and app authentication." = "Puoi accettare chiamate dalla schermata di blocco, senza l'autenticazione del dispositivo e dell'app.";
|
||||||
|
|
||||||
@@ -4070,9 +4040,6 @@
|
|||||||
/* chat list item description */
|
/* chat list item description */
|
||||||
"you shared one-time link incognito" = "hai condiviso un link incognito una tantum";
|
"you shared one-time link incognito" = "hai condiviso un link incognito una tantum";
|
||||||
|
|
||||||
/* snd group event chat item */
|
|
||||||
"you unblocked %@" = "hai sbloccato %@";
|
|
||||||
|
|
||||||
/* No comment provided by engineer. */
|
/* No comment provided by engineer. */
|
||||||
"You will be connected to group when the group host's device is online, please wait or check later!" = "Verrai connesso/a al gruppo quando il dispositivo dell'host del gruppo sarà in linea, attendi o controlla più tardi!";
|
"You will be connected to group when the group host's device is online, please wait or check later!" = "Verrai connesso/a al gruppo quando il dispositivo dell'host del gruppo sarà in linea, attendi o controlla più tardi!";
|
||||||
|
|
||||||
|
|||||||
@@ -202,9 +202,6 @@
|
|||||||
/* No comment provided by engineer. */
|
/* No comment provided by engineer. */
|
||||||
"%lld messages blocked" = "%lld berichten geblokkeerd";
|
"%lld messages blocked" = "%lld berichten geblokkeerd";
|
||||||
|
|
||||||
/* No comment provided by engineer. */
|
|
||||||
"%lld messages blocked by admin" = "%lld berichten geblokkeerd door beheerder";
|
|
||||||
|
|
||||||
/* No comment provided by engineer. */
|
/* No comment provided by engineer. */
|
||||||
"%lld messages marked deleted" = "%lld berichten gemarkeerd als verwijderd";
|
"%lld messages marked deleted" = "%lld berichten gemarkeerd als verwijderd";
|
||||||
|
|
||||||
@@ -587,33 +584,18 @@
|
|||||||
/* No comment provided by engineer. */
|
/* No comment provided by engineer. */
|
||||||
"Block" = "Blokkeren";
|
"Block" = "Blokkeren";
|
||||||
|
|
||||||
/* No comment provided by engineer. */
|
|
||||||
"Block for all" = "Blokkeren voor iedereen";
|
|
||||||
|
|
||||||
/* No comment provided by engineer. */
|
/* No comment provided by engineer. */
|
||||||
"Block group members" = "Groepsleden blokkeren";
|
"Block group members" = "Groepsleden blokkeren";
|
||||||
|
|
||||||
/* No comment provided by engineer. */
|
/* No comment provided by engineer. */
|
||||||
"Block member" = "Lid blokkeren";
|
"Block member" = "Lid blokkeren";
|
||||||
|
|
||||||
/* No comment provided by engineer. */
|
|
||||||
"Block member for all?" = "Lid voor iedereen blokkeren?";
|
|
||||||
|
|
||||||
/* No comment provided by engineer. */
|
/* No comment provided by engineer. */
|
||||||
"Block member?" = "Lid blokkeren?";
|
"Block member?" = "Lid blokkeren?";
|
||||||
|
|
||||||
/* No comment provided by engineer. */
|
/* No comment provided by engineer. */
|
||||||
"blocked" = "geblokkeerd";
|
"blocked" = "geblokkeerd";
|
||||||
|
|
||||||
/* rcv group event chat item */
|
|
||||||
"blocked %@" = "geblokkeerd %@";
|
|
||||||
|
|
||||||
/* blocked chat item */
|
|
||||||
"blocked by admin" = "geblokkeerd door beheerder";
|
|
||||||
|
|
||||||
/* No comment provided by engineer. */
|
|
||||||
"Blocked by admin" = "Geblokkeerd door beheerder";
|
|
||||||
|
|
||||||
/* No comment provided by engineer. */
|
/* No comment provided by engineer. */
|
||||||
"bold" = "vetgedrukt";
|
"bold" = "vetgedrukt";
|
||||||
|
|
||||||
@@ -2155,7 +2137,7 @@
|
|||||||
"Join group" = "Word lid van groep";
|
"Join group" = "Word lid van groep";
|
||||||
|
|
||||||
/* No comment provided by engineer. */
|
/* No comment provided by engineer. */
|
||||||
"Join group conversations" = "Neem deel aan groepsgesprekken";
|
"Join group conversations" = "Neem deel aan groep gesprekken";
|
||||||
|
|
||||||
/* No comment provided by engineer. */
|
/* No comment provided by engineer. */
|
||||||
"Join group?" = "Deelnemen aan groep?";
|
"Join group?" = "Deelnemen aan groep?";
|
||||||
@@ -2649,7 +2631,7 @@
|
|||||||
"Paste image" = "Afbeelding plakken";
|
"Paste image" = "Afbeelding plakken";
|
||||||
|
|
||||||
/* No comment provided by engineer. */
|
/* No comment provided by engineer. */
|
||||||
"Paste link to connect!" = "Plak een link om te verbinden!";
|
"Paste link to connect!" = "Plak link om te verbinden!";
|
||||||
|
|
||||||
/* No comment provided by engineer. */
|
/* No comment provided by engineer. */
|
||||||
"Paste the link you received" = "Plak de link die je hebt ontvangen";
|
"Paste the link you received" = "Plak de link die je hebt ontvangen";
|
||||||
@@ -3069,7 +3051,7 @@
|
|||||||
"Search bar accepts invitation links." = "Zoekbalk accepteert uitnodigingslinks.";
|
"Search bar accepts invitation links." = "Zoekbalk accepteert uitnodigingslinks.";
|
||||||
|
|
||||||
/* No comment provided by engineer. */
|
/* No comment provided by engineer. */
|
||||||
"Search or paste SimpleX link" = "Zoek of plak een SimpleX link";
|
"Search or paste SimpleX link" = "Zoek of plak de SimpleX link";
|
||||||
|
|
||||||
/* network option */
|
/* network option */
|
||||||
"sec" = "sec";
|
"sec" = "sec";
|
||||||
@@ -3614,21 +3596,12 @@
|
|||||||
/* No comment provided by engineer. */
|
/* No comment provided by engineer. */
|
||||||
"Unblock" = "Deblokkeren";
|
"Unblock" = "Deblokkeren";
|
||||||
|
|
||||||
/* No comment provided by engineer. */
|
|
||||||
"Unblock for all" = "Deblokkeer voor iedereen";
|
|
||||||
|
|
||||||
/* No comment provided by engineer. */
|
/* No comment provided by engineer. */
|
||||||
"Unblock member" = "Lid deblokkeren";
|
"Unblock member" = "Lid deblokkeren";
|
||||||
|
|
||||||
/* No comment provided by engineer. */
|
|
||||||
"Unblock member for all?" = "Lid voor iedereen deblokkeren?";
|
|
||||||
|
|
||||||
/* No comment provided by engineer. */
|
/* No comment provided by engineer. */
|
||||||
"Unblock member?" = "Lid deblokkeren?";
|
"Unblock member?" = "Lid deblokkeren?";
|
||||||
|
|
||||||
/* rcv group event chat item */
|
|
||||||
"unblocked %@" = "gedeblokkeerd %@";
|
|
||||||
|
|
||||||
/* item status description */
|
/* item status description */
|
||||||
"Unexpected error: %@" = "Onverwachte fout: %@";
|
"Unexpected error: %@" = "Onverwachte fout: %@";
|
||||||
|
|
||||||
@@ -3956,9 +3929,6 @@
|
|||||||
/* No comment provided by engineer. */
|
/* No comment provided by engineer. */
|
||||||
"you are observer" = "jij bent waarnemer";
|
"you are observer" = "jij bent waarnemer";
|
||||||
|
|
||||||
/* snd group event chat item */
|
|
||||||
"you blocked %@" = "je hebt %@ geblokkeerd";
|
|
||||||
|
|
||||||
/* No comment provided by engineer. */
|
/* No comment provided by engineer. */
|
||||||
"You can accept calls from lock screen, without device and app authentication." = "U kunt oproepen van het vergrendelingsscherm accepteren, zonder apparaat- en app-verificatie.";
|
"You can accept calls from lock screen, without device and app authentication." = "U kunt oproepen van het vergrendelingsscherm accepteren, zonder apparaat- en app-verificatie.";
|
||||||
|
|
||||||
@@ -4070,9 +4040,6 @@
|
|||||||
/* chat list item description */
|
/* chat list item description */
|
||||||
"you shared one-time link incognito" = "je hebt een eenmalige link incognito gedeeld";
|
"you shared one-time link incognito" = "je hebt een eenmalige link incognito gedeeld";
|
||||||
|
|
||||||
/* snd group event chat item */
|
|
||||||
"you unblocked %@" = "je hebt %@ gedeblokkeerd";
|
|
||||||
|
|
||||||
/* No comment provided by engineer. */
|
/* No comment provided by engineer. */
|
||||||
"You will be connected to group when the group host's device is online, please wait or check later!" = "Je wordt verbonden met de groep wanneer het apparaat van de groep host online is, even geduld a.u.b. of controleer het later!";
|
"You will be connected to group when the group host's device is online, please wait or check later!" = "Je wordt verbonden met de groep wanneer het apparaat van de groep host online is, even geduld a.u.b. of controleer het later!";
|
||||||
|
|
||||||
|
|||||||
@@ -202,9 +202,6 @@
|
|||||||
/* No comment provided by engineer. */
|
/* No comment provided by engineer. */
|
||||||
"%lld messages blocked" = "%lld wiadomości zablokowanych";
|
"%lld messages blocked" = "%lld wiadomości zablokowanych";
|
||||||
|
|
||||||
/* No comment provided by engineer. */
|
|
||||||
"%lld messages blocked by admin" = "%lld wiadomości zablokowanych przez admina";
|
|
||||||
|
|
||||||
/* No comment provided by engineer. */
|
/* No comment provided by engineer. */
|
||||||
"%lld messages marked deleted" = "%lld wiadomości oznaczonych do usunięcia";
|
"%lld messages marked deleted" = "%lld wiadomości oznaczonych do usunięcia";
|
||||||
|
|
||||||
@@ -401,9 +398,6 @@
|
|||||||
/* No comment provided by engineer. */
|
/* No comment provided by engineer. */
|
||||||
"All group members will remain connected." = "Wszyscy członkowie grupy pozostaną połączeni.";
|
"All group members will remain connected." = "Wszyscy członkowie grupy pozostaną połączeni.";
|
||||||
|
|
||||||
/* No comment provided by engineer. */
|
|
||||||
"All messages will be deleted - this cannot be undone!" = "Wszystkie wiadomości zostaną usunięte – nie można tego cofnąć!";
|
|
||||||
|
|
||||||
/* No comment provided by engineer. */
|
/* No comment provided by engineer. */
|
||||||
"All messages will be deleted - this cannot be undone! The messages will be deleted ONLY for you." = "Wszystkie wiadomości zostaną usunięte - nie można tego cofnąć! Wiadomości zostaną usunięte TYLKO dla Ciebie.";
|
"All messages will be deleted - this cannot be undone! The messages will be deleted ONLY for you." = "Wszystkie wiadomości zostaną usunięte - nie można tego cofnąć! Wiadomości zostaną usunięte TYLKO dla Ciebie.";
|
||||||
|
|
||||||
@@ -587,33 +581,18 @@
|
|||||||
/* No comment provided by engineer. */
|
/* No comment provided by engineer. */
|
||||||
"Block" = "Zablokuj";
|
"Block" = "Zablokuj";
|
||||||
|
|
||||||
/* No comment provided by engineer. */
|
|
||||||
"Block for all" = "Zablokuj dla wszystkich";
|
|
||||||
|
|
||||||
/* No comment provided by engineer. */
|
/* No comment provided by engineer. */
|
||||||
"Block group members" = "Blokuj członków grupy";
|
"Block group members" = "Blokuj członków grupy";
|
||||||
|
|
||||||
/* No comment provided by engineer. */
|
/* No comment provided by engineer. */
|
||||||
"Block member" = "Zablokuj członka";
|
"Block member" = "Zablokuj członka";
|
||||||
|
|
||||||
/* No comment provided by engineer. */
|
|
||||||
"Block member for all?" = "Zablokować członka dla wszystkich?";
|
|
||||||
|
|
||||||
/* No comment provided by engineer. */
|
/* No comment provided by engineer. */
|
||||||
"Block member?" = "Zablokować członka?";
|
"Block member?" = "Zablokować członka?";
|
||||||
|
|
||||||
/* No comment provided by engineer. */
|
/* No comment provided by engineer. */
|
||||||
"blocked" = "zablokowany";
|
"blocked" = "zablokowany";
|
||||||
|
|
||||||
/* rcv group event chat item */
|
|
||||||
"blocked %@" = "zablokowany %@";
|
|
||||||
|
|
||||||
/* blocked chat item */
|
|
||||||
"blocked by admin" = "zablokowany przez admina";
|
|
||||||
|
|
||||||
/* No comment provided by engineer. */
|
|
||||||
"Blocked by admin" = "Zablokowany przez admina";
|
|
||||||
|
|
||||||
/* No comment provided by engineer. */
|
/* No comment provided by engineer. */
|
||||||
"bold" = "pogrubiona";
|
"bold" = "pogrubiona";
|
||||||
|
|
||||||
@@ -771,9 +750,6 @@
|
|||||||
/* No comment provided by engineer. */
|
/* No comment provided by engineer. */
|
||||||
"Clear conversation?" = "Wyczyścić rozmowę?";
|
"Clear conversation?" = "Wyczyścić rozmowę?";
|
||||||
|
|
||||||
/* No comment provided by engineer. */
|
|
||||||
"Clear private notes?" = "Wyczyścić prywatne notatki?";
|
|
||||||
|
|
||||||
/* No comment provided by engineer. */
|
/* No comment provided by engineer. */
|
||||||
"Clear verification" = "Wyczyść weryfikację";
|
"Clear verification" = "Wyczyść weryfikację";
|
||||||
|
|
||||||
@@ -912,9 +888,6 @@
|
|||||||
/* connection information */
|
/* connection information */
|
||||||
"connection:%@" = "połączenie: %@";
|
"connection:%@" = "połączenie: %@";
|
||||||
|
|
||||||
/* profile update event chat item */
|
|
||||||
"contact %@ changed to %@" = "kontakt %1$@ zmieniony na %2$@";
|
|
||||||
|
|
||||||
/* No comment provided by engineer. */
|
/* No comment provided by engineer. */
|
||||||
"Contact allows" = "Kontakt pozwala";
|
"Contact allows" = "Kontakt pozwala";
|
||||||
|
|
||||||
@@ -999,12 +972,6 @@
|
|||||||
/* No comment provided by engineer. */
|
/* No comment provided by engineer. */
|
||||||
"Create your profile" = "Utwórz swój profil";
|
"Create your profile" = "Utwórz swój profil";
|
||||||
|
|
||||||
/* No comment provided by engineer. */
|
|
||||||
"Created at" = "Utworzony o";
|
|
||||||
|
|
||||||
/* copied message info */
|
|
||||||
"Created at: %@" = "Utworzony o: %@";
|
|
||||||
|
|
||||||
/* No comment provided by engineer. */
|
/* No comment provided by engineer. */
|
||||||
"Created on %@" = "Utworzony w dniu %@";
|
"Created on %@" = "Utworzony w dniu %@";
|
||||||
|
|
||||||
@@ -1329,9 +1296,6 @@
|
|||||||
/* No comment provided by engineer. */
|
/* No comment provided by engineer. */
|
||||||
"Do it later" = "Zrób to później";
|
"Do it later" = "Zrób to później";
|
||||||
|
|
||||||
/* No comment provided by engineer. */
|
|
||||||
"Do not send history to new members." = "Nie wysyłaj historii do nowych członków.";
|
|
||||||
|
|
||||||
/* No comment provided by engineer. */
|
/* No comment provided by engineer. */
|
||||||
"Do NOT use SimpleX for emergency calls." = "NIE używaj SimpleX do połączeń alarmowych.";
|
"Do NOT use SimpleX for emergency calls." = "NIE używaj SimpleX do połączeń alarmowych.";
|
||||||
|
|
||||||
@@ -1557,9 +1521,6 @@
|
|||||||
/* No comment provided by engineer. */
|
/* No comment provided by engineer. */
|
||||||
"Error creating member contact" = "Błąd tworzenia kontaktu członka";
|
"Error creating member contact" = "Błąd tworzenia kontaktu członka";
|
||||||
|
|
||||||
/* No comment provided by engineer. */
|
|
||||||
"Error creating message" = "Błąd tworzenia wiadomości";
|
|
||||||
|
|
||||||
/* No comment provided by engineer. */
|
/* No comment provided by engineer. */
|
||||||
"Error creating profile!" = "Błąd tworzenia profilu!";
|
"Error creating profile!" = "Błąd tworzenia profilu!";
|
||||||
|
|
||||||
@@ -1914,9 +1875,6 @@
|
|||||||
/* No comment provided by engineer. */
|
/* No comment provided by engineer. */
|
||||||
"History" = "Historia";
|
"History" = "Historia";
|
||||||
|
|
||||||
/* No comment provided by engineer. */
|
|
||||||
"History is not sent to new members." = "Historia nie jest wysyłana do nowych członków.";
|
|
||||||
|
|
||||||
/* time unit */
|
/* time unit */
|
||||||
"hours" = "godziny";
|
"hours" = "godziny";
|
||||||
|
|
||||||
@@ -1974,9 +1932,6 @@
|
|||||||
/* No comment provided by engineer. */
|
/* No comment provided by engineer. */
|
||||||
"Import database" = "Importuj bazę danych";
|
"Import database" = "Importuj bazę danych";
|
||||||
|
|
||||||
/* No comment provided by engineer. */
|
|
||||||
"Improved message delivery" = "Ulepszona dostawa wiadomości";
|
|
||||||
|
|
||||||
/* No comment provided by engineer. */
|
/* No comment provided by engineer. */
|
||||||
"Improved privacy and security" = "Zwiększona prywatność i bezpieczeństwo";
|
"Improved privacy and security" = "Zwiększona prywatność i bezpieczeństwo";
|
||||||
|
|
||||||
@@ -2061,9 +2016,6 @@
|
|||||||
/* invalid chat item */
|
/* invalid chat item */
|
||||||
"invalid data" = "nieprawidłowe dane";
|
"invalid data" = "nieprawidłowe dane";
|
||||||
|
|
||||||
/* No comment provided by engineer. */
|
|
||||||
"Invalid display name!" = "Nieprawidłowa nazwa wyświetlana!";
|
|
||||||
|
|
||||||
/* No comment provided by engineer. */
|
/* No comment provided by engineer. */
|
||||||
"Invalid link" = "Nieprawidłowy link";
|
"Invalid link" = "Nieprawidłowy link";
|
||||||
|
|
||||||
@@ -2154,9 +2106,6 @@
|
|||||||
/* No comment provided by engineer. */
|
/* No comment provided by engineer. */
|
||||||
"Join group" = "Dołącz do grupy";
|
"Join group" = "Dołącz do grupy";
|
||||||
|
|
||||||
/* No comment provided by engineer. */
|
|
||||||
"Join group conversations" = "Dołącz do grupowej rozmowy";
|
|
||||||
|
|
||||||
/* No comment provided by engineer. */
|
/* No comment provided by engineer. */
|
||||||
"Join group?" = "Dołączyć do grupy?";
|
"Join group?" = "Dołączyć do grupy?";
|
||||||
|
|
||||||
@@ -2292,9 +2241,6 @@
|
|||||||
/* No comment provided by engineer. */
|
/* No comment provided by engineer. */
|
||||||
"Member" = "Członek";
|
"Member" = "Członek";
|
||||||
|
|
||||||
/* profile update event chat item */
|
|
||||||
"member %@ changed to %@" = "członek %1$@ zmieniony na %2$@";
|
|
||||||
|
|
||||||
/* rcv group event chat item */
|
/* rcv group event chat item */
|
||||||
"member connected" = "połączony";
|
"member connected" = "połączony";
|
||||||
|
|
||||||
@@ -2639,18 +2585,12 @@
|
|||||||
/* No comment provided by engineer. */
|
/* No comment provided by engineer. */
|
||||||
"Password to show" = "Hasło do wyświetlenia";
|
"Password to show" = "Hasło do wyświetlenia";
|
||||||
|
|
||||||
/* past/unknown group member */
|
|
||||||
"Past member %@" = "Były członek %@";
|
|
||||||
|
|
||||||
/* No comment provided by engineer. */
|
/* No comment provided by engineer. */
|
||||||
"Paste desktop address" = "Wklej adres komputera";
|
"Paste desktop address" = "Wklej adres komputera";
|
||||||
|
|
||||||
/* No comment provided by engineer. */
|
/* No comment provided by engineer. */
|
||||||
"Paste image" = "Wklej obraz";
|
"Paste image" = "Wklej obraz";
|
||||||
|
|
||||||
/* No comment provided by engineer. */
|
|
||||||
"Paste link to connect!" = "Wklej link, aby połączyć!";
|
|
||||||
|
|
||||||
/* No comment provided by engineer. */
|
/* No comment provided by engineer. */
|
||||||
"Paste the link you received" = "Wklej link, który otrzymałeś";
|
"Paste the link you received" = "Wklej link, który otrzymałeś";
|
||||||
|
|
||||||
@@ -2738,9 +2678,6 @@
|
|||||||
/* No comment provided by engineer. */
|
/* No comment provided by engineer. */
|
||||||
"Private filenames" = "Prywatne nazwy plików";
|
"Private filenames" = "Prywatne nazwy plików";
|
||||||
|
|
||||||
/* name of notes to self */
|
|
||||||
"Private notes" = "Prywatne notatki";
|
|
||||||
|
|
||||||
/* No comment provided by engineer. */
|
/* No comment provided by engineer. */
|
||||||
"Profile and server connections" = "Profil i połączenia z serwerem";
|
"Profile and server connections" = "Profil i połączenia z serwerem";
|
||||||
|
|
||||||
@@ -2855,9 +2792,6 @@
|
|||||||
/* No comment provided by engineer. */
|
/* No comment provided by engineer. */
|
||||||
"Receiving via" = "Odbieranie przez";
|
"Receiving via" = "Odbieranie przez";
|
||||||
|
|
||||||
/* No comment provided by engineer. */
|
|
||||||
"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)." = "Ostania historia i ulepszony [bot adresowy](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).";
|
|
||||||
|
|
||||||
/* No comment provided by engineer. */
|
/* No comment provided by engineer. */
|
||||||
"Recipients see updates as you type them." = "Odbiorcy widzą aktualizacje podczas ich wpisywania.";
|
"Recipients see updates as you type them." = "Odbiorcy widzą aktualizacje podczas ich wpisywania.";
|
||||||
|
|
||||||
@@ -2912,12 +2846,6 @@
|
|||||||
/* rcv group event chat item */
|
/* rcv group event chat item */
|
||||||
"removed %@" = "usunięto %@";
|
"removed %@" = "usunięto %@";
|
||||||
|
|
||||||
/* profile update event chat item */
|
|
||||||
"removed contact address" = "usunięto adres kontaktu";
|
|
||||||
|
|
||||||
/* profile update event chat item */
|
|
||||||
"removed profile picture" = "usunięto zdjęcie profilu";
|
|
||||||
|
|
||||||
/* rcv group event chat item */
|
/* rcv group event chat item */
|
||||||
"removed you" = "usunął cię";
|
"removed you" = "usunął cię";
|
||||||
|
|
||||||
@@ -3041,9 +2969,6 @@
|
|||||||
/* No comment provided by engineer. */
|
/* No comment provided by engineer. */
|
||||||
"Save welcome message?" = "Zapisać wiadomość powitalną?";
|
"Save welcome message?" = "Zapisać wiadomość powitalną?";
|
||||||
|
|
||||||
/* message info title */
|
|
||||||
"Saved message" = "Zachowano wiadomość";
|
|
||||||
|
|
||||||
/* No comment provided by engineer. */
|
/* No comment provided by engineer. */
|
||||||
"Saved WebRTC ICE servers will be removed" = "Zapisane serwery WebRTC ICE zostaną usunięte";
|
"Saved WebRTC ICE servers will be removed" = "Zapisane serwery WebRTC ICE zostaną usunięte";
|
||||||
|
|
||||||
@@ -3065,9 +2990,6 @@
|
|||||||
/* No comment provided by engineer. */
|
/* No comment provided by engineer. */
|
||||||
"Search" = "Szukaj";
|
"Search" = "Szukaj";
|
||||||
|
|
||||||
/* No comment provided by engineer. */
|
|
||||||
"Search bar accepts invitation links." = "Pasek wyszukiwania akceptuje linki zaproszenia.";
|
|
||||||
|
|
||||||
/* No comment provided by engineer. */
|
/* No comment provided by engineer. */
|
||||||
"Search or paste SimpleX link" = "Wyszukaj lub wklej link SimpleX";
|
"Search or paste SimpleX link" = "Wyszukaj lub wklej link SimpleX";
|
||||||
|
|
||||||
@@ -3149,9 +3071,6 @@
|
|||||||
/* No comment provided by engineer. */
|
/* No comment provided by engineer. */
|
||||||
"Send them from gallery or custom keyboards." = "Wyślij je z galerii lub niestandardowych klawiatur.";
|
"Send them from gallery or custom keyboards." = "Wyślij je z galerii lub niestandardowych klawiatur.";
|
||||||
|
|
||||||
/* No comment provided by engineer. */
|
|
||||||
"Send up to 100 last messages to new members." = "Wysyłaj do 100 ostatnich wiadomości do nowych członków.";
|
|
||||||
|
|
||||||
/* No comment provided by engineer. */
|
/* No comment provided by engineer. */
|
||||||
"Sender cancelled file transfer." = "Nadawca anulował transfer pliku.";
|
"Sender cancelled file transfer." = "Nadawca anulował transfer pliku.";
|
||||||
|
|
||||||
@@ -3224,12 +3143,6 @@
|
|||||||
/* No comment provided by engineer. */
|
/* No comment provided by engineer. */
|
||||||
"Set it instead of system authentication." = "Ustaw go zamiast uwierzytelniania systemowego.";
|
"Set it instead of system authentication." = "Ustaw go zamiast uwierzytelniania systemowego.";
|
||||||
|
|
||||||
/* profile update event chat item */
|
|
||||||
"set new contact address" = "ustaw nowy adres kontaktu";
|
|
||||||
|
|
||||||
/* profile update event chat item */
|
|
||||||
"set new profile picture" = "ustaw nowe zdjęcie profilu";
|
|
||||||
|
|
||||||
/* No comment provided by engineer. */
|
/* No comment provided by engineer. */
|
||||||
"Set passcode" = "Ustaw pin";
|
"Set passcode" = "Ustaw pin";
|
||||||
|
|
||||||
@@ -3536,9 +3449,6 @@
|
|||||||
/* No comment provided by engineer. */
|
/* No comment provided by engineer. */
|
||||||
"This device name" = "Nazwa tego urządzenia";
|
"This device name" = "Nazwa tego urządzenia";
|
||||||
|
|
||||||
/* No comment provided by engineer. */
|
|
||||||
"This display name is invalid. Please choose another name." = "Nazwa wyświetlana jest nieprawidłowa. Proszę wybrać inną nazwę.";
|
|
||||||
|
|
||||||
/* No comment provided by engineer. */
|
/* No comment provided by engineer. */
|
||||||
"This group has over %lld members, delivery receipts are not sent." = "Ta grupa ma ponad %lld członków, potwierdzenia dostawy nie są wysyłane.";
|
"This group has over %lld members, delivery receipts are not sent." = "Ta grupa ma ponad %lld członków, potwierdzenia dostawy nie są wysyłane.";
|
||||||
|
|
||||||
@@ -3599,9 +3509,6 @@
|
|||||||
/* No comment provided by engineer. */
|
/* No comment provided by engineer. */
|
||||||
"Trying to connect to the server used to receive messages from this contact." = "Próbowanie połączenia z serwerem używanym do odbierania wiadomości od tego kontaktu.";
|
"Trying to connect to the server used to receive messages from this contact." = "Próbowanie połączenia z serwerem używanym do odbierania wiadomości od tego kontaktu.";
|
||||||
|
|
||||||
/* No comment provided by engineer. */
|
|
||||||
"Turkish interface" = "Turecki interfejs";
|
|
||||||
|
|
||||||
/* No comment provided by engineer. */
|
/* No comment provided by engineer. */
|
||||||
"Turn off" = "Wyłącz";
|
"Turn off" = "Wyłącz";
|
||||||
|
|
||||||
@@ -3614,21 +3521,12 @@
|
|||||||
/* No comment provided by engineer. */
|
/* No comment provided by engineer. */
|
||||||
"Unblock" = "Odblokuj";
|
"Unblock" = "Odblokuj";
|
||||||
|
|
||||||
/* No comment provided by engineer. */
|
|
||||||
"Unblock for all" = "Odblokuj dla wszystkich";
|
|
||||||
|
|
||||||
/* No comment provided by engineer. */
|
/* No comment provided by engineer. */
|
||||||
"Unblock member" = "Odblokuj członka";
|
"Unblock member" = "Odblokuj członka";
|
||||||
|
|
||||||
/* No comment provided by engineer. */
|
|
||||||
"Unblock member for all?" = "Odblokować członka dla wszystkich?";
|
|
||||||
|
|
||||||
/* No comment provided by engineer. */
|
/* No comment provided by engineer. */
|
||||||
"Unblock member?" = "Odblokować członka?";
|
"Unblock member?" = "Odblokować członka?";
|
||||||
|
|
||||||
/* rcv group event chat item */
|
|
||||||
"unblocked %@" = "odblokowano %@";
|
|
||||||
|
|
||||||
/* item status description */
|
/* item status description */
|
||||||
"Unexpected error: %@" = "Nieoczekiwany błąd: %@";
|
"Unexpected error: %@" = "Nieoczekiwany błąd: %@";
|
||||||
|
|
||||||
@@ -3662,9 +3560,6 @@
|
|||||||
/* No comment provided by engineer. */
|
/* No comment provided by engineer. */
|
||||||
"Unknown error" = "Nieznany błąd";
|
"Unknown error" = "Nieznany błąd";
|
||||||
|
|
||||||
/* No comment provided by engineer. */
|
|
||||||
"unknown status" = "nieznany status";
|
|
||||||
|
|
||||||
/* No comment provided by engineer. */
|
/* No comment provided by engineer. */
|
||||||
"Unless you use iOS call interface, enable Do Not Disturb mode to avoid interruptions." = "O ile nie korzystasz z interfejsu połączeń systemu iOS, włącz tryb Nie przeszkadzać, aby uniknąć przerywania.";
|
"Unless you use iOS call interface, enable Do Not Disturb mode to avoid interruptions." = "O ile nie korzystasz z interfejsu połączeń systemu iOS, włącz tryb Nie przeszkadzać, aby uniknąć przerywania.";
|
||||||
|
|
||||||
@@ -3689,9 +3584,6 @@
|
|||||||
/* No comment provided by engineer. */
|
/* No comment provided by engineer. */
|
||||||
"Unread" = "Nieprzeczytane";
|
"Unread" = "Nieprzeczytane";
|
||||||
|
|
||||||
/* No comment provided by engineer. */
|
|
||||||
"Up to 100 last messages are sent to new members." = "Do nowych członków wysyłanych jest do 100 ostatnich wiadomości.";
|
|
||||||
|
|
||||||
/* No comment provided by engineer. */
|
/* No comment provided by engineer. */
|
||||||
"Update" = "Aktualizuj";
|
"Update" = "Aktualizuj";
|
||||||
|
|
||||||
@@ -3710,9 +3602,6 @@
|
|||||||
/* rcv group event chat item */
|
/* rcv group event chat item */
|
||||||
"updated group profile" = "zaktualizowano profil grupy";
|
"updated group profile" = "zaktualizowano profil grupy";
|
||||||
|
|
||||||
/* profile update event chat item */
|
|
||||||
"updated profile" = "zaktualizowano profil";
|
|
||||||
|
|
||||||
/* No comment provided by engineer. */
|
/* No comment provided by engineer. */
|
||||||
"Updating settings will re-connect the client to all servers." = "Aktualizacja ustawień spowoduje ponowne połączenie klienta ze wszystkimi serwerami.";
|
"Updating settings will re-connect the client to all servers." = "Aktualizacja ustawień spowoduje ponowne połączenie klienta ze wszystkimi serwerami.";
|
||||||
|
|
||||||
@@ -3821,9 +3710,6 @@
|
|||||||
/* No comment provided by engineer. */
|
/* No comment provided by engineer. */
|
||||||
"View security code" = "Pokaż kod bezpieczeństwa";
|
"View security code" = "Pokaż kod bezpieczeństwa";
|
||||||
|
|
||||||
/* chat feature */
|
|
||||||
"Visible history" = "Widoczna historia";
|
|
||||||
|
|
||||||
/* No comment provided by engineer. */
|
/* No comment provided by engineer. */
|
||||||
"Voice message…" = "Wiadomość głosowa…";
|
"Voice message…" = "Wiadomość głosowa…";
|
||||||
|
|
||||||
@@ -3887,15 +3773,9 @@
|
|||||||
/* No comment provided by engineer. */
|
/* No comment provided by engineer. */
|
||||||
"When you share an incognito profile with somebody, this profile will be used for the groups they invite you to." = "Gdy udostępnisz komuś profil incognito, będzie on używany w grupach, do których Cię zaprosi.";
|
"When you share an incognito profile with somebody, this profile will be used for the groups they invite you to." = "Gdy udostępnisz komuś profil incognito, będzie on używany w grupach, do których Cię zaprosi.";
|
||||||
|
|
||||||
/* No comment provided by engineer. */
|
|
||||||
"With encrypted files and media." = "Z zaszyfrowanymi plikami i multimediami.";
|
|
||||||
|
|
||||||
/* No comment provided by engineer. */
|
/* No comment provided by engineer. */
|
||||||
"With optional welcome message." = "Z opcjonalną wiadomością powitalną.";
|
"With optional welcome message." = "Z opcjonalną wiadomością powitalną.";
|
||||||
|
|
||||||
/* No comment provided by engineer. */
|
|
||||||
"With reduced battery usage." = "Ze zmniejszonym zużyciem baterii.";
|
|
||||||
|
|
||||||
/* No comment provided by engineer. */
|
/* No comment provided by engineer. */
|
||||||
"Wrong database passphrase" = "Nieprawidłowe hasło bazy danych";
|
"Wrong database passphrase" = "Nieprawidłowe hasło bazy danych";
|
||||||
|
|
||||||
@@ -3956,9 +3836,6 @@
|
|||||||
/* No comment provided by engineer. */
|
/* No comment provided by engineer. */
|
||||||
"you are observer" = "jesteś obserwatorem";
|
"you are observer" = "jesteś obserwatorem";
|
||||||
|
|
||||||
/* snd group event chat item */
|
|
||||||
"you blocked %@" = "zablokowałeś %@";
|
|
||||||
|
|
||||||
/* No comment provided by engineer. */
|
/* No comment provided by engineer. */
|
||||||
"You can accept calls from lock screen, without device and app authentication." = "Możesz przyjmować połączenia z ekranu blokady, bez uwierzytelniania urządzenia i aplikacji.";
|
"You can accept calls from lock screen, without device and app authentication." = "Możesz przyjmować połączenia z ekranu blokady, bez uwierzytelniania urządzenia i aplikacji.";
|
||||||
|
|
||||||
@@ -4070,9 +3947,6 @@
|
|||||||
/* chat list item description */
|
/* chat list item description */
|
||||||
"you shared one-time link incognito" = "udostępniłeś jednorazowy link incognito";
|
"you shared one-time link incognito" = "udostępniłeś jednorazowy link incognito";
|
||||||
|
|
||||||
/* snd group event chat item */
|
|
||||||
"you unblocked %@" = "odblokowałeś %@";
|
|
||||||
|
|
||||||
/* No comment provided by engineer. */
|
/* No comment provided by engineer. */
|
||||||
"You will be connected to group when the group host's device is online, please wait or check later!" = "Zostaniesz połączony do grupy, gdy urządzenie gospodarza grupy będzie online, proszę czekać lub sprawdzić później!";
|
"You will be connected to group when the group host's device is online, please wait or check later!" = "Zostaniesz połączony do grupy, gdy urządzenie gospodarza grupy będzie online, proszę czekać lub sprawdzić później!";
|
||||||
|
|
||||||
|
|||||||
@@ -202,9 +202,6 @@
|
|||||||
/* No comment provided by engineer. */
|
/* No comment provided by engineer. */
|
||||||
"%lld messages blocked" = "%lld сообщений заблокировано";
|
"%lld messages blocked" = "%lld сообщений заблокировано";
|
||||||
|
|
||||||
/* No comment provided by engineer. */
|
|
||||||
"%lld messages blocked by admin" = "%lld сообщений заблокировано администратором";
|
|
||||||
|
|
||||||
/* No comment provided by engineer. */
|
/* No comment provided by engineer. */
|
||||||
"%lld messages marked deleted" = "%lld сообщений помечено удалёнными";
|
"%lld messages marked deleted" = "%lld сообщений помечено удалёнными";
|
||||||
|
|
||||||
@@ -587,33 +584,18 @@
|
|||||||
/* No comment provided by engineer. */
|
/* No comment provided by engineer. */
|
||||||
"Block" = "Заблокировать";
|
"Block" = "Заблокировать";
|
||||||
|
|
||||||
/* No comment provided by engineer. */
|
|
||||||
"Block for all" = "Заблокировать для всех";
|
|
||||||
|
|
||||||
/* No comment provided by engineer. */
|
/* No comment provided by engineer. */
|
||||||
"Block group members" = "Блокируйте членов группы";
|
"Block group members" = "Блокируйте членов группы";
|
||||||
|
|
||||||
/* No comment provided by engineer. */
|
/* No comment provided by engineer. */
|
||||||
"Block member" = "Заблокировать члена группы";
|
"Block member" = "Заблокировать члена группы";
|
||||||
|
|
||||||
/* No comment provided by engineer. */
|
|
||||||
"Block member for all?" = "Заблокировать члена для всех?";
|
|
||||||
|
|
||||||
/* No comment provided by engineer. */
|
/* No comment provided by engineer. */
|
||||||
"Block member?" = "Заблокировать члена группы?";
|
"Block member?" = "Заблокировать члена группы?";
|
||||||
|
|
||||||
/* No comment provided by engineer. */
|
/* No comment provided by engineer. */
|
||||||
"blocked" = "заблокировано";
|
"blocked" = "заблокировано";
|
||||||
|
|
||||||
/* rcv group event chat item */
|
|
||||||
"blocked %@" = "%@ заблокирован";
|
|
||||||
|
|
||||||
/* blocked chat item */
|
|
||||||
"blocked by admin" = "заблокировано администратором";
|
|
||||||
|
|
||||||
/* No comment provided by engineer. */
|
|
||||||
"Blocked by admin" = "Заблокирован администратором";
|
|
||||||
|
|
||||||
/* No comment provided by engineer. */
|
/* No comment provided by engineer. */
|
||||||
"bold" = "жирный";
|
"bold" = "жирный";
|
||||||
|
|
||||||
@@ -3614,21 +3596,12 @@
|
|||||||
/* No comment provided by engineer. */
|
/* No comment provided by engineer. */
|
||||||
"Unblock" = "Разблокировать";
|
"Unblock" = "Разблокировать";
|
||||||
|
|
||||||
/* No comment provided by engineer. */
|
|
||||||
"Unblock for all" = "Разблокировать для всех";
|
|
||||||
|
|
||||||
/* No comment provided by engineer. */
|
/* No comment provided by engineer. */
|
||||||
"Unblock member" = "Разблокировать члена группы";
|
"Unblock member" = "Разблокировать члена группы";
|
||||||
|
|
||||||
/* No comment provided by engineer. */
|
|
||||||
"Unblock member for all?" = "Разблокировать члена для всех?";
|
|
||||||
|
|
||||||
/* No comment provided by engineer. */
|
/* No comment provided by engineer. */
|
||||||
"Unblock member?" = "Разблокировать члена группы?";
|
"Unblock member?" = "Разблокировать члена группы?";
|
||||||
|
|
||||||
/* rcv group event chat item */
|
|
||||||
"unblocked %@" = "%@ разблокирован";
|
|
||||||
|
|
||||||
/* item status description */
|
/* item status description */
|
||||||
"Unexpected error: %@" = "Неожиданная ошибка: %@";
|
"Unexpected error: %@" = "Неожиданная ошибка: %@";
|
||||||
|
|
||||||
@@ -3956,9 +3929,6 @@
|
|||||||
/* No comment provided by engineer. */
|
/* No comment provided by engineer. */
|
||||||
"you are observer" = "только чтение сообщений";
|
"you are observer" = "только чтение сообщений";
|
||||||
|
|
||||||
/* snd group event chat item */
|
|
||||||
"you blocked %@" = "Вы заблокировали %@";
|
|
||||||
|
|
||||||
/* No comment provided by engineer. */
|
/* No comment provided by engineer. */
|
||||||
"You can accept calls from lock screen, without device and app authentication." = "Вы можете принимать звонки на экране блокировки, без аутентификации.";
|
"You can accept calls from lock screen, without device and app authentication." = "Вы можете принимать звонки на экране блокировки, без аутентификации.";
|
||||||
|
|
||||||
@@ -4070,9 +4040,6 @@
|
|||||||
/* chat list item description */
|
/* chat list item description */
|
||||||
"you shared one-time link incognito" = "Вы создали ссылку инкогнито";
|
"you shared one-time link incognito" = "Вы создали ссылку инкогнито";
|
||||||
|
|
||||||
/* snd group event chat item */
|
|
||||||
"you unblocked %@" = "Вы разблокировали %@";
|
|
||||||
|
|
||||||
/* No comment provided by engineer. */
|
/* No comment provided by engineer. */
|
||||||
"You will be connected to group when the group host's device is online, please wait or check later!" = "Соединение с группой будет установлено, когда хост группы будет онлайн. Пожалуйста, подождите или проверьте позже!";
|
"You will be connected to group when the group host's device is online, please wait or check later!" = "Соединение с группой будет установлено, когда хост группы будет онлайн. Пожалуйста, подождите или проверьте позже!";
|
||||||
|
|
||||||
|
|||||||
@@ -2,13 +2,13 @@
|
|||||||
"\n" = "\n";
|
"\n" = "\n";
|
||||||
|
|
||||||
/* No comment provided by engineer. */
|
/* No comment provided by engineer. */
|
||||||
" " = " ";
|
" " = " . ";
|
||||||
|
|
||||||
/* No comment provided by engineer. */
|
/* No comment provided by engineer. */
|
||||||
" " = " ";
|
" " = " ";
|
||||||
|
|
||||||
/* No comment provided by engineer. */
|
/* No comment provided by engineer. */
|
||||||
" " = " ";
|
" " = " . ";
|
||||||
|
|
||||||
/* No comment provided by engineer. */
|
/* No comment provided by engineer. */
|
||||||
" (" = " (";
|
" (" = " (";
|
||||||
|
|||||||
@@ -103,14 +103,11 @@
|
|||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity-alias>
|
</activity-alias>
|
||||||
|
|
||||||
<activity android:name=".views.call.CallActivity"
|
|
||||||
|
<activity android:name=".views.call.IncomingCallActivity"
|
||||||
android:showOnLockScreen="true"
|
android:showOnLockScreen="true"
|
||||||
android:exported="false"
|
android:exported="false"
|
||||||
android:launchMode="singleInstance"
|
android:launchMode="singleTask"/>
|
||||||
android:supportsPictureInPicture="true"
|
|
||||||
android:autoRemoveFromRecents="true"
|
|
||||||
android:screenOrientation="portrait"
|
|
||||||
android:configChanges="screenSize|smallestScreenSize|screenLayout|orientation"/>
|
|
||||||
|
|
||||||
<provider
|
<provider
|
||||||
android:name="androidx.core.content.FileProvider"
|
android:name="androidx.core.content.FileProvider"
|
||||||
@@ -136,18 +133,6 @@
|
|||||||
android:stopWithTask="false"></service>
|
android:stopWithTask="false"></service>
|
||||||
|
|
||||||
<!-- SimplexService restart on reboot -->
|
<!-- SimplexService restart on reboot -->
|
||||||
|
|
||||||
<service
|
|
||||||
android:name=".CallService"
|
|
||||||
android:enabled="true"
|
|
||||||
android:exported="false"
|
|
||||||
android:stopWithTask="false"/>
|
|
||||||
|
|
||||||
<receiver
|
|
||||||
android:name=".CallService$CallActionReceiver"
|
|
||||||
android:enabled="true"
|
|
||||||
android:exported="false" />
|
|
||||||
|
|
||||||
<receiver
|
<receiver
|
||||||
android:name=".SimplexService$StartReceiver"
|
android:name=".SimplexService$StartReceiver"
|
||||||
android:enabled="true"
|
android:enabled="true"
|
||||||
|
|||||||
@@ -1,176 +0,0 @@
|
|||||||
package chat.simplex.app
|
|
||||||
|
|
||||||
import android.app.*
|
|
||||||
import android.content.*
|
|
||||||
import android.graphics.Bitmap
|
|
||||||
import android.graphics.BitmapFactory
|
|
||||||
import android.os.*
|
|
||||||
import androidx.compose.ui.graphics.asAndroidBitmap
|
|
||||||
import androidx.core.app.NotificationCompat
|
|
||||||
import androidx.core.content.ContextCompat
|
|
||||||
import chat.simplex.app.model.NtfManager.EndCallAction
|
|
||||||
import chat.simplex.app.views.call.CallActivity
|
|
||||||
import chat.simplex.common.model.NotificationPreviewMode
|
|
||||||
import chat.simplex.common.platform.*
|
|
||||||
import chat.simplex.common.views.call.CallState
|
|
||||||
import chat.simplex.common.views.helpers.*
|
|
||||||
import chat.simplex.res.MR
|
|
||||||
import kotlinx.datetime.Instant
|
|
||||||
|
|
||||||
class CallService: Service() {
|
|
||||||
private var wakeLock: PowerManager.WakeLock? = null
|
|
||||||
private var notificationManager: NotificationManager? = null
|
|
||||||
private var serviceNotification: Notification? = null
|
|
||||||
|
|
||||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
|
||||||
Log.d(TAG, "onStartCommand startId: $startId")
|
|
||||||
if (intent != null) {
|
|
||||||
val action = intent.action
|
|
||||||
Log.d(TAG, "intent action $action")
|
|
||||||
when (action) {
|
|
||||||
Action.START.name -> startService()
|
|
||||||
else -> Log.e(TAG, "No action in the intent")
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Log.d(TAG, "null intent. Probably restarted by the system.")
|
|
||||||
}
|
|
||||||
startForeground(CALL_SERVICE_ID, serviceNotification)
|
|
||||||
return START_STICKY
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreate() {
|
|
||||||
super.onCreate()
|
|
||||||
Log.d(TAG, "Call service created")
|
|
||||||
notificationManager = createNotificationChannel()
|
|
||||||
updateNotification()
|
|
||||||
startForeground(CALL_SERVICE_ID, serviceNotification)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDestroy() {
|
|
||||||
Log.d(TAG, "Call service destroyed")
|
|
||||||
try {
|
|
||||||
wakeLock?.let {
|
|
||||||
while (it.isHeld) it.release() // release all, in case acquired more than once
|
|
||||||
}
|
|
||||||
wakeLock = null
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.d(TAG, "Exception while releasing wakelock: ${e.message}")
|
|
||||||
}
|
|
||||||
super.onDestroy()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun startService() {
|
|
||||||
Log.d(TAG, "CallService startService")
|
|
||||||
if (wakeLock != null) return
|
|
||||||
wakeLock = (getSystemService(Context.POWER_SERVICE) as PowerManager).run {
|
|
||||||
newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, WAKE_LOCK_TAG).apply {
|
|
||||||
acquire()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun updateNotification() {
|
|
||||||
val call = chatModel.activeCall.value
|
|
||||||
val previewMode = appPreferences.notificationPreviewMode.get()
|
|
||||||
val title = if (previewMode == NotificationPreviewMode.HIDDEN.name)
|
|
||||||
generalGetString(MR.strings.notification_preview_somebody)
|
|
||||||
else
|
|
||||||
call?.contact?.profile?.displayName ?: ""
|
|
||||||
val text = generalGetString(if (call?.supportsVideo() == true) MR.strings.call_service_notification_video_call else MR.strings.call_service_notification_audio_call)
|
|
||||||
val image = call?.contact?.image
|
|
||||||
val largeIcon = if (image == null || previewMode == NotificationPreviewMode.HIDDEN.name)
|
|
||||||
BitmapFactory.decodeResource(resources, R.drawable.icon)
|
|
||||||
else
|
|
||||||
base64ToBitmap(image).asAndroidBitmap()
|
|
||||||
|
|
||||||
serviceNotification = createNotification(title, text, largeIcon, call?.connectedAt)
|
|
||||||
startForeground(CALL_SERVICE_ID, serviceNotification)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun createNotificationChannel(): NotificationManager? {
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
||||||
val notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
|
||||||
val channel = NotificationChannel(CALL_NOTIFICATION_CHANNEL_ID, CALL_NOTIFICATION_CHANNEL_NAME, NotificationManager.IMPORTANCE_DEFAULT)
|
|
||||||
notificationManager.createNotificationChannel(channel)
|
|
||||||
return notificationManager
|
|
||||||
}
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun createNotification(title: String, text: String, icon: Bitmap, connectedAt: Instant? = null): Notification {
|
|
||||||
val pendingIntent: PendingIntent = Intent(this, CallActivity::class.java).let { notificationIntent ->
|
|
||||||
PendingIntent.getActivity(this, 0, notificationIntent, PendingIntent.FLAG_IMMUTABLE)
|
|
||||||
}
|
|
||||||
|
|
||||||
val endCallPendingIntent: PendingIntent = Intent(this, CallActionReceiver::class.java).let { notificationIntent ->
|
|
||||||
notificationIntent.setAction(EndCallAction)
|
|
||||||
PendingIntent.getBroadcast(this, 1, notificationIntent, PendingIntent.FLAG_CANCEL_CURRENT or PendingIntent.FLAG_IMMUTABLE)
|
|
||||||
}
|
|
||||||
|
|
||||||
val builder = NotificationCompat.Builder(this, CALL_NOTIFICATION_CHANNEL_ID)
|
|
||||||
.setSmallIcon(R.drawable.ntf_icon)
|
|
||||||
.setLargeIcon(icon.clipToCircle())
|
|
||||||
.setColor(0x88FFFF)
|
|
||||||
.setContentTitle(title)
|
|
||||||
.setContentText(text)
|
|
||||||
.setContentIntent(pendingIntent)
|
|
||||||
.setSilent(true)
|
|
||||||
.addAction(R.drawable.ntf_icon, generalGetString(MR.strings.call_service_notification_end_call), endCallPendingIntent)
|
|
||||||
if (connectedAt != null) {
|
|
||||||
builder.setUsesChronometer(true)
|
|
||||||
builder.setWhen(connectedAt.epochSeconds * 1000)
|
|
||||||
}
|
|
||||||
|
|
||||||
return builder.build()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onBind(intent: Intent): IBinder {
|
|
||||||
return CallServiceBinder()
|
|
||||||
}
|
|
||||||
|
|
||||||
inner class CallServiceBinder : Binder() {
|
|
||||||
fun getService() = this@CallService
|
|
||||||
}
|
|
||||||
|
|
||||||
enum class Action {
|
|
||||||
START,
|
|
||||||
}
|
|
||||||
|
|
||||||
class CallActionReceiver: BroadcastReceiver() {
|
|
||||||
override fun onReceive(context: Context?, intent: Intent?) {
|
|
||||||
when (intent?.action) {
|
|
||||||
EndCallAction -> {
|
|
||||||
val call = chatModel.activeCall.value
|
|
||||||
if (call != null) {
|
|
||||||
withBGApi {
|
|
||||||
chatModel.callManager.endCall(call)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else -> {
|
|
||||||
Log.e(TAG, "Unknown action. Make sure you provided an action")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
const val TAG = "CALL_SERVICE"
|
|
||||||
const val CALL_NOTIFICATION_CHANNEL_ID = "chat.simplex.app.CALL_SERVICE_NOTIFICATION"
|
|
||||||
const val CALL_NOTIFICATION_CHANNEL_NAME = "SimpleX Chat call service"
|
|
||||||
const val CALL_SERVICE_ID = 6788
|
|
||||||
const val WAKE_LOCK_TAG = "CallService::lock"
|
|
||||||
|
|
||||||
fun startService(): Intent {
|
|
||||||
Log.d(TAG, "CallService start")
|
|
||||||
return Intent(androidAppContext, CallService::class.java).also {
|
|
||||||
it.action = Action.START.name
|
|
||||||
ContextCompat.startForegroundService(androidAppContext, it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun stopService() {
|
|
||||||
androidAppContext.stopService(Intent(androidAppContext, CallService::class.java))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,12 +1,11 @@
|
|||||||
package chat.simplex.app
|
package chat.simplex.app
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.*
|
import android.os.*
|
||||||
import android.view.WindowManager
|
import android.view.WindowManager
|
||||||
import androidx.activity.compose.setContent
|
import androidx.activity.compose.setContent
|
||||||
import androidx.compose.ui.platform.ClipboardManager
|
import androidx.appcompat.app.AppCompatDelegate
|
||||||
import androidx.fragment.app.FragmentActivity
|
import androidx.fragment.app.FragmentActivity
|
||||||
import chat.simplex.app.model.NtfManager
|
import chat.simplex.app.model.NtfManager
|
||||||
import chat.simplex.app.model.NtfManager.getUserIdFromIntent
|
import chat.simplex.app.model.NtfManager.getUserIdFromIntent
|
||||||
@@ -59,17 +58,6 @@ class MainActivity: FragmentActivity() {
|
|||||||
override fun onResume() {
|
override fun onResume() {
|
||||||
super.onResume()
|
super.onResume()
|
||||||
AppLock.recheckAuthState()
|
AppLock.recheckAuthState()
|
||||||
withApi {
|
|
||||||
delay(1000)
|
|
||||||
if (!isAppOnForeground) return@withApi
|
|
||||||
/**
|
|
||||||
* When the app calls [ClipboardManager.shareText] and a user copies text in clipboard, Android denies
|
|
||||||
* access to clipboard because the app considered in background.
|
|
||||||
* This will ensure that the app will get the event on resume
|
|
||||||
* */
|
|
||||||
val service = getSystemService(Context.CLIPBOARD_SERVICE) as android.content.ClipboardManager
|
|
||||||
chatModel.clipboardHasText.value = service.hasPrimaryClip()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onPause() {
|
override fun onPause() {
|
||||||
|
|||||||
@@ -1,15 +1,14 @@
|
|||||||
package chat.simplex.app
|
package chat.simplex.app
|
||||||
|
|
||||||
import android.app.*
|
import android.app.Application
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import androidx.compose.ui.platform.ClipboardManager
|
||||||
import chat.simplex.common.platform.Log
|
import chat.simplex.common.platform.Log
|
||||||
import android.content.Intent
|
import android.app.UiModeManager
|
||||||
import android.os.*
|
import android.os.*
|
||||||
import androidx.lifecycle.*
|
import androidx.lifecycle.*
|
||||||
import androidx.work.*
|
import androidx.work.*
|
||||||
import chat.simplex.app.model.NtfManager
|
import chat.simplex.app.model.NtfManager
|
||||||
import chat.simplex.app.model.NtfManager.AcceptCallAction
|
|
||||||
import chat.simplex.app.views.call.CallActivity
|
|
||||||
import chat.simplex.common.helpers.APPLICATION_ID
|
import chat.simplex.common.helpers.APPLICATION_ID
|
||||||
import chat.simplex.common.helpers.requiresIgnoringBattery
|
import chat.simplex.common.helpers.requiresIgnoringBattery
|
||||||
import chat.simplex.common.model.*
|
import chat.simplex.common.model.*
|
||||||
@@ -19,7 +18,6 @@ import chat.simplex.common.platform.*
|
|||||||
import chat.simplex.common.ui.theme.CurrentColors
|
import chat.simplex.common.ui.theme.CurrentColors
|
||||||
import chat.simplex.common.ui.theme.DefaultTheme
|
import chat.simplex.common.ui.theme.DefaultTheme
|
||||||
import chat.simplex.common.views.call.RcvCallInvitation
|
import chat.simplex.common.views.call.RcvCallInvitation
|
||||||
import chat.simplex.common.views.call.activeCallDestroyWebView
|
|
||||||
import chat.simplex.common.views.helpers.*
|
import chat.simplex.common.views.helpers.*
|
||||||
import chat.simplex.common.views.onboarding.OnboardingStage
|
import chat.simplex.common.views.onboarding.OnboardingStage
|
||||||
import com.jakewharton.processphoenix.ProcessPhoenix
|
import com.jakewharton.processphoenix.ProcessPhoenix
|
||||||
@@ -73,7 +71,7 @@ class SimplexApp: Application(), LifecycleEventObserver {
|
|||||||
|
|
||||||
override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) {
|
override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) {
|
||||||
Log.d(TAG, "onStateChanged: $event")
|
Log.d(TAG, "onStateChanged: $event")
|
||||||
withLongRunningApi {
|
withBGApi {
|
||||||
when (event) {
|
when (event) {
|
||||||
Lifecycle.Event.ON_START -> {
|
Lifecycle.Event.ON_START -> {
|
||||||
isAppOnForeground = true
|
isAppOnForeground = true
|
||||||
@@ -99,6 +97,13 @@ class SimplexApp: Application(), LifecycleEventObserver {
|
|||||||
}
|
}
|
||||||
Lifecycle.Event.ON_RESUME -> {
|
Lifecycle.Event.ON_RESUME -> {
|
||||||
isAppOnForeground = true
|
isAppOnForeground = true
|
||||||
|
/**
|
||||||
|
* When the app calls [ClipboardManager.shareText] and a user copies text in clipboard, Android denies
|
||||||
|
* access to clipboard because the app considered in background.
|
||||||
|
* This will ensure that the app will get the event on resume
|
||||||
|
* */
|
||||||
|
val service = androidAppContext.getSystemService(Context.CLIPBOARD_SERVICE) as android.content.ClipboardManager
|
||||||
|
chatModel.clipboardHasText.value = service.hasPrimaryClip()
|
||||||
if (chatModel.controller.appPrefs.onboardingStage.get() == OnboardingStage.OnboardingComplete && chatModel.currentUser.value != null) {
|
if (chatModel.controller.appPrefs.onboardingStage.get() == OnboardingStage.OnboardingComplete && chatModel.currentUser.value != null) {
|
||||||
SimplexService.showBackgroundServiceNoticeIfNeeded()
|
SimplexService.showBackgroundServiceNoticeIfNeeded()
|
||||||
}
|
}
|
||||||
@@ -186,28 +191,16 @@ class SimplexApp: Application(), LifecycleEventObserver {
|
|||||||
SimplexService.safeStopService()
|
SimplexService.safeStopService()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun androidCallServiceSafeStop() {
|
|
||||||
CallService.stopService()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun androidNotificationsModeChanged(mode: NotificationsMode) {
|
override fun androidNotificationsModeChanged(mode: NotificationsMode) {
|
||||||
if (mode.requiresIgnoringBattery && !SimplexService.isBackgroundAllowed()) {
|
if (mode.requiresIgnoringBattery && !SimplexService.isBackgroundAllowed()) {
|
||||||
appPrefs.backgroundServiceNoticeShown.set(false)
|
appPrefs.backgroundServiceNoticeShown.set(false)
|
||||||
}
|
}
|
||||||
SimplexService.StartReceiver.toggleReceiver(mode == NotificationsMode.SERVICE)
|
SimplexService.StartReceiver.toggleReceiver(mode == NotificationsMode.SERVICE)
|
||||||
CoroutineScope(Dispatchers.Default).launch {
|
CoroutineScope(Dispatchers.Default).launch {
|
||||||
if (mode == NotificationsMode.SERVICE) {
|
if (mode == NotificationsMode.SERVICE)
|
||||||
SimplexService.start()
|
SimplexService.start()
|
||||||
// Sometimes, when we change modes fast from one to another, system destroys the service after start.
|
else
|
||||||
// We can wait a little and restart the service, and it will work in 100% of cases
|
|
||||||
delay(2000)
|
|
||||||
if (!SimplexService.isServiceStarted && appPrefs.notificationsMode.get() == NotificationsMode.SERVICE) {
|
|
||||||
Log.i(TAG, "Service tried to start but destroyed by system, repeating once more")
|
|
||||||
SimplexService.start()
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
SimplexService.safeStopService()
|
SimplexService.safeStopService()
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (mode != NotificationsMode.PERIODIC) {
|
if (mode != NotificationsMode.PERIODIC) {
|
||||||
@@ -260,28 +253,6 @@ class SimplexApp: Application(), LifecycleEventObserver {
|
|||||||
uiModeManager.setApplicationNightMode(mode)
|
uiModeManager.setApplicationNightMode(mode)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun androidStartCallActivity(acceptCall: Boolean, remoteHostId: Long?, chatId: ChatId?) {
|
|
||||||
val context = mainActivity.get() ?: return
|
|
||||||
val intent = Intent(context, CallActivity::class.java)
|
|
||||||
.addFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION)
|
|
||||||
if (acceptCall) {
|
|
||||||
intent.setAction(AcceptCallAction)
|
|
||||||
.putExtra("remoteHostId", remoteHostId)
|
|
||||||
.putExtra("chatId", chatId)
|
|
||||||
}
|
|
||||||
intent.flags += Intent.FLAG_ACTIVITY_BROUGHT_TO_FRONT
|
|
||||||
context.startActivity(intent)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun androidPictureInPictureAllowed(): Boolean {
|
|
||||||
val appOps = androidAppContext.getSystemService(Context.APP_OPS_SERVICE) as AppOpsManager
|
|
||||||
return appOps.checkOpNoThrow(AppOpsManager.OPSTR_PICTURE_IN_PICTURE, Process.myUid(), packageName) == AppOpsManager.MODE_ALLOWED
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun androidCallEnded() {
|
|
||||||
activeCallDestroyWebView()
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun androidAskToAllowBackgroundCalls(): Boolean {
|
override suspend fun androidAskToAllowBackgroundCalls(): Boolean {
|
||||||
if (SimplexService.isBackgroundRestricted()) {
|
if (SimplexService.isBackgroundRestricted()) {
|
||||||
val userChoice: CompletableDeferred<Boolean> = CompletableDeferred()
|
val userChoice: CompletableDeferred<Boolean> = CompletableDeferred()
|
||||||
|
|||||||
@@ -34,13 +34,12 @@ import kotlin.system.exitProcess
|
|||||||
|
|
||||||
class SimplexService: Service() {
|
class SimplexService: Service() {
|
||||||
private var wakeLock: PowerManager.WakeLock? = null
|
private var wakeLock: PowerManager.WakeLock? = null
|
||||||
private var isCheckingNewMessages = false
|
private var isStartingService = false
|
||||||
private var notificationManager: NotificationManager? = null
|
private var notificationManager: NotificationManager? = null
|
||||||
private var serviceNotification: Notification? = null
|
private var serviceNotification: Notification? = null
|
||||||
|
|
||||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||||
Log.d(TAG, "onStartCommand startId: $startId")
|
Log.d(TAG, "onStartCommand startId: $startId")
|
||||||
isServiceStarting = false
|
|
||||||
if (intent != null) {
|
if (intent != null) {
|
||||||
val action = intent.action
|
val action = intent.action
|
||||||
Log.d(TAG, "intent action $action")
|
Log.d(TAG, "intent action $action")
|
||||||
@@ -72,7 +71,6 @@ class SimplexService: Service() {
|
|||||||
stopForeground(true)
|
stopForeground(true)
|
||||||
stopSelf()
|
stopSelf()
|
||||||
} else {
|
} else {
|
||||||
isServiceStarting = false
|
|
||||||
isServiceStarted = true
|
isServiceStarted = true
|
||||||
// In case of self-destruct is enabled the initialization process will not start in SimplexApp, Let's start it here
|
// In case of self-destruct is enabled the initialization process will not start in SimplexApp, Let's start it here
|
||||||
if (DatabaseUtils.ksSelfDestructPassword.get() != null && chatModel.chatDbStatus.value == null) {
|
if (DatabaseUtils.ksSelfDestructPassword.get() != null && chatModel.chatDbStatus.value == null) {
|
||||||
@@ -91,7 +89,6 @@ class SimplexService: Service() {
|
|||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.d(TAG, "Exception while releasing wakelock: ${e.message}")
|
Log.d(TAG, "Exception while releasing wakelock: ${e.message}")
|
||||||
}
|
}
|
||||||
isServiceStarting = false
|
|
||||||
isServiceStarted = false
|
isServiceStarted = false
|
||||||
stopAfterStart = false
|
stopAfterStart = false
|
||||||
saveServiceState(this, ServiceState.STOPPED)
|
saveServiceState(this, ServiceState.STOPPED)
|
||||||
@@ -104,10 +101,10 @@ class SimplexService: Service() {
|
|||||||
|
|
||||||
private fun startService() {
|
private fun startService() {
|
||||||
Log.d(TAG, "SimplexService startService")
|
Log.d(TAG, "SimplexService startService")
|
||||||
if (wakeLock != null || isCheckingNewMessages) return
|
if (wakeLock != null || isStartingService) return
|
||||||
val self = this
|
val self = this
|
||||||
isCheckingNewMessages = true
|
isStartingService = true
|
||||||
withLongRunningApi {
|
withLongRunningApi(slow = 30_000, deadlock = 60_000) {
|
||||||
val chatController = ChatController
|
val chatController = ChatController
|
||||||
waitDbMigrationEnds(chatController)
|
waitDbMigrationEnds(chatController)
|
||||||
try {
|
try {
|
||||||
@@ -126,7 +123,7 @@ class SimplexService: Service() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
isCheckingNewMessages = false
|
isStartingService = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -265,8 +262,7 @@ class SimplexService: Service() {
|
|||||||
private const val SHARED_PREFS_SERVICE_STATE = "SIMPLEX_SERVICE_STATE"
|
private const val SHARED_PREFS_SERVICE_STATE = "SIMPLEX_SERVICE_STATE"
|
||||||
private const val WORK_NAME_ONCE = "ServiceStartWorkerOnce"
|
private const val WORK_NAME_ONCE = "ServiceStartWorkerOnce"
|
||||||
|
|
||||||
var isServiceStarting = false
|
private var isServiceStarted = false
|
||||||
var isServiceStarted = false
|
|
||||||
private var stopAfterStart = false
|
private var stopAfterStart = false
|
||||||
|
|
||||||
fun scheduleStart(context: Context) {
|
fun scheduleStart(context: Context) {
|
||||||
@@ -285,7 +281,7 @@ class SimplexService: Service() {
|
|||||||
fun safeStopService() {
|
fun safeStopService() {
|
||||||
if (isServiceStarted) {
|
if (isServiceStarted) {
|
||||||
androidAppContext.stopService(Intent(androidAppContext, SimplexService::class.java))
|
androidAppContext.stopService(Intent(androidAppContext, SimplexService::class.java))
|
||||||
} else if (isServiceStarting) {
|
} else {
|
||||||
stopAfterStart = true
|
stopAfterStart = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -295,7 +291,6 @@ class SimplexService: Service() {
|
|||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
Intent(androidAppContext, SimplexService::class.java).also {
|
Intent(androidAppContext, SimplexService::class.java).also {
|
||||||
it.action = action.name
|
it.action = action.name
|
||||||
isServiceStarting = true
|
|
||||||
ContextCompat.startForegroundService(androidAppContext, it)
|
ContextCompat.startForegroundService(androidAppContext, it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-3
@@ -13,7 +13,7 @@ import androidx.compose.ui.graphics.asAndroidBitmap
|
|||||||
import androidx.core.app.*
|
import androidx.core.app.*
|
||||||
import chat.simplex.app.*
|
import chat.simplex.app.*
|
||||||
import chat.simplex.app.TAG
|
import chat.simplex.app.TAG
|
||||||
import chat.simplex.app.views.call.CallActivity
|
import chat.simplex.app.views.call.IncomingCallActivity
|
||||||
import chat.simplex.app.views.call.getKeyguardManager
|
import chat.simplex.app.views.call.getKeyguardManager
|
||||||
import chat.simplex.common.views.helpers.*
|
import chat.simplex.common.views.helpers.*
|
||||||
import chat.simplex.common.model.*
|
import chat.simplex.common.model.*
|
||||||
@@ -33,7 +33,6 @@ object NtfManager {
|
|||||||
const val CallChannel: String = "chat.simplex.app.CALL_NOTIFICATION_2"
|
const val CallChannel: String = "chat.simplex.app.CALL_NOTIFICATION_2"
|
||||||
const val AcceptCallAction: String = "chat.simplex.app.ACCEPT_CALL"
|
const val AcceptCallAction: String = "chat.simplex.app.ACCEPT_CALL"
|
||||||
const val RejectCallAction: String = "chat.simplex.app.REJECT_CALL"
|
const val RejectCallAction: String = "chat.simplex.app.REJECT_CALL"
|
||||||
const val EndCallAction: String = "chat.simplex.app.END_CALL"
|
|
||||||
const val CallNotificationId: Int = -1
|
const val CallNotificationId: Int = -1
|
||||||
private const val UserIdKey: String = "userId"
|
private const val UserIdKey: String = "userId"
|
||||||
private const val ChatIdKey: String = "chatId"
|
private const val ChatIdKey: String = "chatId"
|
||||||
@@ -158,7 +157,7 @@ object NtfManager {
|
|||||||
val screenOff = displayManager.displays.all { it.state != Display.STATE_ON }
|
val screenOff = displayManager.displays.all { it.state != Display.STATE_ON }
|
||||||
var ntfBuilder =
|
var ntfBuilder =
|
||||||
if ((keyguardManager.isKeyguardLocked || screenOff) && appPreferences.callOnLockScreen.get() != CallOnLockScreen.DISABLE) {
|
if ((keyguardManager.isKeyguardLocked || screenOff) && appPreferences.callOnLockScreen.get() != CallOnLockScreen.DISABLE) {
|
||||||
val fullScreenIntent = Intent(context, CallActivity::class.java)
|
val fullScreenIntent = Intent(context, IncomingCallActivity::class.java)
|
||||||
val fullScreenPendingIntent = PendingIntent.getActivity(context, 0, fullScreenIntent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)
|
val fullScreenPendingIntent = PendingIntent.getActivity(context, 0, fullScreenIntent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)
|
||||||
NotificationCompat.Builder(context, CallChannel)
|
NotificationCompat.Builder(context, CallChannel)
|
||||||
.setFullScreenIntent(fullScreenPendingIntent, true)
|
.setFullScreenIntent(fullScreenPendingIntent, true)
|
||||||
|
|||||||
+36
-194
@@ -1,18 +1,17 @@
|
|||||||
package chat.simplex.app.views.call
|
package chat.simplex.app.views.call
|
||||||
|
|
||||||
import android.app.*
|
import android.app.Activity
|
||||||
import android.content.*
|
import android.app.KeyguardManager
|
||||||
import android.content.res.Configuration
|
import android.content.Context
|
||||||
import android.graphics.Rect
|
import android.content.Intent
|
||||||
import android.os.*
|
import android.os.Build
|
||||||
import android.util.Rational
|
import android.os.Bundle
|
||||||
import android.view.*
|
import chat.simplex.common.platform.Log
|
||||||
|
import android.view.WindowManager
|
||||||
import androidx.activity.ComponentActivity
|
import androidx.activity.ComponentActivity
|
||||||
import androidx.activity.compose.setContent
|
import androidx.activity.compose.setContent
|
||||||
import androidx.activity.trackPipAnimationHintView
|
|
||||||
import androidx.compose.desktop.ui.tooling.preview.Preview
|
import androidx.compose.desktop.ui.tooling.preview.Preview
|
||||||
import androidx.compose.foundation.Image
|
import androidx.compose.foundation.Image
|
||||||
import androidx.compose.foundation.background
|
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material.*
|
import androidx.compose.material.*
|
||||||
@@ -23,115 +22,33 @@ import androidx.compose.ui.draw.scale
|
|||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.graphics.painter.Painter
|
import androidx.compose.ui.graphics.painter.Painter
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.platform.LocalView
|
|
||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.res.painterResource
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import androidx.lifecycle.Lifecycle
|
|
||||||
import chat.simplex.app.*
|
import chat.simplex.app.*
|
||||||
import chat.simplex.app.R
|
import chat.simplex.app.R
|
||||||
import chat.simplex.app.TAG
|
|
||||||
import chat.simplex.app.model.NtfManager
|
|
||||||
import chat.simplex.app.model.NtfManager.AcceptCallAction
|
|
||||||
import chat.simplex.common.model.*
|
import chat.simplex.common.model.*
|
||||||
import chat.simplex.common.platform.*
|
import chat.simplex.app.model.NtfManager.OpenChatAction
|
||||||
|
import chat.simplex.common.platform.ntfManager
|
||||||
import chat.simplex.common.ui.theme.*
|
import chat.simplex.common.ui.theme.*
|
||||||
import chat.simplex.common.views.call.*
|
import chat.simplex.common.views.call.*
|
||||||
import chat.simplex.common.views.helpers.*
|
import chat.simplex.common.views.helpers.*
|
||||||
import chat.simplex.res.MR
|
import chat.simplex.res.MR
|
||||||
import dev.icerock.moko.resources.compose.stringResource
|
import dev.icerock.moko.resources.compose.stringResource
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import kotlinx.datetime.Clock
|
import kotlinx.datetime.Clock
|
||||||
import java.lang.ref.WeakReference
|
|
||||||
import chat.simplex.common.platform.chatModel as m
|
|
||||||
|
|
||||||
class CallActivity: ComponentActivity(), ServiceConnection {
|
class IncomingCallActivity: ComponentActivity() {
|
||||||
|
|
||||||
var boundService: CallService? = null
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
callActivity = WeakReference(this)
|
setContent { IncomingCallActivityView(ChatModel) }
|
||||||
when (intent?.action) {
|
unlockForIncomingCall()
|
||||||
AcceptCallAction -> {
|
|
||||||
val remoteHostId = intent.getLongExtra("remoteHostId", -1).takeIf { it != -1L }
|
|
||||||
val chatId = intent.getStringExtra("chatId")
|
|
||||||
val invitation = (m.callInvitations.values + m.activeCallInvitation.value).lastOrNull {
|
|
||||||
it?.remoteHostId == remoteHostId && it?.contact?.id == chatId
|
|
||||||
}
|
|
||||||
if (invitation != null) {
|
|
||||||
m.callManager.acceptIncomingCall(invitation = invitation)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setContent { CallActivityView() }
|
|
||||||
|
|
||||||
if (isOnLockScreenNow()) {
|
|
||||||
unlockForIncomingCall()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
super.onDestroy()
|
super.onDestroy()
|
||||||
if (isOnLockScreenNow()) {
|
lockAfterIncomingCall()
|
||||||
lockAfterIncomingCall()
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
unbindService(this)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.i(TAG, "Unable to unbind service: " + e.stackTraceToString())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun isOnLockScreenNow() = getKeyguardManager(this).isKeyguardLocked
|
|
||||||
|
|
||||||
fun setPipParams(video: Boolean, sourceRectHint: Rect? = null, viewRatio: Rational? = null) {
|
|
||||||
// By manually specifying source rect we exclude empty background while toggling PiP
|
|
||||||
val builder = PictureInPictureParams.Builder()
|
|
||||||
.setAspectRatio(viewRatio)
|
|
||||||
.setSourceRectHint(sourceRectHint)
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
|
||||||
builder.setAutoEnterEnabled(video)
|
|
||||||
}
|
|
||||||
setPictureInPictureParams(builder.build())
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onPictureInPictureModeChanged(isInPictureInPictureMode: Boolean, newConfig: Configuration) {
|
|
||||||
super.onPictureInPictureModeChanged(isInPictureInPictureMode, newConfig)
|
|
||||||
m.activeCallViewIsCollapsed.value = isInPictureInPictureMode
|
|
||||||
val layoutType = if (!isInPictureInPictureMode) {
|
|
||||||
LayoutType.Default
|
|
||||||
} else {
|
|
||||||
LayoutType.RemoteVideo
|
|
||||||
}
|
|
||||||
m.callCommand.add(WCallCommand.Layout(layoutType))
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onBackPressed() {
|
|
||||||
if (isOnLockScreenNow()) {
|
|
||||||
super.onBackPressed()
|
|
||||||
} else {
|
|
||||||
m.activeCallViewIsCollapsed.value = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onPictureInPictureRequested(): Boolean {
|
|
||||||
Log.d(TAG, "Requested picture-in-picture from the system")
|
|
||||||
return super.onPictureInPictureRequested()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onUserLeaveHint() {
|
|
||||||
// On Android 12+ PiP is enabled automatically when a user hides the app
|
|
||||||
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.R && callSupportsVideo() && platform.androidPictureInPictureAllowed()) {
|
|
||||||
enterPictureInPictureMode()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onResume() {
|
|
||||||
super.onResume()
|
|
||||||
m.activeCallViewIsCollapsed.value = false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun unlockForIncomingCall() {
|
private fun unlockForIncomingCall() {
|
||||||
@@ -155,23 +72,6 @@ class CallActivity: ComponentActivity(), ServiceConnection {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun startServiceAndBind() {
|
|
||||||
/**
|
|
||||||
* On Android 12 there is a bug that prevents starting activity after pressing back button
|
|
||||||
* (the error says that it denies to start activity in background).
|
|
||||||
* Workaround is to bind to a service
|
|
||||||
* */
|
|
||||||
bindService(CallService.startService(), this, 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
|
|
||||||
boundService = (service as CallService.CallServiceBinder).getService()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onServiceDisconnected(name: ComponentName?) {
|
|
||||||
boundService = null
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val activityFlags = WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON or WindowManager.LayoutParams.FLAG_ALLOW_LOCK_WHILE_SCREEN_ON
|
const val activityFlags = WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON or WindowManager.LayoutParams.FLAG_ALLOW_LOCK_WHILE_SCREEN_ON
|
||||||
}
|
}
|
||||||
@@ -180,96 +80,38 @@ class CallActivity: ComponentActivity(), ServiceConnection {
|
|||||||
fun getKeyguardManager(context: Context): KeyguardManager =
|
fun getKeyguardManager(context: Context): KeyguardManager =
|
||||||
context.getSystemService(Context.KEYGUARD_SERVICE) as KeyguardManager
|
context.getSystemService(Context.KEYGUARD_SERVICE) as KeyguardManager
|
||||||
|
|
||||||
private fun callSupportsVideo() = m.activeCall.value?.supportsVideo() == true || m.activeCallInvitation.value?.callType?.media == CallMediaType.Video
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun CallActivityView() {
|
fun IncomingCallActivityView(m: ChatModel) {
|
||||||
val switchingCall = m.switchingCall.value
|
val switchingCall = m.switchingCall.value
|
||||||
val invitation = m.activeCallInvitation.value
|
val invitation = m.activeCallInvitation.value
|
||||||
val call = remember { m.activeCall }.value
|
val call = m.activeCall.value
|
||||||
val showCallView = m.showCallView.value
|
val showCallView = m.showCallView.value
|
||||||
val activity = LocalContext.current as CallActivity
|
val activity = LocalContext.current as Activity
|
||||||
LaunchedEffect(Unit) {
|
|
||||||
snapshotFlow { m.activeCallViewIsCollapsed.value }
|
|
||||||
.collect { collapsed ->
|
|
||||||
when {
|
|
||||||
collapsed -> {
|
|
||||||
if (!platform.androidPictureInPictureAllowed() || !callSupportsVideo()) {
|
|
||||||
activity.moveTaskToBack(true)
|
|
||||||
activity.startActivity(Intent(activity, MainActivity::class.java))
|
|
||||||
} else if (!activity.isInPictureInPictureMode && activity.lifecycle.currentState == Lifecycle.State.RESUMED) {
|
|
||||||
// User pressed back button, show MainActivity
|
|
||||||
activity.startActivity(Intent(activity, MainActivity::class.java))
|
|
||||||
activity.enterPictureInPictureMode()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
callSupportsVideo() && !platform.androidPictureInPictureAllowed() -> {
|
|
||||||
// PiP disabled by user
|
|
||||||
platform.androidStartCallActivity(false)
|
|
||||||
}
|
|
||||||
activity.isInPictureInPictureMode -> {
|
|
||||||
platform.androidStartCallActivity(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
SimpleXTheme {
|
|
||||||
var prevCall by remember { mutableStateOf(call) }
|
|
||||||
KeyChangeEffect(m.activeCall.value) {
|
|
||||||
if (m.activeCall.value != null) {
|
|
||||||
prevCall = m.activeCall.value
|
|
||||||
activity.boundService?.updateNotification()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Box(Modifier.background(Color.Black)) {
|
|
||||||
if (call != null) {
|
|
||||||
val view = LocalView.current
|
|
||||||
ActiveCallView()
|
|
||||||
if (callSupportsVideo()) {
|
|
||||||
val scope = rememberCoroutineScope()
|
|
||||||
LaunchedEffect(Unit) {
|
|
||||||
scope.launch {
|
|
||||||
activity.setPipParams(callSupportsVideo(), viewRatio = Rational(view.width, view.height))
|
|
||||||
activity.trackPipAnimationHintView(view)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (prevCall != null) {
|
|
||||||
prevCall?.let { ActiveCallOverlayDisabled(it) }
|
|
||||||
}
|
|
||||||
if (invitation != null) {
|
|
||||||
if (call == null) {
|
|
||||||
Surface(
|
|
||||||
Modifier
|
|
||||||
.fillMaxSize(),
|
|
||||||
color = MaterialTheme.colors.background,
|
|
||||||
contentColor = LocalContentColor.current
|
|
||||||
) {
|
|
||||||
IncomingCallLockScreenAlert(invitation, m)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
IncomingCallAlertView(invitation, m)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
LaunchedEffect(call == null) {
|
|
||||||
if (call != null) {
|
|
||||||
activity.startServiceAndBind()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
LaunchedEffect(invitation, call, switchingCall, showCallView) {
|
LaunchedEffect(invitation, call, switchingCall, showCallView) {
|
||||||
if (!switchingCall && invitation == null && (!showCallView || call == null)) {
|
if (!switchingCall && invitation == null && (!showCallView || call == null)) {
|
||||||
Log.d(TAG, "CallActivityView: finishing activity")
|
Log.d(TAG, "IncomingCallActivityView: finishing activity")
|
||||||
activity.finish()
|
activity.finish()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
SimpleXTheme {
|
||||||
|
Surface(
|
||||||
|
Modifier
|
||||||
|
.fillMaxSize(),
|
||||||
|
color = MaterialTheme.colors.background,
|
||||||
|
contentColor = LocalContentColor.current
|
||||||
|
) {
|
||||||
|
if (showCallView) {
|
||||||
|
Box {
|
||||||
|
ActiveCallView()
|
||||||
|
if (invitation != null) IncomingCallAlertView(invitation, m)
|
||||||
|
}
|
||||||
|
} else if (invitation != null) {
|
||||||
|
IncomingCallLockScreenAlert(invitation, m)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Related to lockscreen
|
|
||||||
* */
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun IncomingCallLockScreenAlert(invitation: RcvCallInvitation, chatModel: ChatModel) {
|
fun IncomingCallLockScreenAlert(invitation: RcvCallInvitation, chatModel: ChatModel) {
|
||||||
val cm = chatModel.callManager
|
val cm = chatModel.callManager
|
||||||
@@ -293,7 +135,7 @@ fun IncomingCallLockScreenAlert(invitation: RcvCallInvitation, chatModel: ChatMo
|
|||||||
acceptCall = { cm.acceptIncomingCall(invitation = invitation) },
|
acceptCall = { cm.acceptIncomingCall(invitation = invitation) },
|
||||||
openApp = {
|
openApp = {
|
||||||
val intent = Intent(context, MainActivity::class.java)
|
val intent = Intent(context, MainActivity::class.java)
|
||||||
.setAction(NtfManager.OpenChatAction)
|
.setAction(OpenChatAction)
|
||||||
.putExtra("userId", invitation.user.userId)
|
.putExtra("userId", invitation.user.userId)
|
||||||
.putExtra("chatId", invitation.contact.id)
|
.putExtra("chatId", invitation.contact.id)
|
||||||
context.startActivity(intent)
|
context.startActivity(intent)
|
||||||
+1
-3
@@ -4,7 +4,6 @@ import android.annotation.SuppressLint
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.net.LocalServerSocket
|
import android.net.LocalServerSocket
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.activity.ComponentActivity
|
|
||||||
import androidx.fragment.app.FragmentActivity
|
import androidx.fragment.app.FragmentActivity
|
||||||
import chat.simplex.common.*
|
import chat.simplex.common.*
|
||||||
import chat.simplex.common.platform.*
|
import chat.simplex.common.platform.*
|
||||||
@@ -26,8 +25,7 @@ val defaultLocale: Locale = Locale.getDefault()
|
|||||||
|
|
||||||
@SuppressLint("StaticFieldLeak")
|
@SuppressLint("StaticFieldLeak")
|
||||||
lateinit var androidAppContext: Context
|
lateinit var androidAppContext: Context
|
||||||
var mainActivity: WeakReference<FragmentActivity> = WeakReference(null)
|
lateinit var mainActivity: WeakReference<FragmentActivity>
|
||||||
var callActivity: WeakReference<ComponentActivity> = WeakReference(null)
|
|
||||||
|
|
||||||
fun initHaskell() {
|
fun initHaskell() {
|
||||||
val socketName = "chat.simplex.app.local.socket.address.listen.native.cmd2" + Random.nextLong(100000)
|
val socketName = "chat.simplex.app.local.socket.address.listen.native.cmd2" + Random.nextLong(100000)
|
||||||
|
|||||||
-10
@@ -61,16 +61,6 @@ actual fun cropToSquare(image: ImageBitmap): ImageBitmap {
|
|||||||
return Bitmap.createBitmap(image.asAndroidBitmap(), xOffset, yOffset, side, side).asImageBitmap()
|
return Bitmap.createBitmap(image.asAndroidBitmap(), xOffset, yOffset, side, side).asImageBitmap()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun Bitmap.clipToCircle(): Bitmap {
|
|
||||||
val circle = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
|
|
||||||
val path = android.graphics.Path()
|
|
||||||
path.addCircle(width / 2f, height / 2f, min(width, height) / 2f, android.graphics.Path.Direction.CCW)
|
|
||||||
val canvas = android.graphics.Canvas(circle)
|
|
||||||
canvas.clipPath(path)
|
|
||||||
canvas.drawBitmap(this, 0f, 0f, null)
|
|
||||||
return circle
|
|
||||||
}
|
|
||||||
|
|
||||||
actual fun compressImageStr(bitmap: ImageBitmap): String {
|
actual fun compressImageStr(bitmap: ImageBitmap): String {
|
||||||
val usePng = bitmap.hasAlpha()
|
val usePng = bitmap.hasAlpha()
|
||||||
val ext = if (usePng) "png" else "jpg"
|
val ext = if (usePng) "png" else "jpg"
|
||||||
|
|||||||
+8
-18
@@ -14,27 +14,17 @@ import chat.simplex.common.views.helpers.*
|
|||||||
import java.io.BufferedOutputStream
|
import java.io.BufferedOutputStream
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import chat.simplex.res.MR
|
import chat.simplex.res.MR
|
||||||
import kotlin.math.min
|
|
||||||
|
|
||||||
actual fun ClipboardManager.shareText(text: String) {
|
actual fun ClipboardManager.shareText(text: String) {
|
||||||
var text = text
|
val sendIntent: Intent = Intent().apply {
|
||||||
for (i in 10 downTo 1) {
|
action = Intent.ACTION_SEND
|
||||||
try {
|
putExtra(Intent.EXTRA_TEXT, text)
|
||||||
val sendIntent: Intent = Intent().apply {
|
type = "text/plain"
|
||||||
action = Intent.ACTION_SEND
|
flags = FLAG_ACTIVITY_NEW_TASK
|
||||||
putExtra(Intent.EXTRA_TEXT, text)
|
|
||||||
type = "text/plain"
|
|
||||||
flags = FLAG_ACTIVITY_NEW_TASK
|
|
||||||
}
|
|
||||||
val shareIntent = Intent.createChooser(sendIntent, null)
|
|
||||||
shareIntent.addFlags(FLAG_ACTIVITY_NEW_TASK)
|
|
||||||
androidAppContext.startActivity(shareIntent)
|
|
||||||
break
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e(TAG, "Failed to share text: ${e.stackTraceToString()}")
|
|
||||||
text = text.substring(0, min(i * 1000, text.length))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
val shareIntent = Intent.createChooser(sendIntent, null)
|
||||||
|
shareIntent.addFlags(FLAG_ACTIVITY_NEW_TASK)
|
||||||
|
androidAppContext.startActivity(shareIntent)
|
||||||
}
|
}
|
||||||
|
|
||||||
actual fun shareFile(text: String, fileSource: CryptoFile) {
|
actual fun shareFile(text: String, fileSource: CryptoFile) {
|
||||||
|
|||||||
+1
-4
@@ -12,8 +12,6 @@ import androidx.activity.compose.setContent
|
|||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.*
|
||||||
import androidx.compose.ui.platform.LocalView
|
import androidx.compose.ui.platform.LocalView
|
||||||
import chat.simplex.common.AppScreen
|
import chat.simplex.common.AppScreen
|
||||||
import chat.simplex.common.model.clear
|
|
||||||
import chat.simplex.common.ui.theme.SimpleXTheme
|
|
||||||
import chat.simplex.common.views.helpers.*
|
import chat.simplex.common.views.helpers.*
|
||||||
import androidx.compose.ui.platform.LocalContext as LocalContext1
|
import androidx.compose.ui.platform.LocalContext as LocalContext1
|
||||||
import chat.simplex.res.MR
|
import chat.simplex.res.MR
|
||||||
@@ -114,8 +112,7 @@ actual class GlobalExceptionsHandler: Thread.UncaughtExceptionHandler {
|
|||||||
Handler(Looper.getMainLooper()).post {
|
Handler(Looper.getMainLooper()).post {
|
||||||
AlertManager.shared.showAlertMsg(
|
AlertManager.shared.showAlertMsg(
|
||||||
title = generalGetString(MR.strings.app_was_crashed),
|
title = generalGetString(MR.strings.app_was_crashed),
|
||||||
text = e.stackTraceToString(),
|
text = e.stackTraceToString()
|
||||||
shareText = true
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+95
-170
@@ -28,7 +28,6 @@ import androidx.compose.ui.platform.LocalLifecycleOwner
|
|||||||
import dev.icerock.moko.resources.compose.painterResource
|
import dev.icerock.moko.resources.compose.painterResource
|
||||||
import dev.icerock.moko.resources.compose.stringResource
|
import dev.icerock.moko.resources.compose.stringResource
|
||||||
import androidx.compose.ui.text.TextStyle
|
import androidx.compose.ui.text.TextStyle
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.viewinterop.AndroidView
|
import androidx.compose.ui.viewinterop.AndroidView
|
||||||
import androidx.lifecycle.Lifecycle
|
import androidx.lifecycle.Lifecycle
|
||||||
@@ -51,30 +50,20 @@ import kotlinx.datetime.Clock
|
|||||||
import kotlinx.serialization.decodeFromString
|
import kotlinx.serialization.decodeFromString
|
||||||
import kotlinx.serialization.encodeToString
|
import kotlinx.serialization.encodeToString
|
||||||
|
|
||||||
// Should be destroy()'ed and set as null when call is ended. Otherwise, it will be a leak
|
|
||||||
@SuppressLint("StaticFieldLeak")
|
|
||||||
private var staticWebView: WebView? = null
|
|
||||||
|
|
||||||
// WebView methods must be called on Main thread
|
|
||||||
fun activeCallDestroyWebView() = withApi {
|
|
||||||
// Stop it when call ended
|
|
||||||
platform.androidCallServiceSafeStop()
|
|
||||||
staticWebView?.destroy()
|
|
||||||
staticWebView = null
|
|
||||||
Log.d(TAG, "CallView: webview was destroyed")
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressLint("SourceLockedOrientationActivity")
|
@SuppressLint("SourceLockedOrientationActivity")
|
||||||
@Composable
|
@Composable
|
||||||
actual fun ActiveCallView() {
|
actual fun ActiveCallView() {
|
||||||
|
val chatModel = ChatModel
|
||||||
|
BackHandler(onBack = {
|
||||||
|
val call = chatModel.activeCall.value
|
||||||
|
if (call != null) withBGApi { chatModel.callManager.endCall(call) }
|
||||||
|
})
|
||||||
val audioViaBluetooth = rememberSaveable { mutableStateOf(false) }
|
val audioViaBluetooth = rememberSaveable { mutableStateOf(false) }
|
||||||
val proximityLock = remember {
|
val ntfModeService = remember { chatModel.controller.appPrefs.notificationsMode.get() == NotificationsMode.SERVICE }
|
||||||
val pm = (androidAppContext.getSystemService(Context.POWER_SERVICE) as PowerManager)
|
LaunchedEffect(Unit) {
|
||||||
if (pm.isWakeLockLevelSupported(PROXIMITY_SCREEN_OFF_WAKE_LOCK)) {
|
// Start service when call happening since it's not already started.
|
||||||
pm.newWakeLock(PROXIMITY_SCREEN_OFF_WAKE_LOCK, androidAppContext.packageName + ":proximityLock")
|
// It's needed to prevent Android from shutting down a microphone after a minute or so when screen is off
|
||||||
} else {
|
if (!ntfModeService) platform.androidServiceStart()
|
||||||
null
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
DisposableEffect(Unit) {
|
DisposableEffect(Unit) {
|
||||||
val am = androidAppContext.getSystemService(Context.AUDIO_SERVICE) as AudioManager
|
val am = androidAppContext.getSystemService(Context.AUDIO_SERVICE) as AudioManager
|
||||||
@@ -104,24 +93,22 @@ actual fun ActiveCallView() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
am.registerAudioDeviceCallback(audioCallback, null)
|
am.registerAudioDeviceCallback(audioCallback, null)
|
||||||
|
val pm = (androidAppContext.getSystemService(Context.POWER_SERVICE) as PowerManager)
|
||||||
|
val proximityLock = if (pm.isWakeLockLevelSupported(PROXIMITY_SCREEN_OFF_WAKE_LOCK)) {
|
||||||
|
pm.newWakeLock(PROXIMITY_SCREEN_OFF_WAKE_LOCK, androidAppContext.packageName + ":proximityLock")
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
proximityLock?.acquire()
|
||||||
onDispose {
|
onDispose {
|
||||||
|
// Stop it when call ended
|
||||||
|
if (!ntfModeService) platform.androidServiceSafeStop()
|
||||||
dropAudioManagerOverrides()
|
dropAudioManagerOverrides()
|
||||||
am.unregisterAudioDeviceCallback(audioCallback)
|
am.unregisterAudioDeviceCallback(audioCallback)
|
||||||
if (proximityLock?.isHeld == true) {
|
proximityLock?.release()
|
||||||
proximityLock.release()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
LaunchedEffect(chatModel.activeCallViewIsCollapsed.value) {
|
|
||||||
if (chatModel.activeCallViewIsCollapsed.value) {
|
|
||||||
if (proximityLock?.isHeld == true) proximityLock.release()
|
|
||||||
} else {
|
|
||||||
delay(1000)
|
|
||||||
if (proximityLock?.isHeld == false) proximityLock.acquire()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
val call = chatModel.activeCall.value
|
|
||||||
Box(Modifier.fillMaxSize()) {
|
Box(Modifier.fillMaxSize()) {
|
||||||
WebRTCView(chatModel.callCommand) { apiMsg ->
|
WebRTCView(chatModel.callCommand) { apiMsg ->
|
||||||
Log.d(TAG, "received from WebRTCView: $apiMsg")
|
Log.d(TAG, "received from WebRTCView: $apiMsg")
|
||||||
@@ -133,15 +120,15 @@ actual fun ActiveCallView() {
|
|||||||
is WCallResponse.Capabilities -> withBGApi {
|
is WCallResponse.Capabilities -> withBGApi {
|
||||||
val callType = CallType(call.localMedia, r.capabilities)
|
val callType = CallType(call.localMedia, r.capabilities)
|
||||||
chatModel.controller.apiSendCallInvitation(callRh, call.contact, callType)
|
chatModel.controller.apiSendCallInvitation(callRh, call.contact, callType)
|
||||||
updateActiveCall(call) { it.copy(callState = CallState.InvitationSent, localCapabilities = r.capabilities) }
|
chatModel.activeCall.value = call.copy(callState = CallState.InvitationSent, localCapabilities = r.capabilities)
|
||||||
}
|
}
|
||||||
is WCallResponse.Offer -> withBGApi {
|
is WCallResponse.Offer -> withBGApi {
|
||||||
chatModel.controller.apiSendCallOffer(callRh, call.contact, r.offer, r.iceCandidates, call.localMedia, r.capabilities)
|
chatModel.controller.apiSendCallOffer(callRh, call.contact, r.offer, r.iceCandidates, call.localMedia, r.capabilities)
|
||||||
updateActiveCall(call) { it.copy(callState = CallState.OfferSent, localCapabilities = r.capabilities) }
|
chatModel.activeCall.value = call.copy(callState = CallState.OfferSent, localCapabilities = r.capabilities)
|
||||||
}
|
}
|
||||||
is WCallResponse.Answer -> withBGApi {
|
is WCallResponse.Answer -> withBGApi {
|
||||||
chatModel.controller.apiSendCallAnswer(callRh, call.contact, r.answer, r.iceCandidates)
|
chatModel.controller.apiSendCallAnswer(callRh, call.contact, r.answer, r.iceCandidates)
|
||||||
updateActiveCall(call) { it.copy(callState = CallState.Negotiated) }
|
chatModel.activeCall.value = call.copy(callState = CallState.Negotiated)
|
||||||
}
|
}
|
||||||
is WCallResponse.Ice -> withBGApi {
|
is WCallResponse.Ice -> withBGApi {
|
||||||
chatModel.controller.apiSendCallExtraInfo(callRh, call.contact, r.iceCandidates)
|
chatModel.controller.apiSendCallExtraInfo(callRh, call.contact, r.iceCandidates)
|
||||||
@@ -150,7 +137,7 @@ actual fun ActiveCallView() {
|
|||||||
try {
|
try {
|
||||||
val callStatus = json.decodeFromString<WebRTCCallStatus>("\"${r.state.connectionState}\"")
|
val callStatus = json.decodeFromString<WebRTCCallStatus>("\"${r.state.connectionState}\"")
|
||||||
if (callStatus == WebRTCCallStatus.Connected) {
|
if (callStatus == WebRTCCallStatus.Connected) {
|
||||||
updateActiveCall(call) { it.copy(callState = CallState.Connected, connectedAt = Clock.System.now()) }
|
chatModel.activeCall.value = call.copy(callState = CallState.Connected, connectedAt = Clock.System.now())
|
||||||
setCallSound(call.soundSpeaker, audioViaBluetooth)
|
setCallSound(call.soundSpeaker, audioViaBluetooth)
|
||||||
}
|
}
|
||||||
withBGApi { chatModel.controller.apiCallStatus(callRh, call.contact, callStatus) }
|
withBGApi { chatModel.controller.apiCallStatus(callRh, call.contact, callStatus) }
|
||||||
@@ -158,7 +145,7 @@ actual fun ActiveCallView() {
|
|||||||
Log.d(TAG,"call status ${r.state.connectionState} not used")
|
Log.d(TAG,"call status ${r.state.connectionState} not used")
|
||||||
}
|
}
|
||||||
is WCallResponse.Connected -> {
|
is WCallResponse.Connected -> {
|
||||||
updateActiveCall(call) { it.copy(callState = CallState.Connected, connectionInfo = r.connectionInfo) }
|
chatModel.activeCall.value = call.copy(callState = CallState.Connected, connectionInfo = r.connectionInfo)
|
||||||
scope.launch {
|
scope.launch {
|
||||||
setCallSound(call.soundSpeaker, audioViaBluetooth)
|
setCallSound(call.soundSpeaker, audioViaBluetooth)
|
||||||
}
|
}
|
||||||
@@ -167,29 +154,27 @@ actual fun ActiveCallView() {
|
|||||||
withBGApi { chatModel.callManager.endCall(call) }
|
withBGApi { chatModel.callManager.endCall(call) }
|
||||||
}
|
}
|
||||||
is WCallResponse.Ended -> {
|
is WCallResponse.Ended -> {
|
||||||
updateActiveCall(call) { it.copy(callState = CallState.Ended) }
|
chatModel.activeCall.value = call.copy(callState = CallState.Ended)
|
||||||
withBGApi { chatModel.callManager.endCall(call) }
|
withBGApi { chatModel.callManager.endCall(call) }
|
||||||
|
chatModel.showCallView.value = false
|
||||||
}
|
}
|
||||||
is WCallResponse.Ok -> when (val cmd = apiMsg.command) {
|
is WCallResponse.Ok -> when (val cmd = apiMsg.command) {
|
||||||
is WCallCommand.Answer ->
|
is WCallCommand.Answer ->
|
||||||
updateActiveCall(call) { it.copy(callState = CallState.Negotiated) }
|
chatModel.activeCall.value = call.copy(callState = CallState.Negotiated)
|
||||||
is WCallCommand.Media -> {
|
is WCallCommand.Media -> {
|
||||||
updateActiveCall(call) {
|
when (cmd.media) {
|
||||||
when (cmd.media) {
|
CallMediaType.Video -> chatModel.activeCall.value = call.copy(videoEnabled = cmd.enable)
|
||||||
CallMediaType.Video -> it.copy(videoEnabled = cmd.enable)
|
CallMediaType.Audio -> chatModel.activeCall.value = call.copy(audioEnabled = cmd.enable)
|
||||||
CallMediaType.Audio -> it.copy(audioEnabled = cmd.enable)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
is WCallCommand.Camera -> {
|
is WCallCommand.Camera -> {
|
||||||
updateActiveCall(call) { it.copy(localCamera = cmd.camera) }
|
chatModel.activeCall.value = call.copy(localCamera = cmd.camera)
|
||||||
if (!call.audioEnabled) {
|
if (!call.audioEnabled) {
|
||||||
chatModel.callCommand.add(WCallCommand.Media(CallMediaType.Audio, enable = false))
|
chatModel.callCommand.add(WCallCommand.Media(CallMediaType.Audio, enable = false))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
is WCallCommand.End -> {
|
is WCallCommand.End ->
|
||||||
withBGApi { chatModel.callManager.endCall(call) }
|
chatModel.showCallView.value = false
|
||||||
}
|
|
||||||
else -> {}
|
else -> {}
|
||||||
}
|
}
|
||||||
is WCallResponse.Error -> {
|
is WCallResponse.Error -> {
|
||||||
@@ -198,16 +183,8 @@ actual fun ActiveCallView() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val showOverlay = when {
|
val call = chatModel.activeCall.value
|
||||||
call == null -> false
|
if (call != null) ActiveCallOverlay(call, chatModel, audioViaBluetooth)
|
||||||
!platform.androidPictureInPictureAllowed() -> true
|
|
||||||
!call.supportsVideo() -> true
|
|
||||||
!chatModel.activeCallViewIsCollapsed.value -> true
|
|
||||||
else -> false
|
|
||||||
}
|
|
||||||
if (call != null && showOverlay) {
|
|
||||||
ActiveCallOverlay(call, chatModel, audioViaBluetooth)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
@@ -252,20 +229,6 @@ private fun ActiveCallOverlay(call: Call, chatModel: ChatModel, audioViaBluetoot
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun ActiveCallOverlayDisabled(call: Call) {
|
|
||||||
ActiveCallOverlayLayout(
|
|
||||||
call = call,
|
|
||||||
speakerCanBeEnabled = false,
|
|
||||||
enabled = false,
|
|
||||||
dismiss = {},
|
|
||||||
toggleAudio = {},
|
|
||||||
toggleVideo = {},
|
|
||||||
toggleSound = {},
|
|
||||||
flipCamera = {}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun setCallSound(speaker: Boolean, audioViaBluetooth: MutableState<Boolean>) {
|
private fun setCallSound(speaker: Boolean, audioViaBluetooth: MutableState<Boolean>) {
|
||||||
val am = androidAppContext.getSystemService(Context.AUDIO_SERVICE) as AudioManager
|
val am = androidAppContext.getSystemService(Context.AUDIO_SERVICE) as AudioManager
|
||||||
Log.d(TAG, "setCallSound: set audio mode, speaker enabled: $speaker")
|
Log.d(TAG, "setCallSound: set audio mode, speaker enabled: $speaker")
|
||||||
@@ -308,69 +271,59 @@ private fun dropAudioManagerOverrides() {
|
|||||||
private fun ActiveCallOverlayLayout(
|
private fun ActiveCallOverlayLayout(
|
||||||
call: Call,
|
call: Call,
|
||||||
speakerCanBeEnabled: Boolean,
|
speakerCanBeEnabled: Boolean,
|
||||||
enabled: Boolean = true,
|
|
||||||
dismiss: () -> Unit,
|
dismiss: () -> Unit,
|
||||||
toggleAudio: () -> Unit,
|
toggleAudio: () -> Unit,
|
||||||
toggleVideo: () -> Unit,
|
toggleVideo: () -> Unit,
|
||||||
toggleSound: () -> Unit,
|
toggleSound: () -> Unit,
|
||||||
flipCamera: () -> Unit
|
flipCamera: () -> Unit
|
||||||
) {
|
) {
|
||||||
Column {
|
Column(Modifier.padding(DEFAULT_PADDING)) {
|
||||||
val media = call.peerMedia ?: call.localMedia
|
when (call.peerMedia ?: call.localMedia) {
|
||||||
CloseSheetBar({ chatModel.activeCallViewIsCollapsed.value = true }, true, tintColor = Color(0xFFFFFFD8)) {
|
CallMediaType.Video -> {
|
||||||
if (media == CallMediaType.Video) {
|
CallInfoView(call, alignment = Alignment.Start)
|
||||||
Text(call.contact.chatViewName, Modifier.fillMaxWidth().padding(end = DEFAULT_PADDING), color = Color(0xFFFFFFD8), style = MaterialTheme.typography.h2, overflow = TextOverflow.Ellipsis, maxLines = 1)
|
Box(Modifier.fillMaxWidth().fillMaxHeight().weight(1f), contentAlignment = Alignment.BottomCenter) {
|
||||||
}
|
DisabledBackgroundCallsButton()
|
||||||
}
|
}
|
||||||
Column(Modifier.padding(horizontal = DEFAULT_PADDING)) {
|
Row(Modifier.fillMaxWidth().padding(horizontal = 6.dp), horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically) {
|
||||||
when (media) {
|
ToggleAudioButton(call, toggleAudio)
|
||||||
CallMediaType.Video -> {
|
Spacer(Modifier.size(40.dp))
|
||||||
VideoCallInfoView(call)
|
IconButton(onClick = dismiss) {
|
||||||
Box(Modifier.fillMaxWidth().fillMaxHeight().weight(1f), contentAlignment = Alignment.BottomCenter) {
|
Icon(painterResource(MR.images.ic_call_end_filled), stringResource(MR.strings.icon_descr_hang_up), tint = Color.Red, modifier = Modifier.size(64.dp))
|
||||||
DisabledBackgroundCallsButton()
|
|
||||||
}
|
}
|
||||||
Row(Modifier.fillMaxWidth().padding(horizontal = 6.dp), horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically) {
|
if (call.videoEnabled) {
|
||||||
ToggleAudioButton(call, enabled, toggleAudio)
|
ControlButton(call, painterResource(MR.images.ic_flip_camera_android_filled), MR.strings.icon_descr_flip_camera, flipCamera)
|
||||||
Spacer(Modifier.size(40.dp))
|
ControlButton(call, painterResource(MR.images.ic_videocam_filled), MR.strings.icon_descr_video_off, toggleVideo)
|
||||||
IconButton(onClick = dismiss, enabled = enabled) {
|
} else {
|
||||||
Icon(painterResource(MR.images.ic_call_end_filled), stringResource(MR.strings.icon_descr_hang_up), tint = if (enabled) Color.Red else MaterialTheme.colors.secondary, modifier = Modifier.size(64.dp))
|
Spacer(Modifier.size(48.dp))
|
||||||
}
|
ControlButton(call, painterResource(MR.images.ic_videocam_off), MR.strings.icon_descr_video_on, toggleVideo)
|
||||||
if (call.videoEnabled) {
|
|
||||||
ControlButton(call, painterResource(MR.images.ic_flip_camera_android_filled), MR.strings.icon_descr_flip_camera, enabled, flipCamera)
|
|
||||||
ControlButton(call, painterResource(MR.images.ic_videocam_filled), MR.strings.icon_descr_video_off, enabled, toggleVideo)
|
|
||||||
} else {
|
|
||||||
Spacer(Modifier.size(48.dp))
|
|
||||||
ControlButton(call, painterResource(MR.images.ic_videocam_off), MR.strings.icon_descr_video_on, enabled, toggleVideo)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
CallMediaType.Audio -> {
|
CallMediaType.Audio -> {
|
||||||
Spacer(Modifier.fillMaxHeight().weight(1f))
|
Spacer(Modifier.fillMaxHeight().weight(1f))
|
||||||
Column(
|
Column(
|
||||||
Modifier.fillMaxWidth(),
|
Modifier.fillMaxWidth(),
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
verticalArrangement = Arrangement.Center
|
verticalArrangement = Arrangement.Center
|
||||||
) {
|
) {
|
||||||
ProfileImage(size = 192.dp, image = call.contact.profile.image)
|
ProfileImage(size = 192.dp, image = call.contact.profile.image)
|
||||||
AudioCallInfoView(call)
|
CallInfoView(call, alignment = Alignment.CenterHorizontally)
|
||||||
}
|
}
|
||||||
Box(Modifier.fillMaxWidth().fillMaxHeight().weight(1f), contentAlignment = Alignment.BottomCenter) {
|
Box(Modifier.fillMaxWidth().fillMaxHeight().weight(1f), contentAlignment = Alignment.BottomCenter) {
|
||||||
DisabledBackgroundCallsButton()
|
DisabledBackgroundCallsButton()
|
||||||
}
|
}
|
||||||
Box(Modifier.fillMaxWidth().padding(bottom = DEFAULT_BOTTOM_PADDING), contentAlignment = Alignment.CenterStart) {
|
Box(Modifier.fillMaxWidth().padding(bottom = DEFAULT_BOTTOM_PADDING), contentAlignment = Alignment.CenterStart) {
|
||||||
Box(Modifier.fillMaxWidth(), contentAlignment = Alignment.Center) {
|
Box(Modifier.fillMaxWidth(), contentAlignment = Alignment.Center) {
|
||||||
IconButton(onClick = dismiss, enabled = enabled) {
|
IconButton(onClick = dismiss) {
|
||||||
Icon(painterResource(MR.images.ic_call_end_filled), stringResource(MR.strings.icon_descr_hang_up), tint = if (enabled) Color.Red else MaterialTheme.colors.secondary, modifier = Modifier.size(64.dp))
|
Icon(painterResource(MR.images.ic_call_end_filled), stringResource(MR.strings.icon_descr_hang_up), tint = Color.Red, modifier = Modifier.size(64.dp))
|
||||||
}
|
|
||||||
}
|
}
|
||||||
Box(Modifier.padding(start = 32.dp)) {
|
}
|
||||||
ToggleAudioButton(call, enabled, toggleAudio)
|
Box(Modifier.padding(start = 32.dp)) {
|
||||||
}
|
ToggleAudioButton(call, toggleAudio)
|
||||||
Box(Modifier.fillMaxWidth(), contentAlignment = Alignment.CenterEnd) {
|
}
|
||||||
Box(Modifier.padding(end = 32.dp)) {
|
Box(Modifier.fillMaxWidth(), contentAlignment = Alignment.CenterEnd) {
|
||||||
ToggleSoundButton(call, speakerCanBeEnabled && enabled, toggleSound)
|
Box(Modifier.padding(end = 32.dp)) {
|
||||||
}
|
ToggleSoundButton(call, speakerCanBeEnabled, toggleSound)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -380,7 +333,7 @@ private fun ActiveCallOverlayLayout(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun ControlButton(call: Call, icon: Painter, iconText: StringResource, enabled: Boolean = true, action: () -> Unit) {
|
private fun ControlButton(call: Call, icon: Painter, iconText: StringResource, action: () -> Unit, enabled: Boolean = true) {
|
||||||
if (call.hasMedia) {
|
if (call.hasMedia) {
|
||||||
IconButton(onClick = action, enabled = enabled) {
|
IconButton(onClick = action, enabled = enabled) {
|
||||||
Icon(icon, stringResource(iconText), tint = if (enabled) Color(0xFFFFFFD8) else MaterialTheme.colors.secondary, modifier = Modifier.size(40.dp))
|
Icon(icon, stringResource(iconText), tint = if (enabled) Color(0xFFFFFFD8) else MaterialTheme.colors.secondary, modifier = Modifier.size(40.dp))
|
||||||
@@ -391,26 +344,28 @@ private fun ControlButton(call: Call, icon: Painter, iconText: StringResource, e
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun ToggleAudioButton(call: Call, enabled: Boolean = true, toggleAudio: () -> Unit) {
|
private fun ToggleAudioButton(call: Call, toggleAudio: () -> Unit) {
|
||||||
if (call.audioEnabled) {
|
if (call.audioEnabled) {
|
||||||
ControlButton(call, painterResource(MR.images.ic_mic), MR.strings.icon_descr_audio_off, enabled, toggleAudio)
|
ControlButton(call, painterResource(MR.images.ic_mic), MR.strings.icon_descr_audio_off, toggleAudio)
|
||||||
} else {
|
} else {
|
||||||
ControlButton(call, painterResource(MR.images.ic_mic_off), MR.strings.icon_descr_audio_on, enabled, toggleAudio)
|
ControlButton(call, painterResource(MR.images.ic_mic_off), MR.strings.icon_descr_audio_on, toggleAudio)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun ToggleSoundButton(call: Call, enabled: Boolean, toggleSound: () -> Unit) {
|
private fun ToggleSoundButton(call: Call, enabled: Boolean, toggleSound: () -> Unit) {
|
||||||
if (call.soundSpeaker) {
|
if (call.soundSpeaker) {
|
||||||
ControlButton(call, painterResource(MR.images.ic_volume_up), MR.strings.icon_descr_speaker_off, enabled, toggleSound)
|
ControlButton(call, painterResource(MR.images.ic_volume_up), MR.strings.icon_descr_speaker_off, toggleSound, enabled)
|
||||||
} else {
|
} else {
|
||||||
ControlButton(call, painterResource(MR.images.ic_volume_down), MR.strings.icon_descr_speaker_on, enabled, toggleSound)
|
ControlButton(call, painterResource(MR.images.ic_volume_down), MR.strings.icon_descr_speaker_on, toggleSound, enabled)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun AudioCallInfoView(call: Call) {
|
fun CallInfoView(call: Call, alignment: Alignment.Horizontal) {
|
||||||
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
@Composable fun InfoText(text: String, style: TextStyle = MaterialTheme.typography.body2) =
|
||||||
|
Text(text, color = Color(0xFFFFFFD8), style = style)
|
||||||
|
Column(horizontalAlignment = alignment) {
|
||||||
InfoText(call.contact.chatViewName, style = MaterialTheme.typography.h2)
|
InfoText(call.contact.chatViewName, style = MaterialTheme.typography.h2)
|
||||||
InfoText(call.callState.text)
|
InfoText(call.callState.text)
|
||||||
|
|
||||||
@@ -420,21 +375,6 @@ fun AudioCallInfoView(call: Call) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun VideoCallInfoView(call: Call) {
|
|
||||||
Column(horizontalAlignment = Alignment.Start) {
|
|
||||||
InfoText(call.callState.text)
|
|
||||||
|
|
||||||
val connInfo = call.connectionInfo
|
|
||||||
val connInfoText = if (connInfo == null) "" else " (${connInfo.text})"
|
|
||||||
InfoText(call.encryptionStatus + connInfoText)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun InfoText(text: String, modifier: Modifier = Modifier, style: TextStyle = MaterialTheme.typography.body2) =
|
|
||||||
Text(text, modifier, color = Color(0xFFFFFFD8), style = style)
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun DisabledBackgroundCallsButton() {
|
private fun DisabledBackgroundCallsButton() {
|
||||||
var show by remember { mutableStateOf(!platform.androidIsBackgroundCallAllowed()) }
|
var show by remember { mutableStateOf(!platform.androidIsBackgroundCallAllowed()) }
|
||||||
@@ -512,6 +452,7 @@ private fun DisabledBackgroundCallsButton() {
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun WebRTCView(callCommand: SnapshotStateList<WCallCommand>, onResponse: (WVAPIMessage) -> Unit) {
|
fun WebRTCView(callCommand: SnapshotStateList<WCallCommand>, onResponse: (WVAPIMessage) -> Unit) {
|
||||||
|
val scope = rememberCoroutineScope()
|
||||||
val webView = remember { mutableStateOf<WebView?>(null) }
|
val webView = remember { mutableStateOf<WebView?>(null) }
|
||||||
val permissionsState = rememberMultiplePermissionsState(
|
val permissionsState = rememberMultiplePermissionsState(
|
||||||
permissions = listOf(
|
permissions = listOf(
|
||||||
@@ -534,10 +475,10 @@ fun WebRTCView(callCommand: SnapshotStateList<WCallCommand>, onResponse: (WVAPIM
|
|||||||
}
|
}
|
||||||
lifecycleOwner.lifecycle.addObserver(observer)
|
lifecycleOwner.lifecycle.addObserver(observer)
|
||||||
onDispose {
|
onDispose {
|
||||||
|
val wv = webView.value
|
||||||
|
if (wv != null) processCommand(wv, WCallCommand.End)
|
||||||
lifecycleOwner.lifecycle.removeObserver(observer)
|
lifecycleOwner.lifecycle.removeObserver(observer)
|
||||||
// val wv = webView.value
|
webView.value?.destroy()
|
||||||
// if (wv != null) processCommand(wv, WCallCommand.End)
|
|
||||||
// webView.value?.destroy()
|
|
||||||
webView.value = null
|
webView.value = null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -564,7 +505,7 @@ fun WebRTCView(callCommand: SnapshotStateList<WCallCommand>, onResponse: (WVAPIM
|
|||||||
Box(Modifier.fillMaxSize()) {
|
Box(Modifier.fillMaxSize()) {
|
||||||
AndroidView(
|
AndroidView(
|
||||||
factory = { AndroidViewContext ->
|
factory = { AndroidViewContext ->
|
||||||
(staticWebView ?: WebView(androidAppContext)).apply {
|
WebView(AndroidViewContext).apply {
|
||||||
layoutParams = ViewGroup.LayoutParams(
|
layoutParams = ViewGroup.LayoutParams(
|
||||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||||
@@ -589,11 +530,7 @@ fun WebRTCView(callCommand: SnapshotStateList<WCallCommand>, onResponse: (WVAPIM
|
|||||||
webViewSettings.javaScriptEnabled = true
|
webViewSettings.javaScriptEnabled = true
|
||||||
webViewSettings.mediaPlaybackRequiresUserGesture = false
|
webViewSettings.mediaPlaybackRequiresUserGesture = false
|
||||||
webViewSettings.cacheMode = WebSettings.LOAD_NO_CACHE
|
webViewSettings.cacheMode = WebSettings.LOAD_NO_CACHE
|
||||||
if (staticWebView == null) {
|
this.loadUrl("file:android_asset/www/android/call.html")
|
||||||
this.loadUrl("file:android_asset/www/android/call.html")
|
|
||||||
} else {
|
|
||||||
webView.value = this
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
) { /* WebView */ }
|
) { /* WebView */ }
|
||||||
@@ -617,15 +554,6 @@ class WebRTCInterface(private val onResponse: (WVAPIMessage) -> Unit) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updateActiveCall(initial: Call, transform: (Call) -> Call) {
|
|
||||||
val activeCall = chatModel.activeCall.value
|
|
||||||
if (activeCall != null && activeCall.contact.apiId == initial.contact.apiId) {
|
|
||||||
chatModel.activeCall.value = transform(activeCall)
|
|
||||||
} else {
|
|
||||||
Log.d(TAG, "withActiveCall: ignoring, not in call with the contact ${activeCall?.contact?.id}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private class LocalContentWebViewClient(val webView: MutableState<WebView?>, private val assetLoader: WebViewAssetLoader) : WebViewClientCompat() {
|
private class LocalContentWebViewClient(val webView: MutableState<WebView?>, private val assetLoader: WebViewAssetLoader) : WebViewClientCompat() {
|
||||||
override fun shouldInterceptRequest(
|
override fun shouldInterceptRequest(
|
||||||
view: WebView,
|
view: WebView,
|
||||||
@@ -638,7 +566,6 @@ private class LocalContentWebViewClient(val webView: MutableState<WebView?>, pri
|
|||||||
super.onPageFinished(view, url)
|
super.onPageFinished(view, url)
|
||||||
view.evaluateJavascript("sendMessageToNative = (msg) => WebRTCInterface.postMessage(JSON.stringify(msg))", null)
|
view.evaluateJavascript("sendMessageToNative = (msg) => WebRTCInterface.postMessage(JSON.stringify(msg))", null)
|
||||||
webView.value = view
|
webView.value = view
|
||||||
staticWebView = view
|
|
||||||
Log.d(TAG, "WebRTCView: webview ready")
|
Log.d(TAG, "WebRTCView: webview ready")
|
||||||
// for debugging
|
// for debugging
|
||||||
// view.evaluateJavascript("sendMessageToNative = ({resp}) => WebRTCInterface.postMessage(JSON.stringify({command: resp}))", null)
|
// view.evaluateJavascript("sendMessageToNative = ({resp}) => WebRTCInterface.postMessage(JSON.stringify({command: resp}))", null)
|
||||||
@@ -652,7 +579,6 @@ fun PreviewActiveCallOverlayVideo() {
|
|||||||
ActiveCallOverlayLayout(
|
ActiveCallOverlayLayout(
|
||||||
call = Call(
|
call = Call(
|
||||||
remoteHostId = null,
|
remoteHostId = null,
|
||||||
userProfile = Profile.sampleData,
|
|
||||||
contact = Contact.sampleData,
|
contact = Contact.sampleData,
|
||||||
callState = CallState.Negotiated,
|
callState = CallState.Negotiated,
|
||||||
localMedia = CallMediaType.Video,
|
localMedia = CallMediaType.Video,
|
||||||
@@ -679,7 +605,6 @@ fun PreviewActiveCallOverlayAudio() {
|
|||||||
ActiveCallOverlayLayout(
|
ActiveCallOverlayLayout(
|
||||||
call = Call(
|
call = Call(
|
||||||
remoteHostId = null,
|
remoteHostId = null,
|
||||||
userProfile = Profile.sampleData,
|
|
||||||
contact = Contact.sampleData,
|
contact = Contact.sampleData,
|
||||||
callState = CallState.Negotiated,
|
callState = CallState.Negotiated,
|
||||||
localMedia = CallMediaType.Audio,
|
localMedia = CallMediaType.Audio,
|
||||||
|
|||||||
+1
-105
@@ -1,112 +1,8 @@
|
|||||||
package chat.simplex.common.views.chatlist
|
package chat.simplex.common.views.chatlist
|
||||||
|
|
||||||
import android.app.Activity
|
|
||||||
import androidx.compose.foundation.*
|
|
||||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
|
||||||
import androidx.compose.foundation.layout.*
|
|
||||||
import androidx.compose.foundation.shape.CircleShape
|
|
||||||
import androidx.compose.material.*
|
|
||||||
import androidx.compose.material.ripple.rememberRipple
|
|
||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.*
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.draw.clip
|
|
||||||
import androidx.compose.ui.graphics.Color
|
|
||||||
import androidx.compose.ui.graphics.toArgb
|
|
||||||
import androidx.compose.ui.platform.*
|
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import androidx.compose.ui.unit.sp
|
|
||||||
import chat.simplex.common.ANDROID_CALL_TOP_PADDING
|
|
||||||
import chat.simplex.common.model.durationText
|
|
||||||
import chat.simplex.common.platform.*
|
|
||||||
import chat.simplex.common.ui.theme.*
|
|
||||||
import chat.simplex.common.views.call.*
|
|
||||||
import chat.simplex.common.views.helpers.*
|
import chat.simplex.common.views.helpers.*
|
||||||
import chat.simplex.res.MR
|
|
||||||
import dev.icerock.moko.resources.compose.painterResource
|
|
||||||
import kotlinx.coroutines.delay
|
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.datetime.Clock
|
|
||||||
|
|
||||||
private val CALL_INTERACTIVE_AREA_HEIGHT = 74.dp
|
|
||||||
private val CALL_TOP_OFFSET = (-10).dp
|
|
||||||
private val CALL_TOP_GREEN_LINE_HEIGHT = ANDROID_CALL_TOP_PADDING - CALL_TOP_OFFSET
|
|
||||||
private val CALL_BOTTOM_ICON_OFFSET = (-15).dp
|
|
||||||
private val CALL_BOTTOM_ICON_HEIGHT = CALL_INTERACTIVE_AREA_HEIGHT + CALL_BOTTOM_ICON_OFFSET
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
actual fun ActiveCallInteractiveArea(call: Call, newChatSheetState: MutableStateFlow<AnimatedViewState>) {
|
actual fun DesktopActiveCallOverlayLayout(newChatSheetState: MutableStateFlow<AnimatedViewState>) {}
|
||||||
val onClick = { platform.androidStartCallActivity(false) }
|
|
||||||
Box(Modifier.offset(y = CALL_TOP_OFFSET).height(CALL_INTERACTIVE_AREA_HEIGHT)) {
|
|
||||||
val source = remember { MutableInteractionSource() }
|
|
||||||
val indication = rememberRipple(bounded = true, 3000.dp)
|
|
||||||
Box(Modifier.height(CALL_TOP_GREEN_LINE_HEIGHT).clickable(onClick = onClick, indication = indication, interactionSource = source)) {
|
|
||||||
GreenLine(call)
|
|
||||||
}
|
|
||||||
Box(
|
|
||||||
Modifier
|
|
||||||
.offset(y = CALL_BOTTOM_ICON_OFFSET)
|
|
||||||
.size(CALL_BOTTOM_ICON_HEIGHT)
|
|
||||||
.background(SimplexGreen, CircleShape)
|
|
||||||
.clip(CircleShape)
|
|
||||||
.clickable(onClick = onClick, indication = indication, interactionSource = source)
|
|
||||||
.align(Alignment.BottomCenter),
|
|
||||||
contentAlignment = Alignment.Center
|
|
||||||
) {
|
|
||||||
val media = call.peerMedia ?: call.localMedia
|
|
||||||
if (media == CallMediaType.Video) {
|
|
||||||
Icon(painterResource(MR.images.ic_videocam_filled), null, Modifier.size(27.dp).offset(x = 2.5.dp, y = 2.dp), tint = Color.White)
|
|
||||||
} else {
|
|
||||||
Icon(painterResource(MR.images.ic_call_filled), null, Modifier.size(27.dp).offset(x = -0.5.dp, y = 2.dp), tint = Color.White)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun GreenLine(call: Call) {
|
|
||||||
Row(
|
|
||||||
Modifier
|
|
||||||
.fillMaxSize()
|
|
||||||
.background(SimplexGreen)
|
|
||||||
.padding(top = -CALL_TOP_OFFSET)
|
|
||||||
.padding(horizontal = DEFAULT_PADDING),
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
horizontalArrangement = Arrangement.Center
|
|
||||||
) {
|
|
||||||
ContactName(call.contact.displayName)
|
|
||||||
Spacer(Modifier.weight(1f))
|
|
||||||
CallDuration(call)
|
|
||||||
}
|
|
||||||
val window = (LocalContext.current as Activity).window
|
|
||||||
DisposableEffect(Unit) {
|
|
||||||
window.statusBarColor = SimplexGreen.toArgb()
|
|
||||||
onDispose {
|
|
||||||
window.statusBarColor = Color.Black.toArgb()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun ContactName(name: String) {
|
|
||||||
Text(name, Modifier.width(windowWidth() * 0.35f), color = Color.White, maxLines = 1, overflow = TextOverflow.Ellipsis)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun CallDuration(call: Call) {
|
|
||||||
val connectedAt = call.connectedAt
|
|
||||||
if (connectedAt != null) {
|
|
||||||
val time = remember { mutableStateOf(durationText(0)) }
|
|
||||||
LaunchedEffect(Unit) {
|
|
||||||
while (true) {
|
|
||||||
time.value = durationText((Clock.System.now() - connectedAt).inWholeSeconds.toInt())
|
|
||||||
delay(250)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
val text = time.value
|
|
||||||
val sp40Or50 = with(LocalDensity.current) { if (text.length >= 6) 60.sp.toDp() else 42.sp.toDp() }
|
|
||||||
val offset = with(LocalDensity.current) { 7.sp.toDp() }
|
|
||||||
Text(text, Modifier.offset(x = offset).widthIn(min = sp40Or50), color = Color.White)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
-106
@@ -1,106 +0,0 @@
|
|||||||
package chat.simplex.common.views.helpers
|
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.*
|
|
||||||
import androidx.compose.material.MaterialTheme
|
|
||||||
import androidx.compose.material.Text
|
|
||||||
import androidx.compose.runtime.*
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import androidx.compose.ui.unit.sp
|
|
||||||
import chat.simplex.common.model.CustomTimeUnit
|
|
||||||
import chat.simplex.common.ui.theme.DEFAULT_PADDING
|
|
||||||
import com.sd.lib.compose.wheel_picker.*
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
actual fun CustomTimePicker(
|
|
||||||
selection: MutableState<Int>,
|
|
||||||
timeUnitsLimits: List<TimeUnitLimits>
|
|
||||||
) {
|
|
||||||
fun getUnitValues(unit: CustomTimeUnit, selectedValue: Int): List<Int> {
|
|
||||||
val unitLimits = timeUnitsLimits.firstOrNull { it.timeUnit == unit } ?: TimeUnitLimits.defaultUnitLimits(unit)
|
|
||||||
val regularUnitValues = (unitLimits.minValue..unitLimits.maxValue).toList()
|
|
||||||
return regularUnitValues + if (regularUnitValues.contains(selectedValue)) emptyList() else listOf(selectedValue)
|
|
||||||
}
|
|
||||||
|
|
||||||
val (unit, duration) = CustomTimeUnit.toTimeUnit(selection.value)
|
|
||||||
val selectedUnit: MutableState<CustomTimeUnit> = remember { mutableStateOf(unit) }
|
|
||||||
val selectedDuration = remember { mutableStateOf(duration) }
|
|
||||||
val selectedUnitValues = remember { mutableStateOf(getUnitValues(selectedUnit.value, selectedDuration.value)) }
|
|
||||||
val isTriggered = remember { mutableStateOf(false) }
|
|
||||||
|
|
||||||
LaunchedEffect(selectedUnit.value) {
|
|
||||||
// on initial composition, if passed selection doesn't fit into picker bounds, so that selectedDuration is bigger than selectedUnit maxValue
|
|
||||||
// (e.g., for selection = 121 seconds: selectedUnit would be Second, selectedDuration would be 121 > selectedUnit maxValue of 120),
|
|
||||||
// selectedDuration would've been replaced by maxValue - isTriggered check prevents this by skipping LaunchedEffect on initial composition
|
|
||||||
if (isTriggered.value) {
|
|
||||||
val maxValue = timeUnitsLimits.firstOrNull { it.timeUnit == selectedUnit.value }?.maxValue
|
|
||||||
if (maxValue != null && selectedDuration.value > maxValue) {
|
|
||||||
selectedDuration.value = maxValue
|
|
||||||
selectedUnitValues.value = getUnitValues(selectedUnit.value, selectedDuration.value)
|
|
||||||
} else {
|
|
||||||
selectedUnitValues.value = getUnitValues(selectedUnit.value, selectedDuration.value)
|
|
||||||
selection.value = selectedUnit.value.toSeconds * selectedDuration.value
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
isTriggered.value = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
LaunchedEffect(selectedDuration.value) {
|
|
||||||
selection.value = selectedUnit.value.toSeconds * selectedDuration.value
|
|
||||||
}
|
|
||||||
|
|
||||||
Row(
|
|
||||||
Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(horizontal = DEFAULT_PADDING),
|
|
||||||
horizontalArrangement = Arrangement.spacedBy(0.dp)
|
|
||||||
) {
|
|
||||||
Column(Modifier.weight(1f)) {
|
|
||||||
val durationPickerState = rememberFWheelPickerState(selectedUnitValues.value.indexOf(selectedDuration.value))
|
|
||||||
FVerticalWheelPicker(
|
|
||||||
count = selectedUnitValues.value.count(),
|
|
||||||
state = durationPickerState,
|
|
||||||
unfocusedCount = 2,
|
|
||||||
focus = {
|
|
||||||
FWheelPickerFocusVertical(dividerColor = MaterialTheme.colors.primary)
|
|
||||||
}
|
|
||||||
) { index ->
|
|
||||||
Text(
|
|
||||||
selectedUnitValues.value[index].toString(),
|
|
||||||
fontSize = 18.sp,
|
|
||||||
color = MaterialTheme.colors.primary
|
|
||||||
)
|
|
||||||
}
|
|
||||||
LaunchedEffect(durationPickerState) {
|
|
||||||
snapshotFlow { durationPickerState.currentIndex }
|
|
||||||
.collect {
|
|
||||||
selectedDuration.value = selectedUnitValues.value[it]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Column(Modifier.weight(1f)) {
|
|
||||||
val unitPickerState = rememberFWheelPickerState(timeUnitsLimits.indexOfFirst { it.timeUnit == selectedUnit.value })
|
|
||||||
FVerticalWheelPicker(
|
|
||||||
count = timeUnitsLimits.count(),
|
|
||||||
state = unitPickerState,
|
|
||||||
unfocusedCount = 2,
|
|
||||||
focus = {
|
|
||||||
FWheelPickerFocusVertical(dividerColor = MaterialTheme.colors.primary)
|
|
||||||
}
|
|
||||||
) { index ->
|
|
||||||
Text(
|
|
||||||
timeUnitsLimits[index].timeUnit.text,
|
|
||||||
fontSize = 18.sp,
|
|
||||||
color = MaterialTheme.colors.primary
|
|
||||||
)
|
|
||||||
}
|
|
||||||
LaunchedEffect(unitPickerState) {
|
|
||||||
snapshotFlow { unitPickerState.currentIndex }
|
|
||||||
.collect {
|
|
||||||
selectedUnit.value = timeUnitsLimits[it].timeUnit
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -66,7 +66,6 @@ extern char *chat_parse_markdown(const char *str);
|
|||||||
extern char *chat_parse_server(const char *str);
|
extern char *chat_parse_server(const char *str);
|
||||||
extern char *chat_password_hash(const char *pwd, const char *salt);
|
extern char *chat_password_hash(const char *pwd, const char *salt);
|
||||||
extern char *chat_valid_name(const char *name);
|
extern char *chat_valid_name(const char *name);
|
||||||
extern int chat_json_length(const char *str);
|
|
||||||
extern char *chat_write_file(chat_ctrl ctrl, const char *path, char *ptr, int length);
|
extern char *chat_write_file(chat_ctrl ctrl, const char *path, char *ptr, int length);
|
||||||
extern char *chat_read_file(const char *path, const char *key, const char *nonce);
|
extern char *chat_read_file(const char *path, const char *key, const char *nonce);
|
||||||
extern char *chat_encrypt_file(chat_ctrl ctrl, const char *from_path, const char *to_path);
|
extern char *chat_encrypt_file(chat_ctrl ctrl, const char *from_path, const char *to_path);
|
||||||
@@ -164,14 +163,6 @@ Java_chat_simplex_common_platform_CoreKt_chatValidName(JNIEnv *env, jclass clazz
|
|||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
JNIEXPORT int JNICALL
|
|
||||||
Java_chat_simplex_common_platform_CoreKt_chatJsonLength(JNIEnv *env, jclass clazz, jstring str) {
|
|
||||||
const char *_str = (*env)->GetStringUTFChars(env, str, JNI_FALSE);
|
|
||||||
int res = chat_json_length(_str);
|
|
||||||
(*env)->ReleaseStringUTFChars(env, str, _str);
|
|
||||||
return res;
|
|
||||||
}
|
|
||||||
|
|
||||||
JNIEXPORT jstring JNICALL
|
JNIEXPORT jstring JNICALL
|
||||||
Java_chat_simplex_common_platform_CoreKt_chatWriteFile(JNIEnv *env, jclass clazz, jlong controller, jstring path, jobject buffer) {
|
Java_chat_simplex_common_platform_CoreKt_chatWriteFile(JNIEnv *env, jclass clazz, jlong controller, jstring path, jobject buffer) {
|
||||||
const char *_path = (*env)->GetStringUTFChars(env, path, JNI_FALSE);
|
const char *_path = (*env)->GetStringUTFChars(env, path, JNI_FALSE);
|
||||||
|
|||||||
@@ -39,7 +39,6 @@ extern char *chat_parse_markdown(const char *str);
|
|||||||
extern char *chat_parse_server(const char *str);
|
extern char *chat_parse_server(const char *str);
|
||||||
extern char *chat_password_hash(const char *pwd, const char *salt);
|
extern char *chat_password_hash(const char *pwd, const char *salt);
|
||||||
extern char *chat_valid_name(const char *name);
|
extern char *chat_valid_name(const char *name);
|
||||||
extern int chat_json_length(const char *str);
|
|
||||||
extern char *chat_write_file(chat_ctrl ctrl, const char *path, char *ptr, int length);
|
extern char *chat_write_file(chat_ctrl ctrl, const char *path, char *ptr, int length);
|
||||||
extern char *chat_read_file(const char *path, const char *key, const char *nonce);
|
extern char *chat_read_file(const char *path, const char *key, const char *nonce);
|
||||||
extern char *chat_encrypt_file(chat_ctrl ctrl, const char *from_path, const char *to_path);
|
extern char *chat_encrypt_file(chat_ctrl ctrl, const char *from_path, const char *to_path);
|
||||||
@@ -174,14 +173,6 @@ Java_chat_simplex_common_platform_CoreKt_chatValidName(JNIEnv *env, jclass clazz
|
|||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
JNIEXPORT int JNICALL
|
|
||||||
Java_chat_simplex_common_platform_CoreKt_chatJsonLength(JNIEnv *env, jclass clazz, jstring str) {
|
|
||||||
const char *_str = encode_to_utf8_chars(env, str);
|
|
||||||
int res = chat_json_length(_str);
|
|
||||||
(*env)->ReleaseStringUTFChars(env, str, _str);
|
|
||||||
return res;
|
|
||||||
}
|
|
||||||
|
|
||||||
JNIEXPORT jstring JNICALL
|
JNIEXPORT jstring JNICALL
|
||||||
Java_chat_simplex_common_platform_CoreKt_chatWriteFile(JNIEnv *env, jclass clazz, jlong controller, jstring path, jobject buffer) {
|
Java_chat_simplex_common_platform_CoreKt_chatWriteFile(JNIEnv *env, jclass clazz, jlong controller, jstring path, jobject buffer) {
|
||||||
const char *_path = encode_to_utf8_chars(env, path);
|
const char *_path = encode_to_utf8_chars(env, path);
|
||||||
|
|||||||
@@ -1,19 +1,16 @@
|
|||||||
package chat.simplex.common
|
package chat.simplex.common
|
||||||
|
|
||||||
import androidx.compose.animation.core.Animatable
|
import androidx.compose.animation.core.Animatable
|
||||||
import androidx.compose.foundation.*
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
import androidx.compose.foundation.shape.CircleShape
|
|
||||||
import androidx.compose.material.*
|
import androidx.compose.material.*
|
||||||
import androidx.compose.material.ripple.rememberRipple
|
|
||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.*
|
||||||
import androidx.compose.runtime.saveable.rememberSaveable
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
|
||||||
import androidx.compose.ui.draw.clipToBounds
|
import androidx.compose.ui.draw.clipToBounds
|
||||||
import androidx.compose.ui.graphics.Color
|
|
||||||
import androidx.compose.ui.graphics.graphicsLayer
|
import androidx.compose.ui.graphics.graphicsLayer
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import chat.simplex.common.views.usersettings.SetDeliveryReceiptsView
|
import chat.simplex.common.views.usersettings.SetDeliveryReceiptsView
|
||||||
@@ -23,7 +20,8 @@ import chat.simplex.common.ui.theme.*
|
|||||||
import chat.simplex.common.views.CreateFirstProfile
|
import chat.simplex.common.views.CreateFirstProfile
|
||||||
import chat.simplex.common.views.helpers.SimpleButton
|
import chat.simplex.common.views.helpers.SimpleButton
|
||||||
import chat.simplex.common.views.SplashView
|
import chat.simplex.common.views.SplashView
|
||||||
import chat.simplex.common.views.call.*
|
import chat.simplex.common.views.call.ActiveCallView
|
||||||
|
import chat.simplex.common.views.call.IncomingCallAlertView
|
||||||
import chat.simplex.common.views.chat.ChatView
|
import chat.simplex.common.views.chat.ChatView
|
||||||
import chat.simplex.common.views.chatlist.*
|
import chat.simplex.common.views.chatlist.*
|
||||||
import chat.simplex.common.views.database.DatabaseErrorView
|
import chat.simplex.common.views.database.DatabaseErrorView
|
||||||
@@ -110,7 +108,6 @@ fun MainScreen() {
|
|||||||
val localUserCreated = chatModel.localUserCreated.value
|
val localUserCreated = chatModel.localUserCreated.value
|
||||||
var showInitializationView by remember { mutableStateOf(false) }
|
var showInitializationView by remember { mutableStateOf(false) }
|
||||||
when {
|
when {
|
||||||
chatModel.dbMigrationInProgress.value -> DefaultProgressView(stringResource(MR.strings.database_migration_in_progress))
|
|
||||||
chatModel.chatDbStatus.value == null && showInitializationView -> DefaultProgressView(stringResource(MR.strings.opening_database))
|
chatModel.chatDbStatus.value == null && showInitializationView -> DefaultProgressView(stringResource(MR.strings.opening_database))
|
||||||
showChatDatabaseError -> {
|
showChatDatabaseError -> {
|
||||||
// Prevent showing keyboard on Android when: passcode enabled and database password not saved
|
// Prevent showing keyboard on Android when: passcode enabled and database password not saved
|
||||||
@@ -171,17 +168,7 @@ fun MainScreen() {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (chatModel.showCallView.value) {
|
if (chatModel.showCallView.value) {
|
||||||
if (appPlatform.isAndroid) {
|
ActiveCallView()
|
||||||
LaunchedEffect(Unit) {
|
|
||||||
// This if prevents running the activity in the following condition:
|
|
||||||
// - the activity already started before and was destroyed by collapsing active call (start audio call, press back button, go to a launcher)
|
|
||||||
if (!chatModel.activeCallViewIsCollapsed.value) {
|
|
||||||
platform.androidStartCallActivity(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
ActiveCallView()
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
// It's needed for privacy settings toggle, so it can be shown even if the app is passcode unlocked
|
// It's needed for privacy settings toggle, so it can be shown even if the app is passcode unlocked
|
||||||
ModalManager.fullscreen.showPasscodeInView()
|
ModalManager.fullscreen.showPasscodeInView()
|
||||||
@@ -218,13 +205,9 @@ fun MainScreen() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val ANDROID_CALL_TOP_PADDING = 40.dp
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun AndroidScreen(settingsState: SettingsViewState) {
|
fun AndroidScreen(settingsState: SettingsViewState) {
|
||||||
BoxWithConstraints {
|
BoxWithConstraints {
|
||||||
val call = remember { chatModel.activeCall} .value
|
|
||||||
val showCallArea = call != null && call.callState != CallState.WaitCapabilities && call.callState != CallState.InvitationAccepted
|
|
||||||
var currentChatId by rememberSaveable { mutableStateOf(chatModel.chatId.value) }
|
var currentChatId by rememberSaveable { mutableStateOf(chatModel.chatId.value) }
|
||||||
val offset = remember { Animatable(if (chatModel.chatId.value == null) 0f else maxWidth.value) }
|
val offset = remember { Animatable(if (chatModel.chatId.value == null) 0f else maxWidth.value) }
|
||||||
Box(
|
Box(
|
||||||
@@ -232,7 +215,6 @@ fun AndroidScreen(settingsState: SettingsViewState) {
|
|||||||
.graphicsLayer {
|
.graphicsLayer {
|
||||||
translationX = -offset.value.dp.toPx()
|
translationX = -offset.value.dp.toPx()
|
||||||
}
|
}
|
||||||
.padding(top = if (showCallArea) ANDROID_CALL_TOP_PADDING else 0.dp)
|
|
||||||
) {
|
) {
|
||||||
StartPartOfScreen(settingsState)
|
StartPartOfScreen(settingsState)
|
||||||
}
|
}
|
||||||
@@ -259,17 +241,11 @@ fun AndroidScreen(settingsState: SettingsViewState) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Box(Modifier
|
Box(Modifier.graphicsLayer { translationX = maxWidth.toPx() - offset.value.dp.toPx() }) Box2@{
|
||||||
.graphicsLayer { translationX = maxWidth.toPx() - offset.value.dp.toPx() }
|
|
||||||
.padding(top = if (showCallArea) ANDROID_CALL_TOP_PADDING else 0.dp)
|
|
||||||
) Box2@{
|
|
||||||
currentChatId?.let {
|
currentChatId?.let {
|
||||||
ChatView(it, chatModel, onComposed)
|
ChatView(it, chatModel, onComposed)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (call != null && showCallArea) {
|
|
||||||
ActiveCallInteractiveArea(call, remember { MutableStateFlow(AnimatedViewState.GONE) })
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+33
-86
@@ -2,7 +2,6 @@ package chat.simplex.common.model
|
|||||||
|
|
||||||
import androidx.compose.material.*
|
import androidx.compose.material.*
|
||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.*
|
||||||
import androidx.compose.runtime.snapshots.SnapshotStateList
|
|
||||||
import androidx.compose.runtime.snapshots.SnapshotStateMap
|
import androidx.compose.runtime.snapshots.SnapshotStateMap
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.text.SpanStyle
|
import androidx.compose.ui.text.SpanStyle
|
||||||
@@ -49,7 +48,6 @@ object ChatModel {
|
|||||||
val chatDbEncrypted = mutableStateOf<Boolean?>(false)
|
val chatDbEncrypted = mutableStateOf<Boolean?>(false)
|
||||||
val chatDbStatus = mutableStateOf<DBMigrationResult?>(null)
|
val chatDbStatus = mutableStateOf<DBMigrationResult?>(null)
|
||||||
val ctrlInitInProgress = mutableStateOf(false)
|
val ctrlInitInProgress = mutableStateOf(false)
|
||||||
val dbMigrationInProgress = mutableStateOf(false)
|
|
||||||
val chats = mutableStateListOf<Chat>()
|
val chats = mutableStateListOf<Chat>()
|
||||||
// map of connections network statuses, key is agent connection id
|
// map of connections network statuses, key is agent connection id
|
||||||
val networkStatuses = mutableStateMapOf<String, NetworkStatus>()
|
val networkStatuses = mutableStateMapOf<String, NetworkStatus>()
|
||||||
@@ -57,7 +55,7 @@ object ChatModel {
|
|||||||
|
|
||||||
// current chat
|
// current chat
|
||||||
val chatId = mutableStateOf<String?>(null)
|
val chatId = mutableStateOf<String?>(null)
|
||||||
val chatItems = mutableStateOf(SnapshotStateList<ChatItem>())
|
val chatItems = mutableStateListOf<ChatItem>()
|
||||||
// rhId, chatId
|
// rhId, chatId
|
||||||
val deletedChats = mutableStateOf<List<Pair<Long?, String>>>(emptyList())
|
val deletedChats = mutableStateOf<List<Pair<Long?, String>>>(emptyList())
|
||||||
val chatItemStatuses = mutableMapOf<Long, CIStatus>()
|
val chatItemStatuses = mutableMapOf<Long, CIStatus>()
|
||||||
@@ -65,6 +63,8 @@ object ChatModel {
|
|||||||
|
|
||||||
val terminalItems = mutableStateOf<List<TerminalItem>>(listOf())
|
val terminalItems = mutableStateOf<List<TerminalItem>>(listOf())
|
||||||
val userAddress = mutableStateOf<UserContactLinkRec?>(null)
|
val userAddress = mutableStateOf<UserContactLinkRec?>(null)
|
||||||
|
// Allows to temporary save servers that are being edited on multiple screens
|
||||||
|
val userSMPServersUnsaved = mutableStateOf<(List<ServerCfg>)?>(null)
|
||||||
val chatItemTTL = mutableStateOf<ChatItemTTL>(ChatItemTTL.None)
|
val chatItemTTL = mutableStateOf<ChatItemTTL>(ChatItemTTL.None)
|
||||||
|
|
||||||
// set when app opened from external intent
|
// set when app opened from external intent
|
||||||
@@ -96,7 +96,6 @@ object ChatModel {
|
|||||||
val activeCallInvitation = mutableStateOf<RcvCallInvitation?>(null)
|
val activeCallInvitation = mutableStateOf<RcvCallInvitation?>(null)
|
||||||
val activeCall = mutableStateOf<Call?>(null)
|
val activeCall = mutableStateOf<Call?>(null)
|
||||||
val activeCallViewIsVisible = mutableStateOf<Boolean>(false)
|
val activeCallViewIsVisible = mutableStateOf<Boolean>(false)
|
||||||
val activeCallViewIsCollapsed = mutableStateOf<Boolean>(false)
|
|
||||||
val callCommand = mutableStateListOf<WCallCommand>()
|
val callCommand = mutableStateListOf<WCallCommand>()
|
||||||
val showCallView = mutableStateOf(false)
|
val showCallView = mutableStateOf(false)
|
||||||
val switchingCall = mutableStateOf(false)
|
val switchingCall = mutableStateOf(false)
|
||||||
@@ -270,15 +269,18 @@ object ChatModel {
|
|||||||
} else {
|
} else {
|
||||||
addChat(Chat(remoteHostId = rhId, chatInfo = cInfo, chatItems = arrayListOf(cItem)))
|
addChat(Chat(remoteHostId = rhId, chatInfo = cInfo, chatItems = arrayListOf(cItem)))
|
||||||
}
|
}
|
||||||
|
Log.d(TAG, "TODOCHAT: addChatItem: adding to chat ${chatId.value} from ${cInfo.id} ${cItem.id}, size ${chatItems.size}")
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
// add to current chat
|
// add to current chat
|
||||||
if (chatId.value == cInfo.id) {
|
if (chatId.value == cInfo.id) {
|
||||||
|
Log.d(TAG, "TODOCHAT: addChatItem: chatIds are equal, size ${chatItems.size}")
|
||||||
// Prevent situation when chat item already in the list received from backend
|
// Prevent situation when chat item already in the list received from backend
|
||||||
if (chatItems.value.none { it.id == cItem.id }) {
|
if (chatItems.none { it.id == cItem.id }) {
|
||||||
if (chatItems.value.lastOrNull()?.id == ChatItem.TEMP_LIVE_CHAT_ITEM_ID) {
|
if (chatItems.lastOrNull()?.id == ChatItem.TEMP_LIVE_CHAT_ITEM_ID) {
|
||||||
chatItems.add(kotlin.math.max(0, chatItems.value.lastIndex), cItem)
|
chatItems.add(kotlin.math.max(0, chatItems.lastIndex), cItem)
|
||||||
} else {
|
} else {
|
||||||
chatItems.add(cItem)
|
chatItems.add(cItem)
|
||||||
|
Log.d(TAG, "TODOCHAT: addChatItem: added to chat ${chatId.value} from ${cInfo.id} ${cItem.id}, size ${chatItems.size}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -305,13 +307,14 @@ object ChatModel {
|
|||||||
addChat(Chat(remoteHostId = rhId, chatInfo = cInfo, chatItems = arrayListOf(cItem)))
|
addChat(Chat(remoteHostId = rhId, chatInfo = cInfo, chatItems = arrayListOf(cItem)))
|
||||||
res = true
|
res = true
|
||||||
}
|
}
|
||||||
|
Log.d(TAG, "TODOCHAT: upsertChatItem: upserting to chat ${chatId.value} from ${cInfo.id} ${cItem.id}, size ${chatItems.size}")
|
||||||
return withContext(Dispatchers.Main) {
|
return withContext(Dispatchers.Main) {
|
||||||
// update current chat
|
// update current chat
|
||||||
if (chatId.value == cInfo.id) {
|
if (chatId.value == cInfo.id) {
|
||||||
val items = chatItems.value
|
val itemIndex = chatItems.indexOfFirst { it.id == cItem.id }
|
||||||
val itemIndex = items.indexOfFirst { it.id == cItem.id }
|
|
||||||
if (itemIndex >= 0) {
|
if (itemIndex >= 0) {
|
||||||
items[itemIndex] = cItem
|
chatItems[itemIndex] = cItem
|
||||||
|
Log.d(TAG, "TODOCHAT: upsertChatItem: updated in chat $chatId from ${cInfo.id} ${cItem.id}, size ${chatItems.size}")
|
||||||
false
|
false
|
||||||
} else {
|
} else {
|
||||||
val status = chatItemStatuses.remove(cItem.id)
|
val status = chatItemStatuses.remove(cItem.id)
|
||||||
@@ -321,6 +324,7 @@ object ChatModel {
|
|||||||
cItem
|
cItem
|
||||||
}
|
}
|
||||||
chatItems.add(ci)
|
chatItems.add(ci)
|
||||||
|
Log.d(TAG, "TODOCHAT: upsertChatItem: added to chat $chatId from ${cInfo.id} ${cItem.id}, size ${chatItems.size}")
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -332,10 +336,9 @@ object ChatModel {
|
|||||||
suspend fun updateChatItem(cInfo: ChatInfo, cItem: ChatItem, status: CIStatus? = null) {
|
suspend fun updateChatItem(cInfo: ChatInfo, cItem: ChatItem, status: CIStatus? = null) {
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
if (chatId.value == cInfo.id) {
|
if (chatId.value == cInfo.id) {
|
||||||
val items = chatItems.value
|
val itemIndex = chatItems.indexOfFirst { it.id == cItem.id }
|
||||||
val itemIndex = items.indexOfFirst { it.id == cItem.id }
|
|
||||||
if (itemIndex >= 0) {
|
if (itemIndex >= 0) {
|
||||||
items[itemIndex] = cItem
|
chatItems[itemIndex] = cItem
|
||||||
}
|
}
|
||||||
} else if (status != null) {
|
} else if (status != null) {
|
||||||
chatItemStatuses[cItem.id] = status
|
chatItemStatuses[cItem.id] = status
|
||||||
@@ -359,10 +362,10 @@ object ChatModel {
|
|||||||
}
|
}
|
||||||
// remove from current chat
|
// remove from current chat
|
||||||
if (chatId.value == cInfo.id) {
|
if (chatId.value == cInfo.id) {
|
||||||
chatItems.removeAll {
|
val itemIndex = chatItems.indexOfFirst { it.id == cItem.id }
|
||||||
val remove = it.id == cItem.id
|
if (itemIndex >= 0) {
|
||||||
if (remove) { AudioPlayer.stop(it) }
|
AudioPlayer.stop(chatItems[itemIndex])
|
||||||
remove
|
chatItems.removeAt(itemIndex)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -403,7 +406,7 @@ object ChatModel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun removeLiveDummy() {
|
fun removeLiveDummy() {
|
||||||
if (chatItems.value.lastOrNull()?.id == ChatItem.TEMP_LIVE_CHAT_ITEM_ID) {
|
if (chatItems.lastOrNull()?.id == ChatItem.TEMP_LIVE_CHAT_ITEM_ID) {
|
||||||
chatItems.removeLast()
|
chatItems.removeLast()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -435,14 +438,14 @@ object ChatModel {
|
|||||||
var markedRead = 0
|
var markedRead = 0
|
||||||
if (chatId.value == cInfo.id) {
|
if (chatId.value == cInfo.id) {
|
||||||
var i = 0
|
var i = 0
|
||||||
val items = chatItems.value
|
Log.d(TAG, "TODOCHAT: markItemsReadInCurrentChat: marking read ${cInfo.id}, current chatId ${chatId.value}, size was ${chatItems.size}")
|
||||||
while (i < items.size) {
|
while (i < chatItems.count()) {
|
||||||
val item = items[i]
|
val item = chatItems[i]
|
||||||
if (item.meta.itemStatus is CIStatus.RcvNew && (range == null || (range.from <= item.id && item.id <= range.to))) {
|
if (item.meta.itemStatus is CIStatus.RcvNew && (range == null || (range.from <= item.id && item.id <= range.to))) {
|
||||||
val newItem = item.withStatus(CIStatus.RcvRead())
|
val newItem = item.withStatus(CIStatus.RcvRead())
|
||||||
items[i] = newItem
|
chatItems[i] = newItem
|
||||||
if (newItem.meta.itemLive != true && newItem.meta.itemTimed?.ttl != null) {
|
if (newItem.meta.itemLive != true && newItem.meta.itemTimed?.ttl != null) {
|
||||||
items[i] = newItem.copy(meta = newItem.meta.copy(itemTimed = newItem.meta.itemTimed.copy(
|
chatItems[i] = newItem.copy(meta = newItem.meta.copy(itemTimed = newItem.meta.itemTimed.copy(
|
||||||
deleteAt = Clock.System.now() + newItem.meta.itemTimed.ttl.toDuration(DurationUnit.SECONDS)))
|
deleteAt = Clock.System.now() + newItem.meta.itemTimed.ttl.toDuration(DurationUnit.SECONDS)))
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -450,6 +453,7 @@ object ChatModel {
|
|||||||
}
|
}
|
||||||
i += 1
|
i += 1
|
||||||
}
|
}
|
||||||
|
Log.d(TAG, "TODOCHAT: markItemsReadInCurrentChat: marked read ${cInfo.id}, current chatId ${chatId.value}, size now ${chatItems.size}")
|
||||||
}
|
}
|
||||||
return markedRead
|
return markedRead
|
||||||
}
|
}
|
||||||
@@ -640,8 +644,7 @@ object ChatModel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun addTerminalItem(item: TerminalItem) {
|
fun addTerminalItem(item: TerminalItem) {
|
||||||
val maxItems = if (appPreferences.developerTools.get()) 500 else 200
|
if (terminalItems.value.size >= 500) {
|
||||||
if (terminalItems.value.size >= maxItems) {
|
|
||||||
terminalItems.value = terminalItems.value.subList(1, terminalItems.value.size)
|
terminalItems.value = terminalItems.value.subList(1, terminalItems.value.size)
|
||||||
}
|
}
|
||||||
terminalItems.value += item
|
terminalItems.value += item
|
||||||
@@ -966,16 +969,6 @@ sealed class ChatInfo: SomeChat, NamedChat {
|
|||||||
is Group -> groupInfo.chatSettings
|
is Group -> groupInfo.chatSettings
|
||||||
else -> null
|
else -> null
|
||||||
}
|
}
|
||||||
|
|
||||||
val chatTs: Instant
|
|
||||||
get() = when(this) {
|
|
||||||
is Direct -> contact.chatTs ?: contact.updatedAt
|
|
||||||
is Group -> groupInfo.chatTs ?: groupInfo.updatedAt
|
|
||||||
is Local -> noteFolder.chatTs
|
|
||||||
is ContactRequest -> contactRequest.updatedAt
|
|
||||||
is ContactConnection -> contactConnection.updatedAt
|
|
||||||
is InvalidJSON -> updatedAt
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
@@ -1016,7 +1009,6 @@ data class Contact(
|
|||||||
val mergedPreferences: ContactUserPreferences,
|
val mergedPreferences: ContactUserPreferences,
|
||||||
override val createdAt: Instant,
|
override val createdAt: Instant,
|
||||||
override val updatedAt: Instant,
|
override val updatedAt: Instant,
|
||||||
val chatTs: Instant?,
|
|
||||||
val contactGroupMemberId: Long? = null,
|
val contactGroupMemberId: Long? = null,
|
||||||
val contactGrpInvSent: Boolean
|
val contactGrpInvSent: Boolean
|
||||||
): SomeChat, NamedChat {
|
): SomeChat, NamedChat {
|
||||||
@@ -1085,7 +1077,6 @@ data class Contact(
|
|||||||
mergedPreferences = ContactUserPreferences.sampleData,
|
mergedPreferences = ContactUserPreferences.sampleData,
|
||||||
createdAt = Clock.System.now(),
|
createdAt = Clock.System.now(),
|
||||||
updatedAt = Clock.System.now(),
|
updatedAt = Clock.System.now(),
|
||||||
chatTs = Clock.System.now(),
|
|
||||||
contactGrpInvSent = false
|
contactGrpInvSent = false
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -1213,8 +1204,7 @@ data class GroupInfo (
|
|||||||
val hostConnCustomUserProfileId: Long? = null,
|
val hostConnCustomUserProfileId: Long? = null,
|
||||||
val chatSettings: ChatSettings,
|
val chatSettings: ChatSettings,
|
||||||
override val createdAt: Instant,
|
override val createdAt: Instant,
|
||||||
override val updatedAt: Instant,
|
override val updatedAt: Instant
|
||||||
val chatTs: Instant?
|
|
||||||
): SomeChat, NamedChat {
|
): SomeChat, NamedChat {
|
||||||
override val chatType get() = ChatType.Group
|
override val chatType get() = ChatType.Group
|
||||||
override val id get() = "#$groupId"
|
override val id get() = "#$groupId"
|
||||||
@@ -1255,8 +1245,7 @@ data class GroupInfo (
|
|||||||
hostConnCustomUserProfileId = null,
|
hostConnCustomUserProfileId = null,
|
||||||
chatSettings = ChatSettings(enableNtfs = MsgFilter.All, sendRcpts = null, favorite = false),
|
chatSettings = ChatSettings(enableNtfs = MsgFilter.All, sendRcpts = null, favorite = false),
|
||||||
createdAt = Clock.System.now(),
|
createdAt = Clock.System.now(),
|
||||||
updatedAt = Clock.System.now(),
|
updatedAt = Clock.System.now()
|
||||||
chatTs = Clock.System.now()
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1518,8 +1507,7 @@ class NoteFolder(
|
|||||||
val favorite: Boolean,
|
val favorite: Boolean,
|
||||||
val unread: Boolean,
|
val unread: Boolean,
|
||||||
override val createdAt: Instant,
|
override val createdAt: Instant,
|
||||||
override val updatedAt: Instant,
|
override val updatedAt: Instant
|
||||||
val chatTs: Instant
|
|
||||||
): SomeChat, NamedChat {
|
): SomeChat, NamedChat {
|
||||||
override val chatType get() = ChatType.Local
|
override val chatType get() = ChatType.Local
|
||||||
override val id get() = "*$noteFolderId"
|
override val id get() = "*$noteFolderId"
|
||||||
@@ -1542,8 +1530,7 @@ class NoteFolder(
|
|||||||
favorite = false,
|
favorite = false,
|
||||||
unread = false,
|
unread = false,
|
||||||
createdAt = Clock.System.now(),
|
createdAt = Clock.System.now(),
|
||||||
updatedAt = Clock.System.now(),
|
updatedAt = Clock.System.now()
|
||||||
chatTs = Clock.System.now()
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1822,7 +1809,7 @@ data class ChatItem (
|
|||||||
is CIContent.SndGroupInvitation -> false
|
is CIContent.SndGroupInvitation -> false
|
||||||
is CIContent.RcvDirectEventContent -> when (content.rcvDirectEvent) {
|
is CIContent.RcvDirectEventContent -> when (content.rcvDirectEvent) {
|
||||||
is RcvDirectEvent.ContactDeleted -> false
|
is RcvDirectEvent.ContactDeleted -> false
|
||||||
is RcvDirectEvent.ProfileUpdated -> false
|
is RcvDirectEvent.ProfileUpdated -> true
|
||||||
}
|
}
|
||||||
is CIContent.RcvGroupEventContent -> when (content.rcvGroupEvent) {
|
is CIContent.RcvGroupEventContent -> when (content.rcvGroupEvent) {
|
||||||
is RcvGroupEvent.MemberAdded -> false
|
is RcvGroupEvent.MemberAdded -> false
|
||||||
@@ -2003,46 +1990,6 @@ data class ChatItem (
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun MutableState<SnapshotStateList<ChatItem>>.add(index: Int, chatItem: ChatItem) {
|
|
||||||
value = SnapshotStateList<ChatItem>().apply { addAll(value); add(index, chatItem) }
|
|
||||||
}
|
|
||||||
|
|
||||||
fun MutableState<SnapshotStateList<ChatItem>>.add(chatItem: ChatItem) {
|
|
||||||
value = SnapshotStateList<ChatItem>().apply { addAll(value); add(chatItem) }
|
|
||||||
}
|
|
||||||
|
|
||||||
fun MutableState<SnapshotStateList<ChatItem>>.addAll(index: Int, chatItems: List<ChatItem>) {
|
|
||||||
value = SnapshotStateList<ChatItem>().apply { addAll(value); addAll(index, chatItems) }
|
|
||||||
}
|
|
||||||
|
|
||||||
fun MutableState<SnapshotStateList<ChatItem>>.addAll(chatItems: List<ChatItem>) {
|
|
||||||
value = SnapshotStateList<ChatItem>().apply { addAll(value); addAll(chatItems) }
|
|
||||||
}
|
|
||||||
|
|
||||||
fun MutableState<SnapshotStateList<ChatItem>>.removeAll(block: (ChatItem) -> Boolean) {
|
|
||||||
value = SnapshotStateList<ChatItem>().apply { addAll(value); removeAll(block) }
|
|
||||||
}
|
|
||||||
|
|
||||||
fun MutableState<SnapshotStateList<ChatItem>>.removeAt(index: Int) {
|
|
||||||
value = SnapshotStateList<ChatItem>().apply { addAll(value); removeAt(index) }
|
|
||||||
}
|
|
||||||
|
|
||||||
fun MutableState<SnapshotStateList<ChatItem>>.removeLast() {
|
|
||||||
value = SnapshotStateList<ChatItem>().apply { addAll(value); removeLast() }
|
|
||||||
}
|
|
||||||
|
|
||||||
fun MutableState<SnapshotStateList<ChatItem>>.replaceAll(chatItems: List<ChatItem>) {
|
|
||||||
value = SnapshotStateList<ChatItem>().apply { addAll(chatItems) }
|
|
||||||
}
|
|
||||||
|
|
||||||
fun MutableState<SnapshotStateList<ChatItem>>.clear() {
|
|
||||||
value = SnapshotStateList<ChatItem>()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun State<SnapshotStateList<ChatItem>>.asReversed(): MutableList<ChatItem> = value.asReversed()
|
|
||||||
|
|
||||||
val State<List<ChatItem>>.size: Int get() = value.size
|
|
||||||
|
|
||||||
enum class CIMergeCategory {
|
enum class CIMergeCategory {
|
||||||
MemberConnected,
|
MemberConnected,
|
||||||
RcvGroupEvent,
|
RcvGroupEvent,
|
||||||
|
|||||||
+23
-19
@@ -451,21 +451,7 @@ object ChatController {
|
|||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
val msg = recvMsg(ctrl)
|
val msg = recvMsg(ctrl)
|
||||||
if (msg != null) {
|
if (msg != null) withSingleThreadContext { processReceivedMsg(msg) }
|
||||||
val finishedWithoutTimeout = withTimeoutOrNull(60_000L) {
|
|
||||||
processReceivedMsg(msg)
|
|
||||||
}
|
|
||||||
if (finishedWithoutTimeout == null) {
|
|
||||||
Log.e(TAG, "Timeout reached while processing received message: " + msg.resp.responseType)
|
|
||||||
if (appPreferences.developerTools.get() && appPreferences.showSlowApiCalls.get()) {
|
|
||||||
AlertManager.shared.showAlertMsg(
|
|
||||||
title = generalGetString(MR.strings.possible_slow_function_title),
|
|
||||||
text = generalGetString(MR.strings.possible_slow_function_desc).format(60, msg.resp.responseType + "\n" + Exception().stackTraceToString()),
|
|
||||||
shareText = true
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(TAG, "ChatController recvMsg/processReceivedMsg exception: " + e.stackTraceToString());
|
Log.e(TAG, "ChatController recvMsg/processReceivedMsg exception: " + e.stackTraceToString());
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
@@ -631,6 +617,12 @@ object ChatController {
|
|||||||
throw Error("failed to set remote hosts folder: ${r.responseType} ${r.details}")
|
throw Error("failed to set remote hosts folder: ${r.responseType} ${r.details}")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suspend fun apiSetXFTPConfig(cfg: XFTPFileConfig?) {
|
||||||
|
val r = sendCmd(null, CC.ApiSetXFTPConfig(cfg))
|
||||||
|
if (r is CR.CmdOk) return
|
||||||
|
throw Error("apiSetXFTPConfig bad response: ${r.responseType} ${r.details}")
|
||||||
|
}
|
||||||
|
|
||||||
suspend fun apiSetEncryptLocalFiles(enable: Boolean) = sendCommandOkResp(null, CC.ApiSetEncryptLocalFiles(enable))
|
suspend fun apiSetEncryptLocalFiles(enable: Boolean) = sendCommandOkResp(null, CC.ApiSetEncryptLocalFiles(enable))
|
||||||
|
|
||||||
suspend fun apiExportArchive(config: ArchiveConfig) {
|
suspend fun apiExportArchive(config: ArchiveConfig) {
|
||||||
@@ -1693,7 +1685,7 @@ object ChatController {
|
|||||||
chatModel.networkStatuses[s.agentConnId] = s.networkStatus
|
chatModel.networkStatuses[s.agentConnId] = s.networkStatus
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
is CR.NewChatItem -> withBGApi {
|
is CR.NewChatItem -> {
|
||||||
val cInfo = r.chatItem.chatInfo
|
val cInfo = r.chatItem.chatInfo
|
||||||
val cItem = r.chatItem.chatItem
|
val cItem = r.chatItem.chatItem
|
||||||
if (active(r.user)) {
|
if (active(r.user)) {
|
||||||
@@ -1708,7 +1700,7 @@ object ChatController {
|
|||||||
((mc is MsgContent.MCImage && file.fileSize <= MAX_IMAGE_SIZE_AUTO_RCV)
|
((mc is MsgContent.MCImage && file.fileSize <= MAX_IMAGE_SIZE_AUTO_RCV)
|
||||||
|| (mc is MsgContent.MCVideo && file.fileSize <= MAX_VIDEO_SIZE_AUTO_RCV)
|
|| (mc is MsgContent.MCVideo && file.fileSize <= MAX_VIDEO_SIZE_AUTO_RCV)
|
||||||
|| (mc is MsgContent.MCVoice && file.fileSize <= MAX_VOICE_SIZE_AUTO_RCV && file.fileStatus !is CIFileStatus.RcvAccepted))) {
|
|| (mc is MsgContent.MCVoice && file.fileSize <= MAX_VOICE_SIZE_AUTO_RCV && file.fileStatus !is CIFileStatus.RcvAccepted))) {
|
||||||
receiveFile(rhId, r.user, file.fileId, auto = true)
|
withBGApi { receiveFile(rhId, r.user, file.fileId, auto = true) }
|
||||||
}
|
}
|
||||||
if (cItem.showNotification && (allowedToShowNotification() || chatModel.chatId.value != cInfo.id || chatModel.remoteHostId() != rhId)) {
|
if (cItem.showNotification && (allowedToShowNotification() || chatModel.chatId.value != cInfo.id || chatModel.remoteHostId() != rhId)) {
|
||||||
ntfManager.notifyMessageReceived(r.user, cInfo, cItem)
|
ntfManager.notifyMessageReceived(r.user, cInfo, cItem)
|
||||||
@@ -1908,8 +1900,10 @@ object ChatController {
|
|||||||
if (invitation != null) {
|
if (invitation != null) {
|
||||||
chatModel.callManager.reportCallRemoteEnded(invitation = invitation)
|
chatModel.callManager.reportCallRemoteEnded(invitation = invitation)
|
||||||
}
|
}
|
||||||
withCall(r, r.contact) { call ->
|
withCall(r, r.contact) { _ ->
|
||||||
withBGApi { chatModel.callManager.endCall(call) }
|
chatModel.callCommand.add(WCallCommand.End)
|
||||||
|
chatModel.activeCall.value = null
|
||||||
|
chatModel.showCallView.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
is CR.ContactSwitch ->
|
is CR.ContactSwitch ->
|
||||||
@@ -2165,6 +2159,10 @@ object ChatController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun getXFTPCfg(): XFTPFileConfig {
|
||||||
|
return XFTPFileConfig(minFileSize = 0)
|
||||||
|
}
|
||||||
|
|
||||||
fun getNetCfg(): NetCfg {
|
fun getNetCfg(): NetCfg {
|
||||||
val useSocksProxy = appPrefs.networkUseSocksProxy.get()
|
val useSocksProxy = appPrefs.networkUseSocksProxy.get()
|
||||||
val proxyHostPort = appPrefs.networkProxyHostPort.get()
|
val proxyHostPort = appPrefs.networkProxyHostPort.get()
|
||||||
@@ -2273,6 +2271,7 @@ sealed class CC {
|
|||||||
class SetTempFolder(val tempFolder: String): CC()
|
class SetTempFolder(val tempFolder: String): CC()
|
||||||
class SetFilesFolder(val filesFolder: String): CC()
|
class SetFilesFolder(val filesFolder: String): CC()
|
||||||
class SetRemoteHostsFolder(val remoteHostsFolder: String): CC()
|
class SetRemoteHostsFolder(val remoteHostsFolder: String): CC()
|
||||||
|
class ApiSetXFTPConfig(val config: XFTPFileConfig?): CC()
|
||||||
class ApiSetEncryptLocalFiles(val enable: Boolean): CC()
|
class ApiSetEncryptLocalFiles(val enable: Boolean): CC()
|
||||||
class ApiExportArchive(val config: ArchiveConfig): CC()
|
class ApiExportArchive(val config: ArchiveConfig): CC()
|
||||||
class ApiImportArchive(val config: ArchiveConfig): CC()
|
class ApiImportArchive(val config: ArchiveConfig): CC()
|
||||||
@@ -2402,6 +2401,7 @@ sealed class CC {
|
|||||||
is SetTempFolder -> "/_temp_folder $tempFolder"
|
is SetTempFolder -> "/_temp_folder $tempFolder"
|
||||||
is SetFilesFolder -> "/_files_folder $filesFolder"
|
is SetFilesFolder -> "/_files_folder $filesFolder"
|
||||||
is SetRemoteHostsFolder -> "/remote_hosts_folder $remoteHostsFolder"
|
is SetRemoteHostsFolder -> "/remote_hosts_folder $remoteHostsFolder"
|
||||||
|
is ApiSetXFTPConfig -> if (config != null) "/_xftp on ${json.encodeToString(config)}" else "/_xftp off"
|
||||||
is ApiSetEncryptLocalFiles -> "/_files_encrypt ${onOff(enable)}"
|
is ApiSetEncryptLocalFiles -> "/_files_encrypt ${onOff(enable)}"
|
||||||
is ApiExportArchive -> "/_db export ${json.encodeToString(config)}"
|
is ApiExportArchive -> "/_db export ${json.encodeToString(config)}"
|
||||||
is ApiImportArchive -> "/_db import ${json.encodeToString(config)}"
|
is ApiImportArchive -> "/_db import ${json.encodeToString(config)}"
|
||||||
@@ -2536,6 +2536,7 @@ sealed class CC {
|
|||||||
is SetTempFolder -> "setTempFolder"
|
is SetTempFolder -> "setTempFolder"
|
||||||
is SetFilesFolder -> "setFilesFolder"
|
is SetFilesFolder -> "setFilesFolder"
|
||||||
is SetRemoteHostsFolder -> "setRemoteHostsFolder"
|
is SetRemoteHostsFolder -> "setRemoteHostsFolder"
|
||||||
|
is ApiSetXFTPConfig -> "apiSetXFTPConfig"
|
||||||
is ApiSetEncryptLocalFiles -> "apiSetEncryptLocalFiles"
|
is ApiSetEncryptLocalFiles -> "apiSetEncryptLocalFiles"
|
||||||
is ApiExportArchive -> "apiExportArchive"
|
is ApiExportArchive -> "apiExportArchive"
|
||||||
is ApiImportArchive -> "apiImportArchive"
|
is ApiImportArchive -> "apiImportArchive"
|
||||||
@@ -2701,6 +2702,9 @@ sealed class ChatPagination {
|
|||||||
@Serializable
|
@Serializable
|
||||||
class ComposedMessage(val fileSource: CryptoFile?, val quotedItemId: Long?, val msgContent: MsgContent)
|
class ComposedMessage(val fileSource: CryptoFile?, val quotedItemId: Long?, val msgContent: MsgContent)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
class XFTPFileConfig(val minFileSize: Long)
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
class ArchiveConfig(val archivePath: String, val disableCompression: Boolean? = null, val parentTempDirectory: String? = null)
|
class ArchiveConfig(val archivePath: String, val disableCompression: Boolean? = null, val parentTempDirectory: String? = null)
|
||||||
|
|
||||||
|
|||||||
+4
-18
@@ -28,7 +28,6 @@ external fun chatParseMarkdown(str: String): String
|
|||||||
external fun chatParseServer(str: String): String
|
external fun chatParseServer(str: String): String
|
||||||
external fun chatPasswordHash(pwd: String, salt: String): String
|
external fun chatPasswordHash(pwd: String, salt: String): String
|
||||||
external fun chatValidName(name: String): String
|
external fun chatValidName(name: String): String
|
||||||
external fun chatJsonLength(str: String): Int
|
|
||||||
external fun chatWriteFile(ctrl: ChatCtrl, path: String, buffer: ByteBuffer): String
|
external fun chatWriteFile(ctrl: ChatCtrl, path: String, buffer: ByteBuffer): String
|
||||||
external fun chatReadFile(path: String, key: String, nonce: String): Array<Any>
|
external fun chatReadFile(path: String, key: String, nonce: String): Array<Any>
|
||||||
external fun chatEncryptFile(ctrl: ChatCtrl, fromPath: String, toPath: String): String
|
external fun chatEncryptFile(ctrl: ChatCtrl, fromPath: String, toPath: String): String
|
||||||
@@ -43,7 +42,7 @@ val appPreferences: AppPreferences
|
|||||||
val chatController: ChatController = ChatController
|
val chatController: ChatController = ChatController
|
||||||
|
|
||||||
fun initChatControllerAndRunMigrations() {
|
fun initChatControllerAndRunMigrations() {
|
||||||
withLongRunningApi {
|
withLongRunningApi(slow = 30_000, deadlock = 60_000) {
|
||||||
if (appPreferences.chatStopped.get() && appPreferences.storeDBPassphrase.get() && ksDatabasePassword.get() != null) {
|
if (appPreferences.chatStopped.get() && appPreferences.storeDBPassphrase.get() && ksDatabasePassword.get() != null) {
|
||||||
initChatController(startChat = ::showStartChatAfterRestartAlert)
|
initChatController(startChat = ::showStartChatAfterRestartAlert)
|
||||||
} else {
|
} else {
|
||||||
@@ -59,23 +58,10 @@ suspend fun initChatController(useKey: String? = null, confirmMigrations: Migrat
|
|||||||
chatModel.ctrlInitInProgress.value = true
|
chatModel.ctrlInitInProgress.value = true
|
||||||
val dbKey = useKey ?: DatabaseUtils.useDatabaseKey()
|
val dbKey = useKey ?: DatabaseUtils.useDatabaseKey()
|
||||||
val confirm = confirmMigrations ?: if (appPreferences.developerTools.get() && appPreferences.confirmDBUpgrades.get()) MigrationConfirmation.Error else MigrationConfirmation.YesUp
|
val confirm = confirmMigrations ?: if (appPreferences.developerTools.get() && appPreferences.confirmDBUpgrades.get()) MigrationConfirmation.Error else MigrationConfirmation.YesUp
|
||||||
var migrated: Array<Any> = chatMigrateInit(dbAbsolutePrefixPath, dbKey, MigrationConfirmation.Error.value)
|
val migrated: Array<Any> = chatMigrateInit(dbAbsolutePrefixPath, dbKey, confirm.value)
|
||||||
var res: DBMigrationResult = runCatching {
|
val res: DBMigrationResult = kotlin.runCatching {
|
||||||
json.decodeFromString<DBMigrationResult>(migrated[0] as String)
|
json.decodeFromString<DBMigrationResult>(migrated[0] as String)
|
||||||
}.getOrElse { DBMigrationResult.Unknown(migrated[0] as String) }
|
}.getOrElse { DBMigrationResult.Unknown(migrated[0] as String) }
|
||||||
val rerunMigration = res is DBMigrationResult.ErrorMigration && when (res.migrationError) {
|
|
||||||
// we don't allow to run down migrations without confirmation in UI, so currently it won't be YesUpDown
|
|
||||||
is MigrationError.Upgrade -> confirm == MigrationConfirmation.YesUp || confirm == MigrationConfirmation.YesUpDown
|
|
||||||
is MigrationError.Downgrade -> confirm == MigrationConfirmation.YesUpDown
|
|
||||||
is MigrationError.Error -> false
|
|
||||||
}
|
|
||||||
if (rerunMigration) {
|
|
||||||
chatModel.dbMigrationInProgress.value = true
|
|
||||||
migrated = chatMigrateInit(dbAbsolutePrefixPath, dbKey, confirm.value)
|
|
||||||
res = runCatching {
|
|
||||||
json.decodeFromString<DBMigrationResult>(migrated[0] as String)
|
|
||||||
}.getOrElse { DBMigrationResult.Unknown(migrated[0] as String) }
|
|
||||||
}
|
|
||||||
val ctrl = if (res is DBMigrationResult.OK) {
|
val ctrl = if (res is DBMigrationResult.OK) {
|
||||||
migrated[1] as Long
|
migrated[1] as Long
|
||||||
} else null
|
} else null
|
||||||
@@ -91,6 +77,7 @@ suspend fun initChatController(useKey: String? = null, confirmMigrations: Migrat
|
|||||||
if (appPlatform.isDesktop) {
|
if (appPlatform.isDesktop) {
|
||||||
controller.apiSetRemoteHostsFolder(remoteHostsDir.absolutePath)
|
controller.apiSetRemoteHostsFolder(remoteHostsDir.absolutePath)
|
||||||
}
|
}
|
||||||
|
controller.apiSetXFTPConfig(controller.getXFTPCfg())
|
||||||
controller.apiSetEncryptLocalFiles(controller.appPrefs.privacyEncryptLocalFiles.get())
|
controller.apiSetEncryptLocalFiles(controller.appPrefs.privacyEncryptLocalFiles.get())
|
||||||
// If we migrated successfully means previous re-encryption process on database level finished successfully too
|
// If we migrated successfully means previous re-encryption process on database level finished successfully too
|
||||||
if (appPreferences.encryptionStartedAt.get() != null) appPreferences.encryptionStartedAt.set(null)
|
if (appPreferences.encryptionStartedAt.get() != null) appPreferences.encryptionStartedAt.set(null)
|
||||||
@@ -132,7 +119,6 @@ suspend fun initChatController(useKey: String? = null, confirmMigrations: Migrat
|
|||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
chatModel.ctrlInitInProgress.value = false
|
chatModel.ctrlInitInProgress.value = false
|
||||||
chatModel.dbMigrationInProgress.value = false
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+2
-2
@@ -55,7 +55,7 @@ abstract class NtfManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun openChatAction(userId: Long?, chatId: ChatId) {
|
fun openChatAction(userId: Long?, chatId: ChatId) {
|
||||||
withLongRunningApi {
|
withLongRunningApi(slow = 30_000, deadlock = 60_000) {
|
||||||
awaitChatStartedIfNeeded(chatModel)
|
awaitChatStartedIfNeeded(chatModel)
|
||||||
if (userId != null && userId != chatModel.currentUser.value?.userId && chatModel.currentUser.value != null) {
|
if (userId != null && userId != chatModel.currentUser.value?.userId && chatModel.currentUser.value != null) {
|
||||||
// TODO include remote host ID in desktop notifications?
|
// TODO include remote host ID in desktop notifications?
|
||||||
@@ -70,7 +70,7 @@ abstract class NtfManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun showChatsAction(userId: Long?) {
|
fun showChatsAction(userId: Long?) {
|
||||||
withLongRunningApi {
|
withLongRunningApi(slow = 30_000, deadlock = 60_000) {
|
||||||
awaitChatStartedIfNeeded(chatModel)
|
awaitChatStartedIfNeeded(chatModel)
|
||||||
if (userId != null && userId != chatModel.currentUser.value?.userId && chatModel.currentUser.value != null) {
|
if (userId != null && userId != chatModel.currentUser.value?.userId && chatModel.currentUser.value != null) {
|
||||||
// TODO include remote host ID in desktop notifications?
|
// TODO include remote host ID in desktop notifications?
|
||||||
|
|||||||
-5
@@ -1,21 +1,16 @@
|
|||||||
package chat.simplex.common.platform
|
package chat.simplex.common.platform
|
||||||
|
|
||||||
import chat.simplex.common.model.ChatId
|
|
||||||
import chat.simplex.common.model.NotificationsMode
|
import chat.simplex.common.model.NotificationsMode
|
||||||
|
|
||||||
interface PlatformInterface {
|
interface PlatformInterface {
|
||||||
suspend fun androidServiceStart() {}
|
suspend fun androidServiceStart() {}
|
||||||
fun androidServiceSafeStop() {}
|
fun androidServiceSafeStop() {}
|
||||||
fun androidCallServiceSafeStop() {}
|
|
||||||
fun androidNotificationsModeChanged(mode: NotificationsMode) {}
|
fun androidNotificationsModeChanged(mode: NotificationsMode) {}
|
||||||
fun androidChatStartedAfterBeingOff() {}
|
fun androidChatStartedAfterBeingOff() {}
|
||||||
fun androidChatStopped() {}
|
fun androidChatStopped() {}
|
||||||
fun androidChatInitializedAndStarted() {}
|
fun androidChatInitializedAndStarted() {}
|
||||||
fun androidIsBackgroundCallAllowed(): Boolean = true
|
fun androidIsBackgroundCallAllowed(): Boolean = true
|
||||||
fun androidSetNightModeIfSupported() {}
|
fun androidSetNightModeIfSupported() {}
|
||||||
fun androidStartCallActivity(acceptCall: Boolean, remoteHostId: Long? = null, chatId: ChatId? = null) {}
|
|
||||||
fun androidPictureInPictureAllowed(): Boolean = true
|
|
||||||
fun androidCallEnded() {}
|
|
||||||
suspend fun androidAskToAllowBackgroundCalls(): Boolean = true
|
suspend fun androidAskToAllowBackgroundCalls(): Boolean = true
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
|
|||||||
+19
-27
@@ -1,6 +1,6 @@
|
|||||||
package chat.simplex.common.views.call
|
package chat.simplex.common.views.call
|
||||||
|
|
||||||
import chat.simplex.common.model.*
|
import chat.simplex.common.model.ChatModel
|
||||||
import chat.simplex.common.platform.*
|
import chat.simplex.common.platform.*
|
||||||
import chat.simplex.common.views.helpers.withBGApi
|
import chat.simplex.common.views.helpers.withBGApi
|
||||||
import kotlinx.datetime.Clock
|
import kotlinx.datetime.Clock
|
||||||
@@ -23,29 +23,27 @@ class CallManager(val chatModel: ChatModel) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun acceptIncomingCall(invitation: RcvCallInvitation) = withBGApi {
|
fun acceptIncomingCall(invitation: RcvCallInvitation) {
|
||||||
val call = chatModel.activeCall.value
|
val call = chatModel.activeCall.value
|
||||||
val contactInfo = chatModel.controller.apiContactInfo(invitation.remoteHostId, invitation.contact.contactId)
|
if (call == null) {
|
||||||
val profile = contactInfo?.second ?: invitation.user.profile.toProfile()
|
justAcceptIncomingCall(invitation = invitation)
|
||||||
// In case the same contact calling while previous call didn't end yet (abnormal ending of call from the other side)
|
|
||||||
if (call == null || (call.remoteHostId == invitation.remoteHostId && call.contact.id == invitation.contact.id)) {
|
|
||||||
justAcceptIncomingCall(invitation = invitation, profile)
|
|
||||||
} else {
|
} else {
|
||||||
chatModel.switchingCall.value = true
|
withBGApi {
|
||||||
try {
|
chatModel.switchingCall.value = true
|
||||||
endCall(call = call)
|
try {
|
||||||
justAcceptIncomingCall(invitation = invitation, profile)
|
endCall(call = call)
|
||||||
} finally {
|
justAcceptIncomingCall(invitation = invitation)
|
||||||
chatModel.switchingCall.value = false
|
} finally {
|
||||||
|
chatModel.switchingCall.value = false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun justAcceptIncomingCall(invitation: RcvCallInvitation, userProfile: Profile) {
|
private fun justAcceptIncomingCall(invitation: RcvCallInvitation) {
|
||||||
with (chatModel) {
|
with (chatModel) {
|
||||||
activeCall.value = Call(
|
activeCall.value = Call(
|
||||||
remoteHostId = invitation.remoteHostId,
|
remoteHostId = invitation.remoteHostId,
|
||||||
userProfile = userProfile,
|
|
||||||
contact = invitation.contact,
|
contact = invitation.contact,
|
||||||
callState = CallState.InvitationAccepted,
|
callState = CallState.InvitationAccepted,
|
||||||
localMedia = invitation.callType.media,
|
localMedia = invitation.callType.media,
|
||||||
@@ -70,23 +68,17 @@ class CallManager(val chatModel: ChatModel) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
suspend fun endCall(call: Call) {
|
suspend fun endCall(call: Call) {
|
||||||
with(chatModel) {
|
with (chatModel) {
|
||||||
// If there is active call currently and it's with other contact, don't interrupt it
|
|
||||||
if (activeCall.value != null && !(activeCall.value?.remoteHostId == call.remoteHostId && activeCall.value?.contact?.id == call.contact.id)) return
|
|
||||||
|
|
||||||
// Don't destroy WebView if you plan to accept next call right after this one
|
|
||||||
if (!switchingCall.value) {
|
|
||||||
showCallView.value = false
|
|
||||||
activeCall.value = null
|
|
||||||
activeCallViewIsCollapsed.value = false
|
|
||||||
platform.androidCallEnded()
|
|
||||||
}
|
|
||||||
if (call.callState == CallState.Ended) {
|
if (call.callState == CallState.Ended) {
|
||||||
Log.d(TAG, "CallManager.endCall: call ended")
|
Log.d(TAG, "CallManager.endCall: call ended")
|
||||||
|
activeCall.value = null
|
||||||
|
showCallView.value = false
|
||||||
} else {
|
} else {
|
||||||
Log.d(TAG, "CallManager.endCall: ending call...")
|
Log.d(TAG, "CallManager.endCall: ending call...")
|
||||||
//callCommand.add(WCallCommand.End)
|
callCommand.add(WCallCommand.End)
|
||||||
|
showCallView.value = false
|
||||||
controller.apiEndCall(call.remoteHostId, call.contact)
|
controller.apiEndCall(call.remoteHostId, call.contact)
|
||||||
|
activeCall.value = null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-13
@@ -7,11 +7,11 @@ import kotlinx.datetime.Instant
|
|||||||
import kotlinx.serialization.SerialName
|
import kotlinx.serialization.SerialName
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
import java.net.URI
|
import java.net.URI
|
||||||
|
import java.util.*
|
||||||
import kotlin.collections.ArrayList
|
import kotlin.collections.ArrayList
|
||||||
|
|
||||||
data class Call(
|
data class Call(
|
||||||
val remoteHostId: Long?,
|
val remoteHostId: Long?,
|
||||||
val userProfile: Profile,
|
|
||||||
val contact: Contact,
|
val contact: Contact,
|
||||||
val callState: CallState,
|
val callState: CallState,
|
||||||
val localMedia: CallMediaType,
|
val localMedia: CallMediaType,
|
||||||
@@ -23,7 +23,7 @@ data class Call(
|
|||||||
val soundSpeaker: Boolean = localMedia == CallMediaType.Video,
|
val soundSpeaker: Boolean = localMedia == CallMediaType.Video,
|
||||||
var localCamera: VideoCamera = VideoCamera.User,
|
var localCamera: VideoCamera = VideoCamera.User,
|
||||||
val connectionInfo: ConnectionInfo? = null,
|
val connectionInfo: ConnectionInfo? = null,
|
||||||
var connectedAt: Instant? = null,
|
var connectedAt: Instant? = null
|
||||||
) {
|
) {
|
||||||
val encrypted: Boolean get() = localEncrypted && sharedKey != null
|
val encrypted: Boolean get() = localEncrypted && sharedKey != null
|
||||||
val localEncrypted: Boolean get() = localCapabilities?.encryption ?: false
|
val localEncrypted: Boolean get() = localCapabilities?.encryption ?: false
|
||||||
@@ -36,9 +36,6 @@ data class Call(
|
|||||||
}
|
}
|
||||||
|
|
||||||
val hasMedia: Boolean get() = callState == CallState.OfferSent || callState == CallState.Negotiated || callState == CallState.Connected
|
val hasMedia: Boolean get() = callState == CallState.OfferSent || callState == CallState.Negotiated || callState == CallState.Connected
|
||||||
|
|
||||||
fun supportsVideo(): Boolean = peerMedia == CallMediaType.Video || localMedia == CallMediaType.Video
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
enum class CallState {
|
enum class CallState {
|
||||||
@@ -78,7 +75,6 @@ sealed class WCallCommand {
|
|||||||
@Serializable @SerialName("media") data class Media(val media: CallMediaType, val enable: Boolean): WCallCommand()
|
@Serializable @SerialName("media") data class Media(val media: CallMediaType, val enable: Boolean): WCallCommand()
|
||||||
@Serializable @SerialName("camera") data class Camera(val camera: VideoCamera): WCallCommand()
|
@Serializable @SerialName("camera") data class Camera(val camera: VideoCamera): WCallCommand()
|
||||||
@Serializable @SerialName("description") data class Description(val state: String, val description: String): WCallCommand()
|
@Serializable @SerialName("description") data class Description(val state: String, val description: String): WCallCommand()
|
||||||
@Serializable @SerialName("layout") data class Layout(val layout: LayoutType): WCallCommand()
|
|
||||||
@Serializable @SerialName("end") object End: WCallCommand()
|
@Serializable @SerialName("end") object End: WCallCommand()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -171,13 +167,6 @@ enum class VideoCamera {
|
|||||||
val flipped: VideoCamera get() = if (this == User) Environment else User
|
val flipped: VideoCamera get() = if (this == User) Environment else User
|
||||||
}
|
}
|
||||||
|
|
||||||
@Serializable
|
|
||||||
enum class LayoutType {
|
|
||||||
@SerialName("default") Default,
|
|
||||||
@SerialName("localVideo") LocalVideo,
|
|
||||||
@SerialName("remoteVideo") RemoteVideo
|
|
||||||
}
|
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class ConnectionState(
|
data class ConnectionState(
|
||||||
val connectionState: String,
|
val connectionState: String,
|
||||||
|
|||||||
+1
-1
@@ -324,7 +324,7 @@ fun ChatItemInfoView(chatModel: ChatModel, ci: ChatItem, ciInfo: ChatItemInfo, d
|
|||||||
.fillMaxHeight(),
|
.fillMaxHeight(),
|
||||||
verticalArrangement = Arrangement.SpaceBetween
|
verticalArrangement = Arrangement.SpaceBetween
|
||||||
) {
|
) {
|
||||||
LaunchedEffect(ciInfo) {
|
LaunchedEffect(Unit) {
|
||||||
if (ciInfo.memberDeliveryStatuses != null) {
|
if (ciInfo.memberDeliveryStatuses != null) {
|
||||||
selection.value = CIInfoTab.Delivery(ciInfo.memberDeliveryStatuses)
|
selection.value = CIInfoTab.Delivery(ciInfo.memberDeliveryStatuses)
|
||||||
}
|
}
|
||||||
|
|||||||
+35
-32
@@ -67,13 +67,13 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: suspend (chatId:
|
|||||||
launch {
|
launch {
|
||||||
snapshotFlow { chatModel.chatId.value }
|
snapshotFlow { chatModel.chatId.value }
|
||||||
.distinctUntilChanged()
|
.distinctUntilChanged()
|
||||||
.filterNotNull()
|
.onEach { Log.d(TAG, "TODOCHAT: chatId: activeChatId ${activeChat.value?.id} == new chatId $it ${activeChat.value?.id == it} ") }
|
||||||
|
.filter { it != null && activeChat.value?.id != it }
|
||||||
.collect { chatId ->
|
.collect { chatId ->
|
||||||
if (activeChat.value?.id != chatId) {
|
// Redisplay the whole hierarchy if the chat is different to make going from groups to direct chat working correctly
|
||||||
// Redisplay the whole hierarchy if the chat is different to make going from groups to direct chat working correctly
|
// Also for situation when chatId changes after clicking in notification, etc
|
||||||
// Also for situation when chatId changes after clicking in notification, etc
|
activeChat.value = chatModel.getChat(chatId!!)
|
||||||
activeChat.value = chatModel.getChat(chatId)
|
Log.d(TAG, "TODOCHAT: chatId: activeChatId became ${activeChat.value?.id}")
|
||||||
}
|
|
||||||
markUnreadChatAsRead(activeChat, chatModel)
|
markUnreadChatAsRead(activeChat, chatModel)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -92,10 +92,12 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: suspend (chatId:
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
.distinctUntilChanged()
|
.distinctUntilChanged()
|
||||||
|
.onEach { Log.d(TAG, "TODOCHAT: chats: activeChatId ${activeChat.value?.id} == new chatId ${it?.id} ${activeChat.value?.id == it?.id} ") }
|
||||||
// Only changed chatInfo is important thing. Other properties can be skipped for reducing recompositions
|
// Only changed chatInfo is important thing. Other properties can be skipped for reducing recompositions
|
||||||
.filter { it != null && it.chatInfo != activeChat.value?.chatInfo }
|
.filter { it != null && it?.chatInfo != activeChat.value?.chatInfo }
|
||||||
.collect {
|
.collect {
|
||||||
activeChat.value = it
|
activeChat.value = it
|
||||||
|
Log.d(TAG, "TODOCHAT: chats: activeChatId became ${activeChat.value?.id}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -146,6 +148,7 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: suspend (chatId:
|
|||||||
},
|
},
|
||||||
attachmentOption,
|
attachmentOption,
|
||||||
attachmentBottomSheetState,
|
attachmentBottomSheetState,
|
||||||
|
chatModel.chatItems,
|
||||||
searchText,
|
searchText,
|
||||||
useLinkPreviews = useLinkPreviews,
|
useLinkPreviews = useLinkPreviews,
|
||||||
linkMode = chatModel.simplexLinkMode.value,
|
linkMode = chatModel.simplexLinkMode.value,
|
||||||
@@ -223,17 +226,19 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: suspend (chatId:
|
|||||||
loadPrevMessages = {
|
loadPrevMessages = {
|
||||||
if (chatModel.chatId.value != activeChat.value?.id) return@ChatLayout
|
if (chatModel.chatId.value != activeChat.value?.id) return@ChatLayout
|
||||||
val c = chatModel.getChat(chatModel.chatId.value ?: return@ChatLayout)
|
val c = chatModel.getChat(chatModel.chatId.value ?: return@ChatLayout)
|
||||||
val firstId = chatModel.chatItems.value.firstOrNull()?.id
|
val firstId = chatModel.chatItems.firstOrNull()?.id
|
||||||
if (c != null && firstId != null) {
|
if (c != null && firstId != null) {
|
||||||
withBGApi {
|
withBGApi {
|
||||||
|
Log.d(TAG, "TODOCHAT: loadPrevMessages: loading for ${c.id}, current chatId ${ChatModel.chatId.value}, size was ${ChatModel.chatItems.size}")
|
||||||
apiLoadPrevMessages(c, chatModel, firstId, searchText.value)
|
apiLoadPrevMessages(c, chatModel, firstId, searchText.value)
|
||||||
|
Log.d(TAG, "TODOCHAT: loadPrevMessages: loaded for ${c.id}, current chatId ${ChatModel.chatId.value}, size now ${ChatModel.chatItems.size}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
deleteMessage = { itemId, mode ->
|
deleteMessage = { itemId, mode ->
|
||||||
withBGApi {
|
withBGApi {
|
||||||
val cInfo = chat.chatInfo
|
val cInfo = chat.chatInfo
|
||||||
val toDeleteItem = chatModel.chatItems.value.firstOrNull { it.id == itemId }
|
val toDeleteItem = chatModel.chatItems.firstOrNull { it.id == itemId }
|
||||||
val toModerate = toDeleteItem?.memberToModerate(chat.chatInfo)
|
val toModerate = toDeleteItem?.memberToModerate(chat.chatInfo)
|
||||||
val groupInfo = toModerate?.first
|
val groupInfo = toModerate?.first
|
||||||
val groupMember = toModerate?.second
|
val groupMember = toModerate?.second
|
||||||
@@ -301,9 +306,7 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: suspend (chatId:
|
|||||||
withBGApi {
|
withBGApi {
|
||||||
val cInfo = chat.chatInfo
|
val cInfo = chat.chatInfo
|
||||||
if (cInfo is ChatInfo.Direct) {
|
if (cInfo is ChatInfo.Direct) {
|
||||||
val contactInfo = chatModel.controller.apiContactInfo(chat.remoteHostId, cInfo.contact.contactId)
|
chatModel.activeCall.value = Call(remoteHostId = chatRh, contact = cInfo.contact, callState = CallState.WaitCapabilities, localMedia = media)
|
||||||
val profile = contactInfo?.second ?: chatModel.currentUser.value?.profile?.toProfile() ?: return@withBGApi
|
|
||||||
chatModel.activeCall.value = Call(remoteHostId = chatRh, contact = cInfo.contact, callState = CallState.WaitCapabilities, localMedia = media, userProfile = profile)
|
|
||||||
chatModel.showCallView.value = true
|
chatModel.showCallView.value = true
|
||||||
chatModel.callCommand.add(WCallCommand.Capabilities(media))
|
chatModel.callCommand.add(WCallCommand.Capabilities(media))
|
||||||
}
|
}
|
||||||
@@ -401,15 +404,12 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: suspend (chatId:
|
|||||||
setGroupMembers(chatRh, chat.chatInfo.groupInfo, chatModel)
|
setGroupMembers(chatRh, chat.chatInfo.groupInfo, chatModel)
|
||||||
}
|
}
|
||||||
ModalManager.end.closeModals()
|
ModalManager.end.closeModals()
|
||||||
ModalManager.end.showModalCloseable(endButtons = {
|
ModalManager.end.showModal(endButtons = {
|
||||||
ShareButton {
|
ShareButton {
|
||||||
clipboard.shareText(itemInfoShareText(chatModel, cItem, ciInfo, chatModel.controller.appPrefs.developerTools.get()))
|
clipboard.shareText(itemInfoShareText(chatModel, cItem, ciInfo, chatModel.controller.appPrefs.developerTools.get()))
|
||||||
}
|
}
|
||||||
}) { close ->
|
}) {
|
||||||
ChatItemInfoView(chatModel, cItem, ciInfo, devTools = chatModel.controller.appPrefs.developerTools.get())
|
ChatItemInfoView(chatModel, cItem, ciInfo, devTools = chatModel.controller.appPrefs.developerTools.get())
|
||||||
KeyChangeEffect(chatModel.chatId.value) {
|
|
||||||
close()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -495,6 +495,7 @@ fun ChatLayout(
|
|||||||
composeView: (@Composable () -> Unit),
|
composeView: (@Composable () -> Unit),
|
||||||
attachmentOption: MutableState<AttachmentOption?>,
|
attachmentOption: MutableState<AttachmentOption?>,
|
||||||
attachmentBottomSheetState: ModalBottomSheetState,
|
attachmentBottomSheetState: ModalBottomSheetState,
|
||||||
|
chatItems: List<ChatItem>,
|
||||||
searchValue: State<String>,
|
searchValue: State<String>,
|
||||||
useLinkPreviews: Boolean,
|
useLinkPreviews: Boolean,
|
||||||
linkMode: SimplexLinkMode,
|
linkMode: SimplexLinkMode,
|
||||||
@@ -581,7 +582,7 @@ fun ChatLayout(
|
|||||||
.padding(contentPadding)
|
.padding(contentPadding)
|
||||||
) {
|
) {
|
||||||
ChatItemsList(
|
ChatItemsList(
|
||||||
chat, unreadCount, composeState, searchValue,
|
chat, unreadCount, composeState, chatItems, searchValue,
|
||||||
useLinkPreviews, linkMode, showMemberInfo, loadPrevMessages, deleteMessage, deleteMessages,
|
useLinkPreviews, linkMode, showMemberInfo, loadPrevMessages, deleteMessage, deleteMessages,
|
||||||
receiveFile, cancelFile, joinGroup, acceptCall, acceptFeature, openDirectChat,
|
receiveFile, cancelFile, joinGroup, acceptCall, acceptFeature, openDirectChat,
|
||||||
updateContactStats, updateMemberStats, syncContactConnection, syncMemberConnection, findModelChat, findModelMember,
|
updateContactStats, updateMemberStats, syncContactConnection, syncMemberConnection, findModelChat, findModelMember,
|
||||||
@@ -646,7 +647,7 @@ fun ChatInfoToolbar(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (chat.chatInfo is ChatInfo.Direct && chat.chatInfo.contact.mergedPreferences.calls.enabled.forUser) {
|
if (chat.chatInfo is ChatInfo.Direct && chat.chatInfo.contact.allowsFeature(ChatFeature.Calls)) {
|
||||||
if (activeCall == null) {
|
if (activeCall == null) {
|
||||||
barButtons.add {
|
barButtons.add {
|
||||||
if (appPlatform.isAndroid) {
|
if (appPlatform.isAndroid) {
|
||||||
@@ -675,7 +676,7 @@ fun ChatInfoToolbar(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (activeCall?.contact?.id == chat.id && appPlatform.isDesktop) {
|
} else if (activeCall?.contact?.id == chat.id) {
|
||||||
barButtons.add {
|
barButtons.add {
|
||||||
val call = remember { chatModel.activeCall }.value
|
val call = remember { chatModel.activeCall }.value
|
||||||
val connectedAt = call?.connectedAt
|
val connectedAt = call?.connectedAt
|
||||||
@@ -839,6 +840,7 @@ fun BoxWithConstraintsScope.ChatItemsList(
|
|||||||
chat: Chat,
|
chat: Chat,
|
||||||
unreadCount: State<Int>,
|
unreadCount: State<Int>,
|
||||||
composeState: MutableState<ComposeState>,
|
composeState: MutableState<ComposeState>,
|
||||||
|
chatItems: List<ChatItem>,
|
||||||
searchValue: State<String>,
|
searchValue: State<String>,
|
||||||
useLinkPreviews: Boolean,
|
useLinkPreviews: Boolean,
|
||||||
linkMode: SimplexLinkMode,
|
linkMode: SimplexLinkMode,
|
||||||
@@ -867,7 +869,7 @@ fun BoxWithConstraintsScope.ChatItemsList(
|
|||||||
) {
|
) {
|
||||||
val listState = rememberLazyListState()
|
val listState = rememberLazyListState()
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
ScrollToBottom(chat.id, listState, chatModel.chatItems)
|
ScrollToBottom(chat.id, listState, chatItems)
|
||||||
var prevSearchEmptiness by rememberSaveable { mutableStateOf(searchValue.value.isEmpty()) }
|
var prevSearchEmptiness by rememberSaveable { mutableStateOf(searchValue.value.isEmpty()) }
|
||||||
// Scroll to bottom when search value changes from something to nothing and back
|
// Scroll to bottom when search value changes from something to nothing and back
|
||||||
LaunchedEffect(searchValue.value.isEmpty()) {
|
LaunchedEffect(searchValue.value.isEmpty()) {
|
||||||
@@ -884,7 +886,7 @@ fun BoxWithConstraintsScope.ChatItemsList(
|
|||||||
PreloadItems(listState, ChatPagination.UNTIL_PRELOAD_COUNT, loadPrevMessages)
|
PreloadItems(listState, ChatPagination.UNTIL_PRELOAD_COUNT, loadPrevMessages)
|
||||||
|
|
||||||
Spacer(Modifier.size(8.dp))
|
Spacer(Modifier.size(8.dp))
|
||||||
val reversedChatItems by remember { derivedStateOf { chatModel.chatItems.asReversed() } }
|
val reversedChatItems by remember { derivedStateOf { chatItems.reversed().toList() } }
|
||||||
val maxHeightRounded = with(LocalDensity.current) { maxHeight.roundToPx() }
|
val maxHeightRounded = with(LocalDensity.current) { maxHeight.roundToPx() }
|
||||||
val scrollToItem: (Long) -> Unit = { itemId: Long ->
|
val scrollToItem: (Long) -> Unit = { itemId: Long ->
|
||||||
val index = reversedChatItems.indexOfFirst { it.id == itemId }
|
val index = reversedChatItems.indexOfFirst { it.id == itemId }
|
||||||
@@ -937,7 +939,7 @@ fun BoxWithConstraintsScope.ChatItemsList(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
val provider = {
|
val provider = {
|
||||||
providerForGallery(i, chatModel.chatItems.value, cItem.id) { indexInReversed ->
|
providerForGallery(i, chatItems, cItem.id) { indexInReversed ->
|
||||||
scope.launch {
|
scope.launch {
|
||||||
listState.scrollToItem(
|
listState.scrollToItem(
|
||||||
kotlin.math.min(reversedChatItems.lastIndex, indexInReversed + 1),
|
kotlin.math.min(reversedChatItems.lastIndex, indexInReversed + 1),
|
||||||
@@ -1060,11 +1062,11 @@ fun BoxWithConstraintsScope.ChatItemsList(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
FloatingButtons(chatModel.chatItems, unreadCount, chat.chatStats.minUnreadItemId, searchValue, markRead, setFloatingButton, listState)
|
FloatingButtons(chatItems, unreadCount, chat.chatStats.minUnreadItemId, searchValue, markRead, setFloatingButton, listState)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun ScrollToBottom(chatId: ChatId, listState: LazyListState, chatItems: State<List<ChatItem>>) {
|
private fun ScrollToBottom(chatId: ChatId, listState: LazyListState, chatItems: List<ChatItem>) {
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
// Helps to scroll to bottom after moving from Group to Direct chat
|
// Helps to scroll to bottom after moving from Group to Direct chat
|
||||||
// and prevents scrolling to bottom on orientation change
|
// and prevents scrolling to bottom on orientation change
|
||||||
@@ -1082,7 +1084,7 @@ private fun ScrollToBottom(chatId: ChatId, listState: LazyListState, chatItems:
|
|||||||
* When the first visible item (from bottom) is visible (even partially) we can autoscroll to 0 item. Or just scrollBy small distance otherwise
|
* When the first visible item (from bottom) is visible (even partially) we can autoscroll to 0 item. Or just scrollBy small distance otherwise
|
||||||
* */
|
* */
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
snapshotFlow { chatItems.value.lastOrNull()?.id }
|
snapshotFlow { chatItems.lastOrNull()?.id }
|
||||||
.distinctUntilChanged()
|
.distinctUntilChanged()
|
||||||
.filter { listState.layoutInfo.visibleItemsInfo.firstOrNull()?.key != it }
|
.filter { listState.layoutInfo.visibleItemsInfo.firstOrNull()?.key != it }
|
||||||
.collect {
|
.collect {
|
||||||
@@ -1105,7 +1107,7 @@ private fun ScrollToBottom(chatId: ChatId, listState: LazyListState, chatItems:
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun BoxWithConstraintsScope.FloatingButtons(
|
fun BoxWithConstraintsScope.FloatingButtons(
|
||||||
chatItems: State<List<ChatItem>>,
|
chatItems: List<ChatItem>,
|
||||||
unreadCount: State<Int>,
|
unreadCount: State<Int>,
|
||||||
minUnreadItemId: Long,
|
minUnreadItemId: Long,
|
||||||
searchValue: State<String>,
|
searchValue: State<String>,
|
||||||
@@ -1139,11 +1141,10 @@ fun BoxWithConstraintsScope.FloatingButtons(
|
|||||||
val bottomUnreadCount by remember {
|
val bottomUnreadCount by remember {
|
||||||
derivedStateOf {
|
derivedStateOf {
|
||||||
if (unreadCount.value == 0) return@derivedStateOf 0
|
if (unreadCount.value == 0) return@derivedStateOf 0
|
||||||
val items = chatItems.value
|
val from = chatItems.lastIndex - firstVisibleIndex - lastIndexOfVisibleItems
|
||||||
val from = items.lastIndex - firstVisibleIndex - lastIndexOfVisibleItems
|
if (chatItems.size <= from || from < 0) return@derivedStateOf 0
|
||||||
if (items.size <= from || from < 0) return@derivedStateOf 0
|
|
||||||
|
|
||||||
items.subList(from, items.size).count { it.isRcvNew }
|
chatItems.subList(from, chatItems.size).count { it.isRcvNew }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val firstVisibleOffset = (-with(LocalDensity.current) { maxHeight.roundToPx() } * 0.8).toInt()
|
val firstVisibleOffset = (-with(LocalDensity.current) { maxHeight.roundToPx() } * 0.8).toInt()
|
||||||
@@ -1189,7 +1190,7 @@ fun BoxWithConstraintsScope.FloatingButtons(
|
|||||||
painterResource(MR.images.ic_check),
|
painterResource(MR.images.ic_check),
|
||||||
onClick = {
|
onClick = {
|
||||||
markRead(
|
markRead(
|
||||||
CC.ItemRange(minUnreadItemId, chatItems.value[chatItems.size - listState.layoutInfo.visibleItemsInfo.lastIndex - 1].id - 1),
|
CC.ItemRange(minUnreadItemId, chatItems[chatItems.size - listState.layoutInfo.visibleItemsInfo.lastIndex - 1].id - 1),
|
||||||
bottomUnreadCount
|
bottomUnreadCount
|
||||||
)
|
)
|
||||||
showDropDown.value = false
|
showDropDown.value = false
|
||||||
@@ -1494,6 +1495,7 @@ fun PreviewChatLayout() {
|
|||||||
composeView = {},
|
composeView = {},
|
||||||
attachmentOption = remember { mutableStateOf<AttachmentOption?>(null) },
|
attachmentOption = remember { mutableStateOf<AttachmentOption?>(null) },
|
||||||
attachmentBottomSheetState = rememberModalBottomSheetState(initialValue = ModalBottomSheetValue.Hidden),
|
attachmentBottomSheetState = rememberModalBottomSheetState(initialValue = ModalBottomSheetValue.Hidden),
|
||||||
|
chatItems = chatItems,
|
||||||
searchValue,
|
searchValue,
|
||||||
useLinkPreviews = true,
|
useLinkPreviews = true,
|
||||||
linkMode = SimplexLinkMode.DESCRIPTION,
|
linkMode = SimplexLinkMode.DESCRIPTION,
|
||||||
@@ -1566,6 +1568,7 @@ fun PreviewGroupChatLayout() {
|
|||||||
composeView = {},
|
composeView = {},
|
||||||
attachmentOption = remember { mutableStateOf<AttachmentOption?>(null) },
|
attachmentOption = remember { mutableStateOf<AttachmentOption?>(null) },
|
||||||
attachmentBottomSheetState = rememberModalBottomSheetState(initialValue = ModalBottomSheetValue.Hidden),
|
attachmentBottomSheetState = rememberModalBottomSheetState(initialValue = ModalBottomSheetValue.Hidden),
|
||||||
|
chatItems = chatItems,
|
||||||
searchValue,
|
searchValue,
|
||||||
useLinkPreviews = true,
|
useLinkPreviews = true,
|
||||||
linkMode = SimplexLinkMode.DESCRIPTION,
|
linkMode = SimplexLinkMode.DESCRIPTION,
|
||||||
|
|||||||
+3
-7
@@ -267,7 +267,7 @@ fun ComposeView(
|
|||||||
fun loadLinkPreview(url: String, wait: Long? = null) {
|
fun loadLinkPreview(url: String, wait: Long? = null) {
|
||||||
if (pendingLinkUrl.value == url) {
|
if (pendingLinkUrl.value == url) {
|
||||||
composeState.value = composeState.value.copy(preview = ComposePreview.CLinkPreview(null))
|
composeState.value = composeState.value.copy(preview = ComposePreview.CLinkPreview(null))
|
||||||
withLongRunningApi(slow = 60_000) {
|
withLongRunningApi(slow = 30_000, deadlock = 60_000) {
|
||||||
if (wait != null) delay(wait)
|
if (wait != null) delay(wait)
|
||||||
val lp = getLinkPreview(url)
|
val lp = getLinkPreview(url)
|
||||||
if (lp != null && pendingLinkUrl.value == url) {
|
if (lp != null && pendingLinkUrl.value == url) {
|
||||||
@@ -551,7 +551,7 @@ fun ComposeView(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun sendMessage(ttl: Int?) {
|
fun sendMessage(ttl: Int?) {
|
||||||
withLongRunningApi(slow = 120_000) {
|
withLongRunningApi(slow = 30_000, deadlock = 60_000) {
|
||||||
sendMessageAsync(null, false, ttl)
|
sendMessageAsync(null, false, ttl)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -583,10 +583,6 @@ fun ComposeView(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun cancelLinkPreview() {
|
fun cancelLinkPreview() {
|
||||||
val pendingLink = pendingLinkUrl.value
|
|
||||||
if (pendingLink != null) {
|
|
||||||
cancelledLinks.add(pendingLink)
|
|
||||||
}
|
|
||||||
val uri = composeState.value.linkPreview?.uri
|
val uri = composeState.value.linkPreview?.uri
|
||||||
if (uri != null) {
|
if (uri != null) {
|
||||||
cancelledLinks.add(uri)
|
cancelledLinks.add(uri)
|
||||||
@@ -665,7 +661,7 @@ fun ComposeView(
|
|||||||
|
|
||||||
fun editPrevMessage() {
|
fun editPrevMessage() {
|
||||||
if (composeState.value.contextItem != ComposeContextItem.NoContextItem || composeState.value.preview != ComposePreview.NoPreview) return
|
if (composeState.value.contextItem != ComposeContextItem.NoContextItem || composeState.value.preview != ComposePreview.NoPreview) return
|
||||||
val lastEditable = chatModel.chatItems.value.findLast { it.meta.editable }
|
val lastEditable = chatModel.chatItems.findLast { it.meta.editable }
|
||||||
if (lastEditable != null) {
|
if (lastEditable != null) {
|
||||||
composeState.value = ComposeState(editingItem = lastEditable, useLinkPreviews = useLinkPreviews)
|
composeState.value = ComposeState(editingItem = lastEditable, useLinkPreviews = useLinkPreviews)
|
||||||
}
|
}
|
||||||
|
|||||||
+89
-36
@@ -59,6 +59,14 @@ fun SendMsgView(
|
|||||||
) {
|
) {
|
||||||
val showCustomDisappearingMessageDialog = remember { mutableStateOf(false) }
|
val showCustomDisappearingMessageDialog = remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
if (showCustomDisappearingMessageDialog.value) {
|
||||||
|
CustomDisappearingMessageDialog(
|
||||||
|
sendMessage = sendMessage,
|
||||||
|
setShowDialog = { showCustomDisappearingMessageDialog.value = it },
|
||||||
|
customDisappearingMessageTimePref = customDisappearingMessageTimePref
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
Box(Modifier.padding(vertical = 8.dp)) {
|
Box(Modifier.padding(vertical = 8.dp)) {
|
||||||
val cs = composeState.value
|
val cs = composeState.value
|
||||||
var progressByTimeout by rememberSaveable { mutableStateOf(false) }
|
var progressByTimeout by rememberSaveable { mutableStateOf(false) }
|
||||||
@@ -195,11 +203,6 @@ fun SendMsgView(
|
|||||||
DefaultDropdownMenu(showDropdown) {
|
DefaultDropdownMenu(showDropdown) {
|
||||||
menuItems.forEach { composable -> composable() }
|
menuItems.forEach { composable -> composable() }
|
||||||
}
|
}
|
||||||
CustomDisappearingMessageDialog(
|
|
||||||
showCustomDisappearingMessageDialog,
|
|
||||||
sendMessage = sendMessage,
|
|
||||||
customDisappearingMessageTimePref = customDisappearingMessageTimePref
|
|
||||||
)
|
|
||||||
} else {
|
} else {
|
||||||
SendMsgButton(icon, sendButtonSize, sendButtonAlpha, sendButtonColor, !sendMsgButtonDisabled, sendMessage)
|
SendMsgButton(icon, sendButtonSize, sendButtonAlpha, sendButtonColor, !sendMsgButtonDisabled, sendMessage)
|
||||||
}
|
}
|
||||||
@@ -217,43 +220,93 @@ expect fun VoiceButtonWithoutPermissionByPlatform()
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun CustomDisappearingMessageDialog(
|
private fun CustomDisappearingMessageDialog(
|
||||||
showMenu: MutableState<Boolean>,
|
|
||||||
sendMessage: (Int?) -> Unit,
|
sendMessage: (Int?) -> Unit,
|
||||||
|
setShowDialog: (Boolean) -> Unit,
|
||||||
customDisappearingMessageTimePref: SharedPreference<Int>?
|
customDisappearingMessageTimePref: SharedPreference<Int>?
|
||||||
) {
|
) {
|
||||||
DefaultDropdownMenu(showMenu) {
|
val showCustomTimePicker = remember { mutableStateOf(false) }
|
||||||
Text(
|
|
||||||
generalGetString(MR.strings.send_disappearing_message),
|
|
||||||
Modifier.padding(vertical = DEFAULT_PADDING_HALF, horizontal = DEFAULT_PADDING * 1.5f),
|
|
||||||
fontSize = 16.sp,
|
|
||||||
color = MaterialTheme.colors.secondary
|
|
||||||
)
|
|
||||||
|
|
||||||
ItemAction(generalGetString(MR.strings.send_disappearing_message_30_seconds)) {
|
if (showCustomTimePicker.value) {
|
||||||
sendMessage(30)
|
val selectedDisappearingMessageTime = remember {
|
||||||
showMenu.value = false
|
mutableStateOf(customDisappearingMessageTimePref?.get?.invoke() ?: 300)
|
||||||
}
|
}
|
||||||
ItemAction(generalGetString(MR.strings.send_disappearing_message_1_minute)) {
|
CustomTimePickerDialog(
|
||||||
sendMessage(60)
|
selectedDisappearingMessageTime,
|
||||||
showMenu.value = false
|
title = generalGetString(MR.strings.delete_after),
|
||||||
|
confirmButtonText = generalGetString(MR.strings.send_disappearing_message_send),
|
||||||
|
confirmButtonAction = { ttl ->
|
||||||
|
sendMessage(ttl)
|
||||||
|
customDisappearingMessageTimePref?.set?.invoke(ttl)
|
||||||
|
setShowDialog(false)
|
||||||
|
},
|
||||||
|
cancel = { setShowDialog(false) }
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
@Composable
|
||||||
|
fun ChoiceButton(
|
||||||
|
text: String,
|
||||||
|
onClick: () -> Unit
|
||||||
|
) {
|
||||||
|
TextButton(onClick) {
|
||||||
|
Text(
|
||||||
|
text,
|
||||||
|
fontSize = 18.sp,
|
||||||
|
color = MaterialTheme.colors.primary
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
ItemAction(generalGetString(MR.strings.send_disappearing_message_5_minutes)) {
|
|
||||||
sendMessage(300)
|
DefaultDialog(onDismissRequest = { setShowDialog(false) }) {
|
||||||
showMenu.value = false
|
Surface(
|
||||||
}
|
shape = RoundedCornerShape(corner = CornerSize(25.dp)),
|
||||||
ItemAction(generalGetString(MR.strings.send_disappearing_message_custom_time)) {
|
contentColor = LocalContentColor.current
|
||||||
showMenu.value = false
|
) {
|
||||||
val selectedDisappearingMessageTime = mutableStateOf(customDisappearingMessageTimePref?.get?.invoke() ?: 300)
|
Box(
|
||||||
showCustomTimePickerDialog(
|
contentAlignment = Alignment.Center
|
||||||
selectedDisappearingMessageTime,
|
) {
|
||||||
title = generalGetString(MR.strings.delete_after),
|
Column(
|
||||||
confirmButtonText = generalGetString(MR.strings.send_disappearing_message_send),
|
modifier = Modifier.padding(DEFAULT_PADDING),
|
||||||
confirmButtonAction = { ttl ->
|
verticalArrangement = Arrangement.spacedBy(6.dp),
|
||||||
sendMessage(ttl)
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
customDisappearingMessageTimePref?.set?.invoke(ttl)
|
) {
|
||||||
},
|
Row(
|
||||||
cancel = { showMenu.value = false }
|
modifier = Modifier.fillMaxWidth(),
|
||||||
)
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Text(" ") // centers title
|
||||||
|
Text(
|
||||||
|
generalGetString(MR.strings.send_disappearing_message),
|
||||||
|
fontSize = 16.sp,
|
||||||
|
color = MaterialTheme.colors.secondary
|
||||||
|
)
|
||||||
|
Icon(
|
||||||
|
painterResource(MR.images.ic_close),
|
||||||
|
generalGetString(MR.strings.icon_descr_close_button),
|
||||||
|
tint = MaterialTheme.colors.secondary,
|
||||||
|
modifier = Modifier
|
||||||
|
.size(25.dp)
|
||||||
|
.clickable { setShowDialog(false) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
ChoiceButton(generalGetString(MR.strings.send_disappearing_message_30_seconds)) {
|
||||||
|
sendMessage(30)
|
||||||
|
setShowDialog(false)
|
||||||
|
}
|
||||||
|
ChoiceButton(generalGetString(MR.strings.send_disappearing_message_1_minute)) {
|
||||||
|
sendMessage(60)
|
||||||
|
setShowDialog(false)
|
||||||
|
}
|
||||||
|
ChoiceButton(generalGetString(MR.strings.send_disappearing_message_5_minutes)) {
|
||||||
|
sendMessage(300)
|
||||||
|
setShowDialog(false)
|
||||||
|
}
|
||||||
|
ChoiceButton(generalGetString(MR.strings.send_disappearing_message_custom_time)) {
|
||||||
|
showCustomTimePicker.value = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-2
@@ -54,7 +54,7 @@ fun AddGroupMembersView(rhId: Long?, groupInfo: GroupInfo, creatingGroup: Boolea
|
|||||||
},
|
},
|
||||||
inviteMembers = {
|
inviteMembers = {
|
||||||
allowModifyMembers = false
|
allowModifyMembers = false
|
||||||
withLongRunningApi(slow = 120_000) {
|
withLongRunningApi(slow = 30_000, deadlock = 60_000) {
|
||||||
for (contactId in selectedContacts) {
|
for (contactId in selectedContacts) {
|
||||||
val member = chatModel.controller.apiAddMember(rhId, groupInfo.groupId, contactId, selectedRole.value)
|
val member = chatModel.controller.apiAddMember(rhId, groupInfo.groupId, contactId, selectedRole.value)
|
||||||
if (member != null) {
|
if (member != null) {
|
||||||
@@ -86,7 +86,7 @@ fun getContactsToAdd(chatModel: ChatModel, search: String): List<Contact> {
|
|||||||
.map { it.chatInfo }
|
.map { it.chatInfo }
|
||||||
.filterIsInstance<ChatInfo.Direct>()
|
.filterIsInstance<ChatInfo.Direct>()
|
||||||
.map { it.contact }
|
.map { it.contact }
|
||||||
.filter { c -> c.ready && c.active && c.contactId !in memberContactIds && c.chatViewName.lowercase().contains(s) }
|
.filter { it.contactId !in memberContactIds && it.chatViewName.lowercase().contains(s) }
|
||||||
.sortedBy { it.displayName.lowercase() }
|
.sortedBy { it.displayName.lowercase() }
|
||||||
.toList()
|
.toList()
|
||||||
}
|
}
|
||||||
|
|||||||
+61
-39
@@ -152,7 +152,7 @@ fun leaveGroupDialog(rhId: Long?, groupInfo: GroupInfo, chatModel: ChatModel, cl
|
|||||||
text = generalGetString(MR.strings.you_will_stop_receiving_messages_from_this_group_chat_history_will_be_preserved),
|
text = generalGetString(MR.strings.you_will_stop_receiving_messages_from_this_group_chat_history_will_be_preserved),
|
||||||
confirmText = generalGetString(MR.strings.leave_group_button),
|
confirmText = generalGetString(MR.strings.leave_group_button),
|
||||||
onConfirm = {
|
onConfirm = {
|
||||||
withLongRunningApi(60_000) {
|
withBGApi {
|
||||||
chatModel.controller.leaveGroup(rhId, groupInfo.groupId)
|
chatModel.controller.leaveGroup(rhId, groupInfo.groupId)
|
||||||
close?.invoke()
|
close?.invoke()
|
||||||
}
|
}
|
||||||
@@ -424,47 +424,69 @@ private fun MemberVerifiedShield() {
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun DropDownMenuForMember(rhId: Long?, member: GroupMember, groupInfo: GroupInfo, showMenu: MutableState<Boolean>) {
|
private fun DropDownMenuForMember(rhId: Long?, member: GroupMember, groupInfo: GroupInfo, showMenu: MutableState<Boolean>) {
|
||||||
if (groupInfo.membership.memberRole >= GroupMemberRole.Admin) {
|
// revert from this:
|
||||||
val canBlockForAll = member.canBlockForAll(groupInfo)
|
DefaultDropdownMenu(showMenu) {
|
||||||
val canRemove = member.canBeRemoved(groupInfo)
|
if (member.canBeRemoved(groupInfo)) {
|
||||||
if (canBlockForAll || canRemove) {
|
ItemAction(stringResource(MR.strings.remove_member_button), painterResource(MR.images.ic_delete), color = MaterialTheme.colors.error, onClick = {
|
||||||
DefaultDropdownMenu(showMenu) {
|
removeMemberAlert(rhId, groupInfo, member)
|
||||||
if (canBlockForAll) {
|
showMenu.value = false
|
||||||
if (member.blockedByAdmin) {
|
})
|
||||||
ItemAction(stringResource(MR.strings.unblock_for_all), painterResource(MR.images.ic_do_not_touch), onClick = {
|
|
||||||
unblockForAllAlert(rhId, groupInfo, member)
|
|
||||||
showMenu.value = false
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
ItemAction(stringResource(MR.strings.block_for_all), painterResource(MR.images.ic_back_hand), color = MaterialTheme.colors.error, onClick = {
|
|
||||||
blockForAllAlert(rhId, groupInfo, member)
|
|
||||||
showMenu.value = false
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (canRemove) {
|
|
||||||
ItemAction(stringResource(MR.strings.remove_member_button), painterResource(MR.images.ic_delete), color = MaterialTheme.colors.error, onClick = {
|
|
||||||
removeMemberAlert(rhId, groupInfo, member)
|
|
||||||
showMenu.value = false
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} else if (!member.blockedByAdmin) {
|
if (member.memberSettings.showMessages) {
|
||||||
DefaultDropdownMenu(showMenu) {
|
ItemAction(stringResource(MR.strings.block_member_button), painterResource(MR.images.ic_back_hand), color = MaterialTheme.colors.error, onClick = {
|
||||||
if (member.memberSettings.showMessages) {
|
blockMemberAlert(rhId, groupInfo, member)
|
||||||
ItemAction(stringResource(MR.strings.block_member_button), painterResource(MR.images.ic_back_hand), color = MaterialTheme.colors.error, onClick = {
|
showMenu.value = false
|
||||||
blockMemberAlert(rhId, groupInfo, member)
|
})
|
||||||
showMenu.value = false
|
} else {
|
||||||
})
|
ItemAction(stringResource(MR.strings.unblock_member_button), painterResource(MR.images.ic_do_not_touch), onClick = {
|
||||||
} else {
|
unblockMemberAlert(rhId, groupInfo, member)
|
||||||
ItemAction(stringResource(MR.strings.unblock_member_button), painterResource(MR.images.ic_do_not_touch), onClick = {
|
showMenu.value = false
|
||||||
unblockMemberAlert(rhId, groupInfo, member)
|
})
|
||||||
showMenu.value = false
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// revert to this: vvv
|
||||||
|
// if (groupInfo.membership.memberRole >= GroupMemberRole.Admin) {
|
||||||
|
// val canBlockForAll = member.canBlockForAll(groupInfo)
|
||||||
|
// val canRemove = member.canBeRemoved(groupInfo)
|
||||||
|
// if (canBlockForAll || canRemove) {
|
||||||
|
// DefaultDropdownMenu(showMenu) {
|
||||||
|
// if (canBlockForAll) {
|
||||||
|
// if (member.blockedByAdmin) {
|
||||||
|
// ItemAction(stringResource(MR.strings.unblock_for_all), painterResource(MR.images.ic_do_not_touch), onClick = {
|
||||||
|
// unblockForAllAlert(rhId, groupInfo, member)
|
||||||
|
// showMenu.value = false
|
||||||
|
// })
|
||||||
|
// } else {
|
||||||
|
// ItemAction(stringResource(MR.strings.block_for_all), painterResource(MR.images.ic_back_hand), color = MaterialTheme.colors.error, onClick = {
|
||||||
|
// blockForAllAlert(rhId, groupInfo, member)
|
||||||
|
// showMenu.value = false
|
||||||
|
// })
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// if (canRemove) {
|
||||||
|
// ItemAction(stringResource(MR.strings.remove_member_button), painterResource(MR.images.ic_delete), color = MaterialTheme.colors.error, onClick = {
|
||||||
|
// removeMemberAlert(rhId, groupInfo, member)
|
||||||
|
// showMenu.value = false
|
||||||
|
// })
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// } else if (!member.blockedByAdmin) {
|
||||||
|
// DefaultDropdownMenu(showMenu) {
|
||||||
|
// if (member.memberSettings.showMessages) {
|
||||||
|
// ItemAction(stringResource(MR.strings.block_member_button), painterResource(MR.images.ic_back_hand), color = MaterialTheme.colors.error, onClick = {
|
||||||
|
// blockMemberAlert(rhId, groupInfo, member)
|
||||||
|
// showMenu.value = false
|
||||||
|
// })
|
||||||
|
// } else {
|
||||||
|
// ItemAction(stringResource(MR.strings.unblock_member_button), painterResource(MR.images.ic_do_not_touch), onClick = {
|
||||||
|
// unblockMemberAlert(rhId, groupInfo, member)
|
||||||
|
// showMenu.value = false
|
||||||
|
// })
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// ^^^
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
|
|||||||
+22
-5
@@ -3,9 +3,11 @@ package chat.simplex.common.views.chat.group
|
|||||||
import InfoRow
|
import InfoRow
|
||||||
import SectionBottomSpacer
|
import SectionBottomSpacer
|
||||||
import SectionDividerSpaced
|
import SectionDividerSpaced
|
||||||
|
import SectionItemView
|
||||||
import SectionSpacer
|
import SectionSpacer
|
||||||
import SectionTextFooter
|
import SectionTextFooter
|
||||||
import SectionView
|
import SectionView
|
||||||
|
import TextIconSpaced
|
||||||
import androidx.compose.desktop.ui.tooling.preview.Preview
|
import androidx.compose.desktop.ui.tooling.preview.Preview
|
||||||
import java.net.URI
|
import java.net.URI
|
||||||
import androidx.compose.foundation.*
|
import androidx.compose.foundation.*
|
||||||
@@ -72,8 +74,9 @@ fun GroupMemberInfoView(
|
|||||||
if (chatModel.getContactChat(it) == null) {
|
if (chatModel.getContactChat(it) == null) {
|
||||||
chatModel.addChat(c)
|
chatModel.addChat(c)
|
||||||
}
|
}
|
||||||
|
chatModel.chatItems.clear()
|
||||||
chatModel.chatItemStatuses.clear()
|
chatModel.chatItemStatuses.clear()
|
||||||
chatModel.chatItems.replaceAll(c.chatItems)
|
chatModel.chatItems.addAll(c.chatItems)
|
||||||
chatModel.chatId.value = c.id
|
chatModel.chatId.value = c.id
|
||||||
closeAll()
|
closeAll()
|
||||||
}
|
}
|
||||||
@@ -387,11 +390,25 @@ fun GroupMemberInfoLayout(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (groupInfo.membership.memberRole >= GroupMemberRole.Admin) {
|
// revert from this:
|
||||||
AdminDestructiveSection()
|
SectionDividerSpaced(maxBottomPadding = false)
|
||||||
} else {
|
SectionView {
|
||||||
NonAdminBlockSection()
|
if (member.memberSettings.showMessages) {
|
||||||
|
BlockMemberButton(blockMember)
|
||||||
|
} else {
|
||||||
|
UnblockMemberButton(unblockMember)
|
||||||
|
}
|
||||||
|
if (member.canBeRemoved(groupInfo)) {
|
||||||
|
RemoveMemberButton(removeMember)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
// revert to this: vvv
|
||||||
|
// if (groupInfo.membership.memberRole >= GroupMemberRole.Admin) {
|
||||||
|
// AdminDestructiveSection()
|
||||||
|
// } else {
|
||||||
|
// NonAdminBlockSection()
|
||||||
|
// }
|
||||||
|
// ^^^
|
||||||
|
|
||||||
if (developerTools) {
|
if (developerTools) {
|
||||||
SectionDividerSpaced()
|
SectionDividerSpaced()
|
||||||
|
|||||||
+4
-44
@@ -3,7 +3,6 @@ package chat.simplex.common.views.chat.group
|
|||||||
import SectionBottomSpacer
|
import SectionBottomSpacer
|
||||||
import SectionDividerSpaced
|
import SectionDividerSpaced
|
||||||
import SectionItemView
|
import SectionItemView
|
||||||
import SectionTextFooter
|
|
||||||
import SectionView
|
import SectionView
|
||||||
import TextIconSpaced
|
import TextIconSpaced
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
@@ -15,7 +14,6 @@ import androidx.compose.runtime.*
|
|||||||
import androidx.compose.runtime.saveable.rememberSaveable
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.focus.FocusRequester
|
import androidx.compose.ui.focus.FocusRequester
|
||||||
import androidx.compose.ui.graphics.Color
|
|
||||||
import androidx.compose.ui.platform.LocalClipboardManager
|
import androidx.compose.ui.platform.LocalClipboardManager
|
||||||
import androidx.compose.ui.platform.LocalUriHandler
|
import androidx.compose.ui.platform.LocalUriHandler
|
||||||
import androidx.compose.ui.text.AnnotatedString
|
import androidx.compose.ui.text.AnnotatedString
|
||||||
@@ -29,13 +27,9 @@ import chat.simplex.common.views.chat.item.MarkdownText
|
|||||||
import chat.simplex.common.views.helpers.*
|
import chat.simplex.common.views.helpers.*
|
||||||
import chat.simplex.common.model.ChatModel
|
import chat.simplex.common.model.ChatModel
|
||||||
import chat.simplex.common.model.GroupInfo
|
import chat.simplex.common.model.GroupInfo
|
||||||
import chat.simplex.common.platform.chatJsonLength
|
|
||||||
import chat.simplex.common.ui.theme.DEFAULT_PADDING_HALF
|
|
||||||
import chat.simplex.res.MR
|
import chat.simplex.res.MR
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
|
|
||||||
private const val maxByteCount = 1200
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun GroupWelcomeView(m: ChatModel, rhId: Long?, groupInfo: GroupInfo, close: () -> Unit) {
|
fun GroupWelcomeView(m: ChatModel, rhId: Long?, groupInfo: GroupInfo, close: () -> Unit) {
|
||||||
var gInfo by remember { mutableStateOf(groupInfo) }
|
var gInfo by remember { mutableStateOf(groupInfo) }
|
||||||
@@ -60,11 +54,8 @@ fun GroupWelcomeView(m: ChatModel, rhId: Long?, groupInfo: GroupInfo, close: ()
|
|||||||
|
|
||||||
ModalView(
|
ModalView(
|
||||||
close = {
|
close = {
|
||||||
when {
|
if (welcomeText.value == gInfo.groupProfile.description || (welcomeText.value == "" && gInfo.groupProfile.description == null)) close()
|
||||||
welcomeTextUnchanged(welcomeText, gInfo) -> close()
|
else showUnsavedChangesAlert({ save(close) }, close)
|
||||||
!welcomeTextFitsLimit(welcomeText) -> showUnsavedChangesTooLongAlert(close)
|
|
||||||
else -> showUnsavedChangesAlert({ save(close) }, close)
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
) {
|
) {
|
||||||
GroupWelcomeLayout(
|
GroupWelcomeLayout(
|
||||||
@@ -76,14 +67,6 @@ fun GroupWelcomeView(m: ChatModel, rhId: Long?, groupInfo: GroupInfo, close: ()
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun welcomeTextUnchanged(welcomeText: MutableState<String>, groupInfo: GroupInfo): Boolean {
|
|
||||||
return welcomeText.value == groupInfo.groupProfile.description || (welcomeText.value == "" && groupInfo.groupProfile.description == null)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun welcomeTextFitsLimit(welcomeText: MutableState<String>): Boolean {
|
|
||||||
return chatJsonLength(welcomeText.value) <= maxByteCount
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun GroupWelcomeLayout(
|
private fun GroupWelcomeLayout(
|
||||||
welcomeText: MutableState<String>,
|
welcomeText: MutableState<String>,
|
||||||
@@ -112,13 +95,6 @@ private fun GroupWelcomeLayout(
|
|||||||
} else {
|
} else {
|
||||||
TextPreview(wt.value, linkMode)
|
TextPreview(wt.value, linkMode)
|
||||||
}
|
}
|
||||||
SectionTextFooter(
|
|
||||||
if (!welcomeTextFitsLimit(wt)) { generalGetString(MR.strings.message_too_large) } else "",
|
|
||||||
color = if (welcomeTextFitsLimit(wt)) MaterialTheme.colors.secondary else Color.Red
|
|
||||||
)
|
|
||||||
|
|
||||||
Spacer(Modifier.size(8.dp))
|
|
||||||
|
|
||||||
ChangeModeButton(
|
ChangeModeButton(
|
||||||
editMode.value,
|
editMode.value,
|
||||||
click = {
|
click = {
|
||||||
@@ -128,18 +104,10 @@ private fun GroupWelcomeLayout(
|
|||||||
)
|
)
|
||||||
val clipboard = LocalClipboardManager.current
|
val clipboard = LocalClipboardManager.current
|
||||||
CopyTextButton { clipboard.setText(AnnotatedString(wt.value)) }
|
CopyTextButton { clipboard.setText(AnnotatedString(wt.value)) }
|
||||||
|
SectionDividerSpaced(maxBottomPadding = false)
|
||||||
Divider(
|
|
||||||
Modifier.padding(
|
|
||||||
start = DEFAULT_PADDING_HALF,
|
|
||||||
top = 8.dp,
|
|
||||||
end = DEFAULT_PADDING_HALF,
|
|
||||||
bottom = 8.dp)
|
|
||||||
)
|
|
||||||
|
|
||||||
SaveButton(
|
SaveButton(
|
||||||
save = save,
|
save = save,
|
||||||
disabled = welcomeTextUnchanged(wt, groupInfo) || !welcomeTextFitsLimit(wt)
|
disabled = wt.value == groupInfo.groupProfile.description || (wt.value == "" && groupInfo.groupProfile.description == null)
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
val clipboard = LocalClipboardManager.current
|
val clipboard = LocalClipboardManager.current
|
||||||
@@ -214,11 +182,3 @@ private fun showUnsavedChangesAlert(save: () -> Unit, revert: () -> Unit) {
|
|||||||
onDismiss = revert,
|
onDismiss = revert,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun showUnsavedChangesTooLongAlert(revert: () -> Unit) {
|
|
||||||
AlertManager.shared.showAlertDialogStacked(
|
|
||||||
title = generalGetString(MR.strings.welcome_message_is_too_long),
|
|
||||||
confirmText = generalGetString(MR.strings.exit_without_saving),
|
|
||||||
onConfirm = revert,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|||||||
+1
-1
@@ -94,7 +94,7 @@ fun CIFileView(
|
|||||||
FileProtocol.LOCAL -> {}
|
FileProtocol.LOCAL -> {}
|
||||||
}
|
}
|
||||||
file.fileStatus is CIFileStatus.RcvComplete || (file.fileStatus is CIFileStatus.SndStored && file.fileProtocol == FileProtocol.LOCAL) -> {
|
file.fileStatus is CIFileStatus.RcvComplete || (file.fileStatus is CIFileStatus.SndStored && file.fileProtocol == FileProtocol.LOCAL) -> {
|
||||||
withLongRunningApi(slow = 600_000) {
|
withLongRunningApi(slow = 60_000, deadlock = 600_000) {
|
||||||
var filePath = getLoadedFilePath(file)
|
var filePath = getLoadedFilePath(file)
|
||||||
if (chatModel.connectedToRemote() && filePath == null) {
|
if (chatModel.connectedToRemote() && filePath == null) {
|
||||||
file.loadRemoteFile(true)
|
file.loadRemoteFile(true)
|
||||||
|
|||||||
+1
-1
@@ -41,7 +41,7 @@ fun CIVideoView(
|
|||||||
val filePath = remember(file, CIFile.cachedRemoteFileRequests.toList()) { mutableStateOf(getLoadedFilePath(file)) }
|
val filePath = remember(file, CIFile.cachedRemoteFileRequests.toList()) { mutableStateOf(getLoadedFilePath(file)) }
|
||||||
if (chatModel.connectedToRemote()) {
|
if (chatModel.connectedToRemote()) {
|
||||||
LaunchedEffect(file) {
|
LaunchedEffect(file) {
|
||||||
withLongRunningApi(slow = 600_000) {
|
withLongRunningApi(slow = 60_000, deadlock = 600_000) {
|
||||||
if (file != null && file.loaded && getLoadedFilePath(file) == null) {
|
if (file != null && file.loaded && getLoadedFilePath(file) == null) {
|
||||||
file.loadRemoteFile(false)
|
file.loadRemoteFile(false)
|
||||||
filePath.value = getLoadedFilePath(file)
|
filePath.value = getLoadedFilePath(file)
|
||||||
|
|||||||
+5
-23
@@ -103,7 +103,7 @@ fun ChatItemView(
|
|||||||
setReaction(cInfo, cItem, !r.userReacted, r.reaction)
|
setReaction(cInfo, cItem, !r.userReacted, r.reaction)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Row(modifier.padding(2.dp), verticalAlignment = Alignment.CenterVertically) {
|
Row(modifier.padding(2.dp)) {
|
||||||
ReactionIcon(r.reaction.text, fontSize = 12.sp)
|
ReactionIcon(r.reaction.text, fontSize = 12.sp)
|
||||||
if (r.totalReacted > 1) {
|
if (r.totalReacted > 1) {
|
||||||
Spacer(Modifier.width(4.dp))
|
Spacer(Modifier.width(4.dp))
|
||||||
@@ -112,6 +112,7 @@ fun ChatItemView(
|
|||||||
fontSize = 11.5.sp,
|
fontSize = 11.5.sp,
|
||||||
fontWeight = if (r.userReacted) FontWeight.Bold else FontWeight.Normal,
|
fontWeight = if (r.userReacted) FontWeight.Bold else FontWeight.Normal,
|
||||||
color = if (r.userReacted) MaterialTheme.colors.primary else MaterialTheme.colors.secondary,
|
color = if (r.userReacted) MaterialTheme.colors.primary else MaterialTheme.colors.secondary,
|
||||||
|
modifier = if (appPlatform.isAndroid) Modifier else Modifier.padding(top = 4.dp)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -177,8 +178,7 @@ fun ChatItemView(
|
|||||||
fun MsgContentItemDropdownMenu() {
|
fun MsgContentItemDropdownMenu() {
|
||||||
val saveFileLauncher = rememberSaveFileLauncher(ciFile = cItem.file)
|
val saveFileLauncher = rememberSaveFileLauncher(ciFile = cItem.file)
|
||||||
when {
|
when {
|
||||||
// cItem.id check is a special case for live message chat item which has negative ID while not sent yet
|
cItem.content.msgContent != null -> {
|
||||||
cItem.content.msgContent != null && cItem.id >= 0 -> {
|
|
||||||
DefaultDropdownMenu(showMenu) {
|
DefaultDropdownMenu(showMenu) {
|
||||||
if (cInfo.featureEnabled(ChatFeature.Reactions) && cItem.allowAddReaction) {
|
if (cInfo.featureEnabled(ChatFeature.Reactions) && cItem.allowAddReaction) {
|
||||||
MsgReactionsMenu()
|
MsgReactionsMenu()
|
||||||
@@ -213,7 +213,7 @@ fun ChatItemView(
|
|||||||
showMenu.value = false
|
showMenu.value = false
|
||||||
}
|
}
|
||||||
if (chatModel.connectedToRemote() && fileSource == null) {
|
if (chatModel.connectedToRemote() && fileSource == null) {
|
||||||
withLongRunningApi(slow = 600_000) {
|
withLongRunningApi(slow = 60_000, deadlock = 600_000) {
|
||||||
cItem.file?.loadRemoteFile(true)
|
cItem.file?.loadRemoteFile(true)
|
||||||
fileSource = getLoadedFileSource(cItem.file)
|
fileSource = getLoadedFileSource(cItem.file)
|
||||||
shareIfExists()
|
shareIfExists()
|
||||||
@@ -527,9 +527,8 @@ fun DeleteItemAction(
|
|||||||
val range = chatViewItemsRange(currIndex, prevHidden)
|
val range = chatViewItemsRange(currIndex, prevHidden)
|
||||||
if (range != null) {
|
if (range != null) {
|
||||||
val itemIds: ArrayList<Long> = arrayListOf()
|
val itemIds: ArrayList<Long> = arrayListOf()
|
||||||
val reversedChatItems = chatModel.chatItems.asReversed()
|
|
||||||
for (i in range) {
|
for (i in range) {
|
||||||
itemIds.add(reversedChatItems[i].id)
|
itemIds.add(chatModel.chatItems.asReversed()[i].id)
|
||||||
}
|
}
|
||||||
deleteMessagesAlertDialog(itemIds, generalGetString(MR.strings.delete_message_mark_deleted_warning), deleteMessages = deleteMessages)
|
deleteMessagesAlertDialog(itemIds, generalGetString(MR.strings.delete_message_mark_deleted_warning), deleteMessages = deleteMessages)
|
||||||
} else {
|
} else {
|
||||||
@@ -652,23 +651,6 @@ fun ItemAction(text: String, icon: ImageVector, onClick: () -> Unit, color: Colo
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun ItemAction(text: String, color: Color = Color.Unspecified, onClick: () -> Unit) {
|
|
||||||
val finalColor = if (color == Color.Unspecified) {
|
|
||||||
MenuTextColor
|
|
||||||
} else color
|
|
||||||
DropdownMenuItem(onClick, contentPadding = PaddingValues(horizontal = DEFAULT_PADDING * 1.5f)) {
|
|
||||||
Text(
|
|
||||||
text,
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.weight(1F)
|
|
||||||
.padding(end = 15.dp),
|
|
||||||
color = finalColor
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun cancelFileAlertDialog(fileId: Long, cancelFile: (Long) -> Unit, cancelAction: CancelAction) {
|
fun cancelFileAlertDialog(fileId: Long, cancelFile: (Long) -> Unit, cancelAction: CancelAction) {
|
||||||
AlertManager.shared.showAlertDialog(
|
AlertManager.shared.showAlertDialog(
|
||||||
title = generalGetString(cancelAction.alert.titleId),
|
title = generalGetString(cancelAction.alert.titleId),
|
||||||
|
|||||||
+1
-1
@@ -91,7 +91,7 @@ private fun MergedMarkedDeletedText(chatItem: ChatItem, revealed: MutableState<B
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun markedDeletedText(meta: CIMeta): String =
|
private fun markedDeletedText(meta: CIMeta): String =
|
||||||
when (meta.itemDeleted) {
|
when (meta.itemDeleted) {
|
||||||
is CIDeleted.Moderated ->
|
is CIDeleted.Moderated ->
|
||||||
String.format(generalGetString(MR.strings.moderated_item_description), meta.itemDeleted.byGroupMember.displayName)
|
String.format(generalGetString(MR.strings.moderated_item_description), meta.itemDeleted.byGroupMember.displayName)
|
||||||
|
|||||||
+6
-2
@@ -212,15 +212,18 @@ suspend fun openGroupChat(rhId: Long?, groupId: Long, chatModel: ChatModel) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
suspend fun openChat(rhId: Long?, chatInfo: ChatInfo, chatModel: ChatModel) {
|
suspend fun openChat(rhId: Long?, chatInfo: ChatInfo, chatModel: ChatModel) {
|
||||||
|
Log.d(TAG, "TODOCHAT: openChat: opening ${chatInfo.id}, current chatId ${ChatModel.chatId.value}, size ${ChatModel.chatItems.size}")
|
||||||
val chat = chatModel.controller.apiGetChat(rhId, chatInfo.chatType, chatInfo.apiId)
|
val chat = chatModel.controller.apiGetChat(rhId, chatInfo.chatType, chatInfo.apiId)
|
||||||
if (chat != null) {
|
if (chat != null) {
|
||||||
openLoadedChat(chat, chatModel)
|
openLoadedChat(chat, chatModel)
|
||||||
|
Log.d(TAG, "TODOCHAT: openChat: opened ${chatInfo.id}, current chatId ${ChatModel.chatId.value}, size ${ChatModel.chatItems.size}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun openLoadedChat(chat: Chat, chatModel: ChatModel) {
|
fun openLoadedChat(chat: Chat, chatModel: ChatModel) {
|
||||||
|
chatModel.chatItems.clear()
|
||||||
chatModel.chatItemStatuses.clear()
|
chatModel.chatItemStatuses.clear()
|
||||||
chatModel.chatItems.replaceAll(chat.chatItems)
|
chatModel.chatItems.addAll(chat.chatItems)
|
||||||
chatModel.chatId.value = chat.chatInfo.id
|
chatModel.chatId.value = chat.chatInfo.id
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -236,7 +239,8 @@ suspend fun apiFindMessages(ch: Chat, chatModel: ChatModel, search: String) {
|
|||||||
val chatInfo = ch.chatInfo
|
val chatInfo = ch.chatInfo
|
||||||
val chat = chatModel.controller.apiGetChat(ch.remoteHostId, chatInfo.chatType, chatInfo.apiId, search = search) ?: return
|
val chat = chatModel.controller.apiGetChat(ch.remoteHostId, chatInfo.chatType, chatInfo.apiId, search = search) ?: return
|
||||||
if (chatModel.chatId.value != chat.id) return
|
if (chatModel.chatId.value != chat.id) return
|
||||||
chatModel.chatItems.replaceAll(chat.chatItems)
|
chatModel.chatItems.clear()
|
||||||
|
chatModel.chatItems.addAll(0, chat.chatItems)
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun setGroupMembers(rhId: Long?, groupInfo: GroupInfo, chatModel: ChatModel) {
|
suspend fun setGroupMembers(rhId: Long?, groupInfo: GroupInfo, chatModel: ChatModel) {
|
||||||
|
|||||||
+2
-8
@@ -29,7 +29,6 @@ import chat.simplex.common.views.onboarding.WhatsNewView
|
|||||||
import chat.simplex.common.views.onboarding.shouldShowWhatsNew
|
import chat.simplex.common.views.onboarding.shouldShowWhatsNew
|
||||||
import chat.simplex.common.views.usersettings.SettingsView
|
import chat.simplex.common.views.usersettings.SettingsView
|
||||||
import chat.simplex.common.platform.*
|
import chat.simplex.common.platform.*
|
||||||
import chat.simplex.common.views.call.Call
|
|
||||||
import chat.simplex.common.views.newchat.*
|
import chat.simplex.common.views.newchat.*
|
||||||
import chat.simplex.res.MR
|
import chat.simplex.res.MR
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.*
|
||||||
@@ -122,12 +121,7 @@ fun ChatListView(chatModel: ChatModel, settingsState: SettingsViewState, setPerf
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (searchText.value.text.isEmpty()) {
|
if (searchText.value.text.isEmpty()) {
|
||||||
if (appPlatform.isDesktop) {
|
DesktopActiveCallOverlayLayout(newChatSheetState)
|
||||||
val call = remember { chatModel.activeCall }.value
|
|
||||||
if (call != null) {
|
|
||||||
ActiveCallInteractiveArea(call, newChatSheetState)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// TODO disable this button and sheet for the duration of the switch
|
// TODO disable this button and sheet for the duration of the switch
|
||||||
tryOrShowError("NewChatSheet", error = {}) {
|
tryOrShowError("NewChatSheet", error = {}) {
|
||||||
NewChatSheet(chatModel, newChatSheetState, stopped, hideNewChatSheet)
|
NewChatSheet(chatModel, newChatSheetState, stopped, hideNewChatSheet)
|
||||||
@@ -320,7 +314,7 @@ private fun ToggleFilterDisabledButton() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
expect fun ActiveCallInteractiveArea(call: Call, newChatSheetState: MutableStateFlow<AnimatedViewState>)
|
expect fun DesktopActiveCallOverlayLayout(newChatSheetState: MutableStateFlow<AnimatedViewState>)
|
||||||
|
|
||||||
fun connectIfOpenedViaUri(rhId: Long?, uri: URI, chatModel: ChatModel) {
|
fun connectIfOpenedViaUri(rhId: Long?, uri: URI, chatModel: ChatModel) {
|
||||||
Log.d(TAG, "connectIfOpenedViaUri: opened via link")
|
Log.d(TAG, "connectIfOpenedViaUri: opened via link")
|
||||||
|
|||||||
+2
-3
@@ -26,7 +26,6 @@ import chat.simplex.common.views.helpers.*
|
|||||||
import chat.simplex.common.model.*
|
import chat.simplex.common.model.*
|
||||||
import chat.simplex.common.model.GroupInfo
|
import chat.simplex.common.model.GroupInfo
|
||||||
import chat.simplex.common.platform.chatModel
|
import chat.simplex.common.platform.chatModel
|
||||||
import chat.simplex.common.views.chat.item.markedDeletedText
|
|
||||||
import chat.simplex.res.MR
|
import chat.simplex.res.MR
|
||||||
import dev.icerock.moko.resources.ImageResource
|
import dev.icerock.moko.resources.ImageResource
|
||||||
|
|
||||||
@@ -171,7 +170,7 @@ fun ChatPreviewView(
|
|||||||
val (text: CharSequence, inlineTextContent) = when {
|
val (text: CharSequence, inlineTextContent) = when {
|
||||||
chatModelDraftChatId == chat.id && chatModelDraft != null -> remember(chatModelDraft) { messageDraft(chatModelDraft) }
|
chatModelDraftChatId == chat.id && chatModelDraft != null -> remember(chatModelDraft) { messageDraft(chatModelDraft) }
|
||||||
ci.meta.itemDeleted == null -> ci.text to null
|
ci.meta.itemDeleted == null -> ci.text to null
|
||||||
else -> markedDeletedText(ci.meta) to null
|
else -> generalGetString(MR.strings.marked_deleted_description) to null
|
||||||
}
|
}
|
||||||
val formattedText = when {
|
val formattedText = when {
|
||||||
chatModelDraftChatId == chat.id && chatModelDraft != null -> null
|
chatModelDraftChatId == chat.id && chatModelDraft != null -> null
|
||||||
@@ -287,7 +286,7 @@ fun ChatPreviewView(
|
|||||||
Box(
|
Box(
|
||||||
contentAlignment = Alignment.TopEnd
|
contentAlignment = Alignment.TopEnd
|
||||||
) {
|
) {
|
||||||
val ts = chat.chatItems.lastOrNull()?.timestampText ?: getTimestampText(chat.chatInfo.chatTs)
|
val ts = chat.chatItems.lastOrNull()?.timestampText ?: getTimestampText(chat.chatInfo.updatedAt)
|
||||||
Text(
|
Text(
|
||||||
ts,
|
ts,
|
||||||
color = MaterialTheme.colors.secondary,
|
color = MaterialTheme.colors.secondary,
|
||||||
|
|||||||
+2
-2
@@ -85,7 +85,7 @@ private fun ShareListToolbar(chatModel: ChatModel, userPickerState: MutableState
|
|||||||
userPickerState.value = AnimatedViewState.VISIBLE
|
userPickerState.value = AnimatedViewState.VISIBLE
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else -> NavigationButtonBack(onButtonClicked = { chatModel.sharedContent.value = null })
|
else -> NavigationButtonBack { chatModel.sharedContent.value = null }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (chatModel.chats.size >= 8) {
|
if (chatModel.chats.size >= 8) {
|
||||||
@@ -143,7 +143,7 @@ private fun ShareList(chatModel: ChatModel, search: String) {
|
|||||||
}
|
}
|
||||||
val chats by remember(search) {
|
val chats by remember(search) {
|
||||||
derivedStateOf {
|
derivedStateOf {
|
||||||
if (search.isEmpty()) chatModel.chats.toList().filter { it.chatInfo.ready } else chatModel.chats.toList().filter { it.chatInfo.ready }.filter(filter)
|
if (search.isEmpty()) chatModel.chats.filter { it.chatInfo.ready } else chatModel.chats.filter { it.chatInfo.ready }.filter(filter)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
LazyColumn(
|
LazyColumn(
|
||||||
|
|||||||
+8
-7
@@ -62,7 +62,7 @@ fun DatabaseEncryptionView(m: ChatModel) {
|
|||||||
initialRandomDBPassphrase,
|
initialRandomDBPassphrase,
|
||||||
progressIndicator,
|
progressIndicator,
|
||||||
onConfirmEncrypt = {
|
onConfirmEncrypt = {
|
||||||
withLongRunningApi {
|
withLongRunningApi(slow = 30_000, deadlock = 60_000) {
|
||||||
encryptDatabase(currentKey, newKey, confirmNewKey, initialRandomDBPassphrase, useKeychain, storedKey, progressIndicator)
|
encryptDatabase(currentKey, newKey, confirmNewKey, initialRandomDBPassphrase, useKeychain, storedKey, progressIndicator)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -233,13 +233,13 @@ fun resetFormAfterEncryption(
|
|||||||
storedKey: MutableState<Boolean>,
|
storedKey: MutableState<Boolean>,
|
||||||
stored: Boolean = false,
|
stored: Boolean = false,
|
||||||
) {
|
) {
|
||||||
|
m.chatDbEncrypted.value = true
|
||||||
|
initialRandomDBPassphrase.value = false
|
||||||
|
m.controller.appPrefs.initialRandomDBPassphrase.set(false)
|
||||||
currentKey.value = ""
|
currentKey.value = ""
|
||||||
newKey.value = ""
|
newKey.value = ""
|
||||||
confirmNewKey.value = ""
|
confirmNewKey.value = ""
|
||||||
storedKey.value = stored
|
storedKey.value = stored
|
||||||
m.chatDbEncrypted.value = true
|
|
||||||
initialRandomDBPassphrase.value = false
|
|
||||||
m.controller.appPrefs.initialRandomDBPassphrase.set(false)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setUseKeychain(value: Boolean, useKeychain: MutableState<Boolean>, prefs: AppPreferences) {
|
fun setUseKeychain(value: Boolean, useKeychain: MutableState<Boolean>, prefs: AppPreferences) {
|
||||||
@@ -392,11 +392,12 @@ suspend fun encryptDatabase(
|
|||||||
false
|
false
|
||||||
}
|
}
|
||||||
else -> {
|
else -> {
|
||||||
val new = newKey.value
|
prefs.initialRandomDBPassphrase.set(false)
|
||||||
resetFormAfterEncryption(m, initialRandomDBPassphrase, currentKey, newKey, confirmNewKey, storedKey, useKeychain.value)
|
initialRandomDBPassphrase.value = false
|
||||||
if (useKeychain.value) {
|
if (useKeychain.value) {
|
||||||
DatabaseUtils.ksDatabasePassword.set(new)
|
DatabaseUtils.ksDatabasePassword.set(newKey.value)
|
||||||
}
|
}
|
||||||
|
resetFormAfterEncryption(m, initialRandomDBPassphrase, currentKey, newKey, confirmNewKey, storedKey, useKeychain.value)
|
||||||
operationEnded(m, progressIndicator) {
|
operationEnded(m, progressIndicator) {
|
||||||
AlertManager.shared.showAlertMsg(generalGetString(MR.strings.database_encrypted))
|
AlertManager.shared.showAlertMsg(generalGetString(MR.strings.database_encrypted))
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-2
@@ -368,7 +368,7 @@ fun chatArchiveTitle(chatArchiveTime: Instant, chatLastStart: Instant): String {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun startChat(m: ChatModel, chatLastStart: MutableState<Instant?>, chatDbChanged: MutableState<Boolean>, progressIndicator: MutableState<Boolean>? = null) {
|
fun startChat(m: ChatModel, chatLastStart: MutableState<Instant?>, chatDbChanged: MutableState<Boolean>, progressIndicator: MutableState<Boolean>? = null) {
|
||||||
withLongRunningApi {
|
withLongRunningApi(slow = 30_000, deadlock = 60_000) {
|
||||||
try {
|
try {
|
||||||
progressIndicator?.value = true
|
progressIndicator?.value = true
|
||||||
if (chatDbChanged.value) {
|
if (chatDbChanged.value) {
|
||||||
@@ -581,7 +581,7 @@ private fun importArchive(
|
|||||||
progressIndicator.value = true
|
progressIndicator.value = true
|
||||||
val archivePath = saveArchiveFromURI(importedArchiveURI)
|
val archivePath = saveArchiveFromURI(importedArchiveURI)
|
||||||
if (archivePath != null) {
|
if (archivePath != null) {
|
||||||
withLongRunningApi {
|
withLongRunningApi(slow = 60_000, deadlock = 180_000) {
|
||||||
try {
|
try {
|
||||||
m.controller.apiDeleteStorage()
|
m.controller.apiDeleteStorage()
|
||||||
try {
|
try {
|
||||||
|
|||||||
+1
-18
@@ -12,7 +12,6 @@ import androidx.compose.ui.Alignment
|
|||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.focus.*
|
import androidx.compose.ui.focus.*
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.platform.LocalClipboardManager
|
|
||||||
import androidx.compose.ui.platform.LocalDensity
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
import androidx.compose.ui.text.AnnotatedString
|
import androidx.compose.ui.text.AnnotatedString
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
@@ -23,8 +22,6 @@ import chat.simplex.common.ui.theme.*
|
|||||||
import chat.simplex.res.MR
|
import chat.simplex.res.MR
|
||||||
import dev.icerock.moko.resources.StringResource
|
import dev.icerock.moko.resources.StringResource
|
||||||
import dev.icerock.moko.resources.compose.painterResource
|
import dev.icerock.moko.resources.compose.painterResource
|
||||||
import dev.icerock.moko.resources.compose.stringResource
|
|
||||||
import kotlinx.coroutines.delay
|
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
|
||||||
class AlertManager {
|
class AlertManager {
|
||||||
@@ -131,8 +128,6 @@ class AlertManager {
|
|||||||
) {
|
) {
|
||||||
val focusRequester = remember { FocusRequester() }
|
val focusRequester = remember { FocusRequester() }
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
// Wait before focusing to prevent auto-confirming if a user used Enter key on hardware keyboard
|
|
||||||
delay(200)
|
|
||||||
focusRequester.requestFocus()
|
focusRequester.requestFocus()
|
||||||
}
|
}
|
||||||
TextButton(onClick = {
|
TextButton(onClick = {
|
||||||
@@ -191,7 +186,6 @@ class AlertManager {
|
|||||||
title: String, text: String? = null,
|
title: String, text: String? = null,
|
||||||
confirmText: String = generalGetString(MR.strings.ok),
|
confirmText: String = generalGetString(MR.strings.ok),
|
||||||
hostDevice: Pair<Long?, String>? = null,
|
hostDevice: Pair<Long?, String>? = null,
|
||||||
shareText: Boolean? = null
|
|
||||||
) {
|
) {
|
||||||
showAlert {
|
showAlert {
|
||||||
AlertDialog(
|
AlertDialog(
|
||||||
@@ -201,23 +195,12 @@ class AlertManager {
|
|||||||
AlertContent(text, hostDevice, extraPadding = true) {
|
AlertContent(text, hostDevice, extraPadding = true) {
|
||||||
val focusRequester = remember { FocusRequester() }
|
val focusRequester = remember { FocusRequester() }
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
// Wait before focusing to prevent auto-confirming if a user used Enter key on hardware keyboard
|
|
||||||
delay(200)
|
|
||||||
focusRequester.requestFocus()
|
focusRequester.requestFocus()
|
||||||
}
|
}
|
||||||
// Can pass shareText = false to prevent showing Share button if it's needed in a specific case
|
|
||||||
val showShareButton = text != null && (shareText == true || (shareText == null && text.length > 500))
|
|
||||||
Row(
|
Row(
|
||||||
Modifier.fillMaxWidth().padding(horizontal = DEFAULT_PADDING),
|
Modifier.fillMaxWidth().padding(horizontal = DEFAULT_PADDING),
|
||||||
horizontalArrangement = if (showShareButton) Arrangement.SpaceBetween else Arrangement.Center
|
horizontalArrangement = Arrangement.Center
|
||||||
) {
|
) {
|
||||||
val clipboard = LocalClipboardManager.current
|
|
||||||
if (showShareButton && text != null) {
|
|
||||||
TextButton(onClick = {
|
|
||||||
clipboard.shareText(text)
|
|
||||||
hideAlert()
|
|
||||||
}) { Text(stringResource(MR.strings.share_verb)) }
|
|
||||||
}
|
|
||||||
TextButton(
|
TextButton(
|
||||||
onClick = {
|
onClick = {
|
||||||
hideAlert()
|
hideAlert()
|
||||||
|
|||||||
+2
-2
@@ -27,8 +27,8 @@ fun ChatInfoImage(chatInfo: ChatInfo, size: Dp, iconColor: Color = MaterialTheme
|
|||||||
val icon =
|
val icon =
|
||||||
when (chatInfo) {
|
when (chatInfo) {
|
||||||
is ChatInfo.Group -> MR.images.ic_supervised_user_circle_filled
|
is ChatInfo.Group -> MR.images.ic_supervised_user_circle_filled
|
||||||
is ChatInfo.Local -> MR.images.ic_folder_filled
|
is ChatInfo.Direct -> MR.images.ic_account_circle_filled
|
||||||
else -> MR.images.ic_account_circle_filled
|
else -> MR.images.ic_folder_filled
|
||||||
}
|
}
|
||||||
ProfileImage(size, chatInfo.image, icon, if (chatInfo is ChatInfo.Local) NoteFolderIconColor else iconColor)
|
ProfileImage(size, chatInfo.image, icon, if (chatInfo is ChatInfo.Local) NoteFolderIconColor else iconColor)
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-2
@@ -18,7 +18,7 @@ import chat.simplex.res.MR
|
|||||||
import dev.icerock.moko.resources.compose.painterResource
|
import dev.icerock.moko.resources.compose.painterResource
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun CloseSheetBar(close: (() -> Unit)?, showClose: Boolean = true, tintColor: Color = if (close != null) MaterialTheme.colors.primary else MaterialTheme.colors.secondary, endButtons: @Composable RowScope.() -> Unit = {}) {
|
fun CloseSheetBar(close: (() -> Unit)?, showClose: Boolean = true, endButtons: @Composable RowScope.() -> Unit = {}) {
|
||||||
Column(
|
Column(
|
||||||
Modifier
|
Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
@@ -35,7 +35,7 @@ fun CloseSheetBar(close: (() -> Unit)?, showClose: Boolean = true, tintColor: Co
|
|||||||
verticalAlignment = Alignment.CenterVertically
|
verticalAlignment = Alignment.CenterVertically
|
||||||
) {
|
) {
|
||||||
if (showClose) {
|
if (showClose) {
|
||||||
NavigationButtonBack(tintColor = tintColor, onButtonClicked = close)
|
NavigationButtonBack(onButtonClicked = close)
|
||||||
} else {
|
} else {
|
||||||
Spacer(Modifier)
|
Spacer(Modifier)
|
||||||
}
|
}
|
||||||
|
|||||||
+167
-37
@@ -1,21 +1,116 @@
|
|||||||
package chat.simplex.common.views.helpers
|
package chat.simplex.common.views.helpers
|
||||||
|
|
||||||
import SectionItemView
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.foundation.shape.CornerSize
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material.*
|
import androidx.compose.material.*
|
||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.*
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
import dev.icerock.moko.resources.compose.painterResource
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
import androidx.compose.ui.window.Dialog
|
||||||
|
import chat.simplex.common.ui.theme.DEFAULT_PADDING
|
||||||
import chat.simplex.common.model.CustomTimeUnit
|
import chat.simplex.common.model.CustomTimeUnit
|
||||||
import chat.simplex.common.model.timeText
|
import chat.simplex.common.model.timeText
|
||||||
import chat.simplex.res.MR
|
import chat.simplex.res.MR
|
||||||
|
import com.sd.lib.compose.wheel_picker.*
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
expect fun CustomTimePicker(
|
fun CustomTimePicker(
|
||||||
selection: MutableState<Int>,
|
selection: MutableState<Int>,
|
||||||
timeUnitsLimits: List<TimeUnitLimits> = TimeUnitLimits.defaultUnitsLimits
|
timeUnitsLimits: List<TimeUnitLimits> = TimeUnitLimits.defaultUnitsLimits
|
||||||
)
|
) {
|
||||||
|
fun getUnitValues(unit: CustomTimeUnit, selectedValue: Int): List<Int> {
|
||||||
|
val unitLimits = timeUnitsLimits.firstOrNull { it.timeUnit == unit } ?: TimeUnitLimits.defaultUnitLimits(unit)
|
||||||
|
val regularUnitValues = (unitLimits.minValue..unitLimits.maxValue).toList()
|
||||||
|
return regularUnitValues + if (regularUnitValues.contains(selectedValue)) emptyList() else listOf(selectedValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
val (unit, duration) = CustomTimeUnit.toTimeUnit(selection.value)
|
||||||
|
val selectedUnit: MutableState<CustomTimeUnit> = remember { mutableStateOf(unit) }
|
||||||
|
val selectedDuration = remember { mutableStateOf(duration) }
|
||||||
|
val selectedUnitValues = remember { mutableStateOf(getUnitValues(selectedUnit.value, selectedDuration.value)) }
|
||||||
|
val isTriggered = remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
LaunchedEffect(selectedUnit.value) {
|
||||||
|
// on initial composition, if passed selection doesn't fit into picker bounds, so that selectedDuration is bigger than selectedUnit maxValue
|
||||||
|
// (e.g., for selection = 121 seconds: selectedUnit would be Second, selectedDuration would be 121 > selectedUnit maxValue of 120),
|
||||||
|
// selectedDuration would've been replaced by maxValue - isTriggered check prevents this by skipping LaunchedEffect on initial composition
|
||||||
|
if (isTriggered.value) {
|
||||||
|
val maxValue = timeUnitsLimits.firstOrNull { it.timeUnit == selectedUnit.value }?.maxValue
|
||||||
|
if (maxValue != null && selectedDuration.value > maxValue) {
|
||||||
|
selectedDuration.value = maxValue
|
||||||
|
selectedUnitValues.value = getUnitValues(selectedUnit.value, selectedDuration.value)
|
||||||
|
} else {
|
||||||
|
selectedUnitValues.value = getUnitValues(selectedUnit.value, selectedDuration.value)
|
||||||
|
selection.value = selectedUnit.value.toSeconds * selectedDuration.value
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
isTriggered.value = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LaunchedEffect(selectedDuration.value) {
|
||||||
|
selection.value = selectedUnit.value.toSeconds * selectedDuration.value
|
||||||
|
}
|
||||||
|
|
||||||
|
Row(
|
||||||
|
Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = DEFAULT_PADDING),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(0.dp)
|
||||||
|
) {
|
||||||
|
Column(Modifier.weight(1f)) {
|
||||||
|
val durationPickerState = rememberFWheelPickerState(selectedUnitValues.value.indexOf(selectedDuration.value))
|
||||||
|
FVerticalWheelPicker(
|
||||||
|
count = selectedUnitValues.value.count(),
|
||||||
|
state = durationPickerState,
|
||||||
|
unfocusedCount = 2,
|
||||||
|
focus = {
|
||||||
|
FWheelPickerFocusVertical(dividerColor = MaterialTheme.colors.primary)
|
||||||
|
}
|
||||||
|
) { index ->
|
||||||
|
Text(
|
||||||
|
selectedUnitValues.value[index].toString(),
|
||||||
|
fontSize = 18.sp,
|
||||||
|
color = MaterialTheme.colors.primary
|
||||||
|
)
|
||||||
|
}
|
||||||
|
LaunchedEffect(durationPickerState) {
|
||||||
|
snapshotFlow { durationPickerState.currentIndex }
|
||||||
|
.collect {
|
||||||
|
selectedDuration.value = selectedUnitValues.value[it]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Column(Modifier.weight(1f)) {
|
||||||
|
val unitPickerState = rememberFWheelPickerState(timeUnitsLimits.indexOfFirst { it.timeUnit == selectedUnit.value })
|
||||||
|
FVerticalWheelPicker(
|
||||||
|
count = timeUnitsLimits.count(),
|
||||||
|
state = unitPickerState,
|
||||||
|
unfocusedCount = 2,
|
||||||
|
focus = {
|
||||||
|
FWheelPickerFocusVertical(dividerColor = MaterialTheme.colors.primary)
|
||||||
|
}
|
||||||
|
) { index ->
|
||||||
|
Text(
|
||||||
|
timeUnitsLimits[index].timeUnit.text,
|
||||||
|
fontSize = 18.sp,
|
||||||
|
color = MaterialTheme.colors.primary
|
||||||
|
)
|
||||||
|
}
|
||||||
|
LaunchedEffect(unitPickerState) {
|
||||||
|
snapshotFlow { unitPickerState.currentIndex }
|
||||||
|
.collect {
|
||||||
|
selectedUnit.value = timeUnitsLimits[it].timeUnit
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
data class TimeUnitLimits(
|
data class TimeUnitLimits(
|
||||||
val timeUnit: CustomTimeUnit,
|
val timeUnit: CustomTimeUnit,
|
||||||
@@ -46,7 +141,8 @@ data class TimeUnitLimits(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun showCustomTimePickerDialog(
|
@Composable
|
||||||
|
fun CustomTimePickerDialog(
|
||||||
selection: MutableState<Int>,
|
selection: MutableState<Int>,
|
||||||
timeUnitsLimits: List<TimeUnitLimits> = TimeUnitLimits.defaultUnitsLimits,
|
timeUnitsLimits: List<TimeUnitLimits> = TimeUnitLimits.defaultUnitsLimits,
|
||||||
title: String,
|
title: String,
|
||||||
@@ -54,26 +150,53 @@ fun showCustomTimePickerDialog(
|
|||||||
confirmButtonAction: (Int) -> Unit,
|
confirmButtonAction: (Int) -> Unit,
|
||||||
cancel: () -> Unit
|
cancel: () -> Unit
|
||||||
) {
|
) {
|
||||||
AlertManager.shared.showAlertDialogButtonsColumn(
|
DefaultDialog(onDismissRequest = cancel) {
|
||||||
title = title,
|
Surface(
|
||||||
onDismissRequest = cancel
|
shape = RoundedCornerShape(corner = CornerSize(25.dp)),
|
||||||
) {
|
contentColor = LocalContentColor.current
|
||||||
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
) {
|
||||||
CustomTimePicker(
|
Box(
|
||||||
selection,
|
contentAlignment = Alignment.Center
|
||||||
timeUnitsLimits
|
|
||||||
)
|
|
||||||
SectionItemView({
|
|
||||||
AlertManager.shared.hideAlert()
|
|
||||||
confirmButtonAction(selection.value)
|
|
||||||
}
|
|
||||||
) {
|
) {
|
||||||
Text(
|
Column(
|
||||||
confirmButtonText,
|
modifier = Modifier.padding(DEFAULT_PADDING),
|
||||||
Modifier.fillMaxWidth(),
|
verticalArrangement = Arrangement.spacedBy(6.dp),
|
||||||
textAlign = TextAlign.Center,
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
color = MaterialTheme.colors.primary
|
) {
|
||||||
)
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Text(" ") // centers title
|
||||||
|
Text(
|
||||||
|
title,
|
||||||
|
fontSize = 16.sp,
|
||||||
|
color = MaterialTheme.colors.secondary
|
||||||
|
)
|
||||||
|
Icon(
|
||||||
|
painterResource(MR.images.ic_close),
|
||||||
|
generalGetString(MR.strings.icon_descr_close_button),
|
||||||
|
tint = MaterialTheme.colors.secondary,
|
||||||
|
modifier = Modifier
|
||||||
|
.size(25.dp)
|
||||||
|
.clickable { cancel() }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
CustomTimePicker(
|
||||||
|
selection,
|
||||||
|
timeUnitsLimits
|
||||||
|
)
|
||||||
|
|
||||||
|
TextButton(onClick = { confirmButtonAction(selection.value) }) {
|
||||||
|
Text(
|
||||||
|
confirmButtonText,
|
||||||
|
fontSize = 18.sp,
|
||||||
|
color = MaterialTheme.colors.primary
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -97,6 +220,7 @@ fun DropdownCustomTimePickerSettingRow(
|
|||||||
|
|
||||||
val dropdownSelection: MutableState<DropdownSelection> = remember { mutableStateOf(DropdownSelection.DropdownValue(selection.value)) }
|
val dropdownSelection: MutableState<DropdownSelection> = remember { mutableStateOf(DropdownSelection.DropdownValue(selection.value)) }
|
||||||
val values: MutableState<List<DropdownSelection>> = remember { mutableStateOf(getValues(selection.value)) }
|
val values: MutableState<List<DropdownSelection>> = remember { mutableStateOf(getValues(selection.value)) }
|
||||||
|
val showCustomTimePicker = remember { mutableStateOf(false) }
|
||||||
|
|
||||||
fun updateValue(selectedValue: Int?) {
|
fun updateValue(selectedValue: Int?) {
|
||||||
values.value = getValues(selectedValue)
|
values.value = getValues(selectedValue)
|
||||||
@@ -123,22 +247,28 @@ fun DropdownCustomTimePickerSettingRow(
|
|||||||
onSelected = { sel: DropdownSelection ->
|
onSelected = { sel: DropdownSelection ->
|
||||||
when (sel) {
|
when (sel) {
|
||||||
is DropdownSelection.DropdownValue -> updateValue(sel.value)
|
is DropdownSelection.DropdownValue -> updateValue(sel.value)
|
||||||
DropdownSelection.Custom -> {
|
DropdownSelection.Custom -> showCustomTimePicker.value = true
|
||||||
val selectedCustomTime = mutableStateOf(selection.value ?: 86400)
|
|
||||||
showCustomTimePickerDialog(
|
|
||||||
selectedCustomTime,
|
|
||||||
timeUnitsLimits = customPickerTimeUnitsLimits,
|
|
||||||
title = customPickerTitle,
|
|
||||||
confirmButtonText = customPickerConfirmButtonText,
|
|
||||||
confirmButtonAction = ::updateValue,
|
|
||||||
cancel = {
|
|
||||||
dropdownSelection.value = DropdownSelection.DropdownValue(selection.value)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if (showCustomTimePicker.value) {
|
||||||
|
val selectedCustomTime = remember { mutableStateOf(selection.value ?: 86400) }
|
||||||
|
CustomTimePickerDialog(
|
||||||
|
selectedCustomTime,
|
||||||
|
timeUnitsLimits = customPickerTimeUnitsLimits,
|
||||||
|
title = customPickerTitle,
|
||||||
|
confirmButtonText = customPickerConfirmButtonText,
|
||||||
|
confirmButtonAction = { time ->
|
||||||
|
updateValue(time)
|
||||||
|
showCustomTimePicker.value = false
|
||||||
|
},
|
||||||
|
cancel = {
|
||||||
|
dropdownSelection.value = DropdownSelection.DropdownValue(selection.value)
|
||||||
|
showCustomTimePicker.value = false
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private sealed class DropdownSelection {
|
private sealed class DropdownSelection {
|
||||||
|
|||||||
+1
-2
@@ -5,7 +5,6 @@ import androidx.compose.material.*
|
|||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import chat.simplex.common.ui.theme.DEFAULT_PADDING
|
import chat.simplex.common.ui.theme.DEFAULT_PADDING
|
||||||
|
|
||||||
@@ -21,7 +20,7 @@ fun DefaultProgressView(description: String?) {
|
|||||||
strokeWidth = 2.5.dp
|
strokeWidth = 2.5.dp
|
||||||
)
|
)
|
||||||
if (description != null) {
|
if (description != null) {
|
||||||
Text(description, textAlign = TextAlign.Center)
|
Text(description)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-2
@@ -44,10 +44,10 @@ fun DefaultTopAppBar(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun NavigationButtonBack(onButtonClicked: (() -> Unit)?, tintColor: Color = if (onButtonClicked != null) MaterialTheme.colors.primary else MaterialTheme.colors.secondary) {
|
fun NavigationButtonBack(onButtonClicked: (() -> Unit)?) {
|
||||||
IconButton(onButtonClicked ?: {}, enabled = onButtonClicked != null) {
|
IconButton(onButtonClicked ?: {}, enabled = onButtonClicked != null) {
|
||||||
Icon(
|
Icon(
|
||||||
painterResource(MR.images.ic_arrow_back_ios_new), stringResource(MR.strings.back), tint = tintColor
|
painterResource(MR.images.ic_arrow_back_ios_new), stringResource(MR.strings.back), tint = if (onButtonClicked != null) MaterialTheme.colors.primary else MaterialTheme.colors.secondary
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+3
-3
@@ -29,7 +29,7 @@ fun ModalView(
|
|||||||
}
|
}
|
||||||
Surface(Modifier.fillMaxSize(), contentColor = LocalContentColor.current) {
|
Surface(Modifier.fillMaxSize(), contentColor = LocalContentColor.current) {
|
||||||
Column(if (background != MaterialTheme.colors.background) Modifier.background(background) else Modifier.themedBackground()) {
|
Column(if (background != MaterialTheme.colors.background) Modifier.background(background) else Modifier.themedBackground()) {
|
||||||
CloseSheetBar(close, showClose, endButtons = endButtons)
|
CloseSheetBar(close, showClose, endButtons)
|
||||||
Box(modifier) { content() }
|
Box(modifier) { content() }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -61,10 +61,10 @@ class ModalManager(private val placement: ModalPlacement? = null) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun showModalCloseable(settings: Boolean = false, showClose: Boolean = true, endButtons: @Composable RowScope.() -> Unit = {}, content: @Composable ModalData.(close: () -> Unit) -> Unit) {
|
fun showModalCloseable(settings: Boolean = false, showClose: Boolean = true, content: @Composable ModalData.(close: () -> Unit) -> Unit) {
|
||||||
val data = ModalData()
|
val data = ModalData()
|
||||||
showCustomModal { close ->
|
showCustomModal { close ->
|
||||||
ModalView(close, showClose = showClose, endButtons = endButtons, content = { data.content(close) })
|
ModalView(close, showClose = showClose, content = { data.content(close) })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -16,7 +16,7 @@ class ProcessedErrors <T: AgentErrorType>(val interval: Long) {
|
|||||||
|
|
||||||
fun newError(error: T, offerRestart: Boolean) {
|
fun newError(error: T, offerRestart: Boolean) {
|
||||||
timer.cancel()
|
timer.cancel()
|
||||||
timer = withLongRunningApi(slow = 130_000) {
|
timer = withLongRunningApi(slow = 70_000, deadlock = 130_000) {
|
||||||
val delayBeforeNext = (lastShownTimestamp + interval) - System.currentTimeMillis()
|
val delayBeforeNext = (lastShownTimestamp + interval) - System.currentTimeMillis()
|
||||||
if ((lastShownOfferRestart || !offerRestart) && delayBeforeNext >= 0) {
|
if ((lastShownOfferRestart || !offerRestart) && delayBeforeNext >= 0) {
|
||||||
delay(delayBeforeNext)
|
delay(delayBeforeNext)
|
||||||
|
|||||||
+1
-2
@@ -22,7 +22,6 @@ import dev.icerock.moko.resources.compose.stringResource
|
|||||||
import androidx.compose.ui.text.TextStyle
|
import androidx.compose.ui.text.TextStyle
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.text.input.*
|
import androidx.compose.ui.text.input.*
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import chat.simplex.res.MR
|
import chat.simplex.res.MR
|
||||||
@@ -96,7 +95,7 @@ fun SearchTextField(
|
|||||||
value = searchText.value.text,
|
value = searchText.value.text,
|
||||||
innerTextField = innerTextField,
|
innerTextField = innerTextField,
|
||||||
placeholder = {
|
placeholder = {
|
||||||
Text(placeholder, maxLines = 1, overflow = TextOverflow.Ellipsis)
|
Text(placeholder)
|
||||||
},
|
},
|
||||||
trailingIcon = if (searchText.value.text.isNotEmpty()) {{
|
trailingIcon = if (searchText.value.text.isNotEmpty()) {{
|
||||||
IconButton({
|
IconButton({
|
||||||
|
|||||||
+4
-4
@@ -198,16 +198,16 @@ fun <T> SectionItemWithValue(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun SectionTextFooter(text: String, color: Color = MaterialTheme.colors.secondary) {
|
fun SectionTextFooter(text: String) {
|
||||||
SectionTextFooter(AnnotatedString(text), color = color)
|
SectionTextFooter(AnnotatedString(text))
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun SectionTextFooter(text: AnnotatedString, textAlign: TextAlign = TextAlign.Start, color: Color = MaterialTheme.colors.secondary) {
|
fun SectionTextFooter(text: AnnotatedString, textAlign: TextAlign = TextAlign.Start) {
|
||||||
Text(
|
Text(
|
||||||
text,
|
text,
|
||||||
Modifier.padding(start = DEFAULT_PADDING, end = DEFAULT_PADDING, top = DEFAULT_PADDING_HALF).fillMaxWidth(0.9F),
|
Modifier.padding(start = DEFAULT_PADDING, end = DEFAULT_PADDING, top = DEFAULT_PADDING_HALF).fillMaxWidth(0.9F),
|
||||||
color = color,
|
color = MaterialTheme.colors.secondary,
|
||||||
lineHeight = 18.sp,
|
lineHeight = 18.sp,
|
||||||
fontSize = 14.sp,
|
fontSize = 14.sp,
|
||||||
textAlign = textAlign
|
textAlign = textAlign
|
||||||
|
|||||||
+19
-9
@@ -37,22 +37,32 @@ fun withBGApi(action: suspend CoroutineScope.() -> Unit): Job =
|
|||||||
CoroutineScope(singleThreadDispatcher).launch(block = { wrapWithLogging(action, it) })
|
CoroutineScope(singleThreadDispatcher).launch(block = { wrapWithLogging(action, it) })
|
||||||
}
|
}
|
||||||
|
|
||||||
fun withLongRunningApi(slow: Long = Long.MAX_VALUE, action: suspend CoroutineScope.() -> Unit): Job =
|
fun withLongRunningApi(slow: Long = Long.MAX_VALUE, deadlock: Long = Long.MAX_VALUE, action: suspend CoroutineScope.() -> Unit): Job =
|
||||||
Exception().let {
|
Exception().let {
|
||||||
CoroutineScope(Dispatchers.Default).launch(block = { wrapWithLogging(action, it, slow = slow) })
|
CoroutineScope(Dispatchers.Default).launch(block = { wrapWithLogging(action, it, slow = slow, deadlock = deadlock) })
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun wrapWithLogging(action: suspend CoroutineScope.() -> Unit, exception: java.lang.Exception, slow: Long = 20_000) = coroutineScope {
|
suspend fun withSingleThreadContext(action: suspend CoroutineScope.() -> Unit) = withContext(singleThreadDispatcher, action)
|
||||||
|
|
||||||
|
private suspend fun wrapWithLogging(action: suspend CoroutineScope.() -> Unit, exception: java.lang.Exception, slow: Long = 10_000, deadlock: Long = 60_000) = coroutineScope {
|
||||||
val start = System.currentTimeMillis()
|
val start = System.currentTimeMillis()
|
||||||
|
val job = launch {
|
||||||
|
delay(deadlock)
|
||||||
|
Log.e(TAG, "Possible deadlock of the thread, not finished after ${deadlock / 1000}s:\n${exception.stackTraceToString()}")
|
||||||
|
AlertManager.shared.showAlertMsg(
|
||||||
|
title = generalGetString(MR.strings.possible_deadlock_title),
|
||||||
|
text = generalGetString(MR.strings.possible_deadlock_desc).format(deadlock / 1000, exception.stackTraceToString()),
|
||||||
|
)
|
||||||
|
}
|
||||||
action()
|
action()
|
||||||
val end = System.currentTimeMillis()
|
job.cancel()
|
||||||
if (end - start > slow) {
|
if (appPreferences.developerTools.get() && appPreferences.showSlowApiCalls.get()) {
|
||||||
Log.e(TAG, "Possible problem with execution of the thread, took ${(end - start) / 1000}s:\n${exception.stackTraceToString()}")
|
val end = System.currentTimeMillis()
|
||||||
if (appPreferences.developerTools.get() && appPreferences.showSlowApiCalls.get()) {
|
if (end - start > slow) {
|
||||||
|
Log.e(TAG, "Possible problem with execution of the thread, took ${(end - start) / 1000}s:\n${exception.stackTraceToString()}")
|
||||||
AlertManager.shared.showAlertMsg(
|
AlertManager.shared.showAlertMsg(
|
||||||
title = generalGetString(MR.strings.possible_slow_function_title),
|
title = generalGetString(MR.strings.possible_slow_function_title),
|
||||||
text = generalGetString(MR.strings.possible_slow_function_desc).format((end - start) / 1000, exception.stackTraceToString()),
|
text = generalGetString(MR.strings.possible_slow_function_desc).format((end - start) / 1000, exception.stackTraceToString()),
|
||||||
shareText = true
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -411,7 +421,7 @@ expect fun ByteArray.toBase64StringForPassphrase(): String
|
|||||||
|
|
||||||
// Android's default implementation that was used before multiplatform, adds non-needed characters at the end of string
|
// Android's default implementation that was used before multiplatform, adds non-needed characters at the end of string
|
||||||
// which can be bypassed by:
|
// which can be bypassed by:
|
||||||
// fun String.toByteArrayFromBase64(): ByteArray = Base64.getMimeDecoder().decode(this.trimEnd { it == '\n' || it == ' ' })
|
// fun String.toByteArrayFromBase64(): ByteArray = Base64.getDecoder().decode(this.trimEnd { it == '\n' || it == ' ' })
|
||||||
expect fun String.toByteArrayFromBase64ForPassphrase(): ByteArray
|
expect fun String.toByteArrayFromBase64ForPassphrase(): ByteArray
|
||||||
|
|
||||||
val LongRange.Companion.saver
|
val LongRange.Companion.saver
|
||||||
|
|||||||
+1
-1
@@ -49,7 +49,7 @@ fun LocalAuthView(m: ChatModel, authRequest: LocalAuthRequest) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun deleteStorageAndRestart(m: ChatModel, password: String, completed: (LAResult) -> Unit) {
|
private fun deleteStorageAndRestart(m: ChatModel, password: String, completed: (LAResult) -> Unit) {
|
||||||
withLongRunningApi {
|
withLongRunningApi(slow = 30_000, deadlock = 60_000) {
|
||||||
try {
|
try {
|
||||||
/** Waiting until [initChatController] finishes */
|
/** Waiting until [initChatController] finishes */
|
||||||
while (m.ctrlInitInProgress.value) {
|
while (m.ctrlInitInProgress.value) {
|
||||||
|
|||||||
+1
-1
@@ -50,7 +50,7 @@ fun SetupDatabasePassphrase(m: ChatModel) {
|
|||||||
confirmNewKey,
|
confirmNewKey,
|
||||||
progressIndicator,
|
progressIndicator,
|
||||||
onConfirmEncrypt = {
|
onConfirmEncrypt = {
|
||||||
withLongRunningApi {
|
withLongRunningApi(slow = 30_000, deadlock = 60_000) {
|
||||||
if (m.chatRunning.value == true) {
|
if (m.chatRunning.value == true) {
|
||||||
// Stop chat if it's started before doing anything
|
// Stop chat if it's started before doing anything
|
||||||
stopChatAsync(m)
|
stopChatAsync(m)
|
||||||
|
|||||||
+1
-1
@@ -36,7 +36,7 @@ fun WhatsNewView(viaSettings: Boolean = false, close: () -> Unit) {
|
|||||||
Icon(
|
Icon(
|
||||||
painterResource(MR.images.ic_open_in_new), stringResource(titleId), tint = MaterialTheme.colors.primary,
|
painterResource(MR.images.ic_open_in_new), stringResource(titleId), tint = MaterialTheme.colors.primary,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.clickable { if (link.startsWith("simplex:")) uriHandler.openVerifiedSimplexUri(link) else uriHandler.openUriCatching(link) }
|
.clickable { uriHandler.openUriCatching(link) }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user