Compare commits

..

3 Commits

Author SHA1 Message Date
Avently
2c9866a107 change 2024-02-06 22:28:14 +07:00
Avently
ff85022669 desktop: change width and height to safe values for storing and restoring 2024-02-06 22:16:51 +07:00
Stanislav Dmitrenko
5da8aef794
android: ability to hide active call (#3770)
* android: ability to hide active call

* enhancements

* fixed some problems and adapted to lock screen usage

* change

* reduce diff

* dealing with disable PiP by user

* fix back action

* fix hidden information on view rotation while info collapsed

* better info showing

* status bar color and user icon

* reorder

* experiment

* icon placement

* enhancements

* back button

* invitation accepted state handling

* awesome background work

* better service interaction and UI

* disabled call overlay when call ends and ability to accept a new call from the same contact while previous call is not ended

* incomming call alert

* enhancements

* text

* text2

* top area

* faster ending call

* a lot of enhancements

* paddings

* icon position

* move icon

---------

Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com>
2024-02-05 21:44:02 +00:00
100 changed files with 2358 additions and 757 deletions

View File

@ -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

View File

@ -252,6 +252,12 @@ 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 }
@ -1243,6 +1249,7 @@ 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())
m.chatInitialized = true m.chatInitialized = true
m.currentUser = try apiGetActiveUser() m.currentUser = try apiGetActiveUser()
@ -1854,11 +1861,9 @@ 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)
} }
} }
}
} }
func updateContactsStatus(_ contactRefs: [ContactRef], status: NetworkStatus) async { func updateContactsStatus(_ contactRefs: [ContactRef], status: NetworkStatus) async {

View File

@ -42,6 +42,25 @@ struct DeveloperView: View {
} footer: { } footer: {
(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.")
} }
// Section {
// settingsRow("arrow.up.doc") {
// Toggle("Send videos and files via XFTP", isOn: $xftpSendEnabled)
// .onChange(of: xftpSendEnabled) { _ in
// do {
// try setXFTPConfig(getXFTPCfg())
// } catch {
// logger.error("setXFTPConfig: cannot set XFTP config \(responseError(error))")
// }
// }
// }
// } header: {
// Text("Experimental")
// } footer: {
// if xftpSendEnabled {
// Text("v4.6.1+ is required to receive via XFTP.")
// }
// }
} }
} }
} }

View File

@ -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 }

View File

@ -29,6 +29,11 @@
5C116CDC27AABE0400E66D01 /* ContactRequestView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C116CDB27AABE0400E66D01 /* ContactRequestView.swift */; }; 5C116CDC27AABE0400E66D01 /* ContactRequestView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C116CDB27AABE0400E66D01 /* ContactRequestView.swift */; };
5C13730B28156D2700F43030 /* ContactConnectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C13730A28156D2700F43030 /* ContactConnectionView.swift */; }; 5C13730B28156D2700F43030 /* ContactConnectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C13730A28156D2700F43030 /* ContactConnectionView.swift */; };
5C1A4C1E27A715B700EAD5AD /* ChatItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C1A4C1D27A715B700EAD5AD /* ChatItemView.swift */; }; 5C1A4C1E27A715B700EAD5AD /* ChatItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C1A4C1D27A715B700EAD5AD /* ChatItemView.swift */; };
5C29C3A52B6D09B2003DF84C /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C29C3A02B6D09B2003DF84C /* libgmpxx.a */; };
5C29C3A62B6D09B2003DF84C /* libHSsimplex-chat-5.5.2.0-B3iqnZovI7Z5cYCa3ekeAz.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C29C3A12B6D09B2003DF84C /* libHSsimplex-chat-5.5.2.0-B3iqnZovI7Z5cYCa3ekeAz.a */; };
5C29C3A72B6D09B2003DF84C /* libHSsimplex-chat-5.5.2.0-B3iqnZovI7Z5cYCa3ekeAz-ghc9.6.3.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C29C3A22B6D09B2003DF84C /* libHSsimplex-chat-5.5.2.0-B3iqnZovI7Z5cYCa3ekeAz-ghc9.6.3.a */; };
5C29C3A82B6D09B2003DF84C /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C29C3A32B6D09B2003DF84C /* libgmp.a */; };
5C29C3A92B6D09B2003DF84C /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C29C3A42B6D09B2003DF84C /* libffi.a */; };
5C2E260727A2941F00F70299 /* SimpleXAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C2E260627A2941F00F70299 /* SimpleXAPI.swift */; }; 5C2E260727A2941F00F70299 /* SimpleXAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C2E260627A2941F00F70299 /* SimpleXAPI.swift */; };
5C2E260B27A30CFA00F70299 /* ChatListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C2E260A27A30CFA00F70299 /* ChatListView.swift */; }; 5C2E260B27A30CFA00F70299 /* ChatListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C2E260A27A30CFA00F70299 /* ChatListView.swift */; };
5C2E260F27A30FDC00F70299 /* ChatView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C2E260E27A30FDC00F70299 /* ChatView.swift */; }; 5C2E260F27A30FDC00F70299 /* ChatView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C2E260E27A30FDC00F70299 /* ChatView.swift */; };
@ -90,11 +95,6 @@
5CB0BA90282713D900B3292C /* SimpleXInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CB0BA8F282713D900B3292C /* SimpleXInfo.swift */; }; 5CB0BA90282713D900B3292C /* SimpleXInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CB0BA8F282713D900B3292C /* SimpleXInfo.swift */; };
5CB0BA92282713FD00B3292C /* CreateProfile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CB0BA91282713FD00B3292C /* CreateProfile.swift */; }; 5CB0BA92282713FD00B3292C /* CreateProfile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CB0BA91282713FD00B3292C /* CreateProfile.swift */; };
5CB0BA9A2827FD8800B3292C /* HowItWorks.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CB0BA992827FD8800B3292C /* HowItWorks.swift */; }; 5CB0BA9A2827FD8800B3292C /* HowItWorks.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CB0BA992827FD8800B3292C /* HowItWorks.swift */; };
5CB1CE922B86660100963938 /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CB1CE8D2B86660100963938 /* libgmp.a */; };
5CB1CE932B86660100963938 /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CB1CE8E2B86660100963938 /* libgmpxx.a */; };
5CB1CE942B86660100963938 /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CB1CE8F2B86660100963938 /* libffi.a */; };
5CB1CE952B86660100963938 /* libHSsimplex-chat-5.5.5.0-7lQXtpK7ThcLvpQEItJUcV-ghc9.6.3.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CB1CE902B86660100963938 /* libHSsimplex-chat-5.5.5.0-7lQXtpK7ThcLvpQEItJUcV-ghc9.6.3.a */; };
5CB1CE962B86660100963938 /* libHSsimplex-chat-5.5.5.0-7lQXtpK7ThcLvpQEItJUcV.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CB1CE912B86660100963938 /* libHSsimplex-chat-5.5.5.0-7lQXtpK7ThcLvpQEItJUcV.a */; };
5CB2084F28DA4B4800D024EC /* RTCServers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CB2084E28DA4B4800D024EC /* RTCServers.swift */; }; 5CB2084F28DA4B4800D024EC /* RTCServers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CB2084E28DA4B4800D024EC /* RTCServers.swift */; };
5CB346E52868AA7F001FD2EF /* SuspendChat.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CB346E42868AA7F001FD2EF /* SuspendChat.swift */; }; 5CB346E52868AA7F001FD2EF /* SuspendChat.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CB346E42868AA7F001FD2EF /* SuspendChat.swift */; };
5CB346E72868D76D001FD2EF /* NotificationsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CB346E62868D76D001FD2EF /* NotificationsView.swift */; }; 5CB346E72868D76D001FD2EF /* NotificationsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CB346E62868D76D001FD2EF /* NotificationsView.swift */; };
@ -278,6 +278,11 @@
5C245F3C2B501E98001CC39F /* tr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = tr; path = tr.lproj/Localizable.strings; sourceTree = "<group>"; }; 5C245F3C2B501E98001CC39F /* tr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = tr; path = tr.lproj/Localizable.strings; sourceTree = "<group>"; };
5C245F3D2B501F13001CC39F /* tr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = tr; path = "tr.lproj/SimpleX--iOS--InfoPlist.strings"; sourceTree = "<group>"; }; 5C245F3D2B501F13001CC39F /* tr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = tr; path = "tr.lproj/SimpleX--iOS--InfoPlist.strings"; sourceTree = "<group>"; };
5C245F3E2B501F13001CC39F /* tr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = tr; path = tr.lproj/InfoPlist.strings; sourceTree = "<group>"; }; 5C245F3E2B501F13001CC39F /* tr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = tr; path = tr.lproj/InfoPlist.strings; sourceTree = "<group>"; };
5C29C3A02B6D09B2003DF84C /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = "<group>"; };
5C29C3A12B6D09B2003DF84C /* libHSsimplex-chat-5.5.2.0-B3iqnZovI7Z5cYCa3ekeAz.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-5.5.2.0-B3iqnZovI7Z5cYCa3ekeAz.a"; sourceTree = "<group>"; };
5C29C3A22B6D09B2003DF84C /* libHSsimplex-chat-5.5.2.0-B3iqnZovI7Z5cYCa3ekeAz-ghc9.6.3.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-5.5.2.0-B3iqnZovI7Z5cYCa3ekeAz-ghc9.6.3.a"; sourceTree = "<group>"; };
5C29C3A32B6D09B2003DF84C /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = "<group>"; };
5C29C3A42B6D09B2003DF84C /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = "<group>"; };
5C2E260627A2941F00F70299 /* SimpleXAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SimpleXAPI.swift; sourceTree = "<group>"; }; 5C2E260627A2941F00F70299 /* SimpleXAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SimpleXAPI.swift; sourceTree = "<group>"; };
5C2E260A27A30CFA00F70299 /* ChatListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatListView.swift; sourceTree = "<group>"; }; 5C2E260A27A30CFA00F70299 /* ChatListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatListView.swift; sourceTree = "<group>"; };
5C2E260E27A30FDC00F70299 /* ChatView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatView.swift; sourceTree = "<group>"; }; 5C2E260E27A30FDC00F70299 /* ChatView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatView.swift; sourceTree = "<group>"; };
@ -372,11 +377,6 @@
5CB0BA8F282713D900B3292C /* SimpleXInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SimpleXInfo.swift; sourceTree = "<group>"; }; 5CB0BA8F282713D900B3292C /* SimpleXInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SimpleXInfo.swift; sourceTree = "<group>"; };
5CB0BA91282713FD00B3292C /* CreateProfile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateProfile.swift; sourceTree = "<group>"; }; 5CB0BA91282713FD00B3292C /* CreateProfile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateProfile.swift; sourceTree = "<group>"; };
5CB0BA992827FD8800B3292C /* HowItWorks.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HowItWorks.swift; sourceTree = "<group>"; }; 5CB0BA992827FD8800B3292C /* HowItWorks.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HowItWorks.swift; sourceTree = "<group>"; };
5CB1CE8D2B86660100963938 /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = "<group>"; };
5CB1CE8E2B86660100963938 /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = "<group>"; };
5CB1CE8F2B86660100963938 /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = "<group>"; };
5CB1CE902B86660100963938 /* libHSsimplex-chat-5.5.5.0-7lQXtpK7ThcLvpQEItJUcV-ghc9.6.3.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-5.5.5.0-7lQXtpK7ThcLvpQEItJUcV-ghc9.6.3.a"; sourceTree = "<group>"; };
5CB1CE912B86660100963938 /* libHSsimplex-chat-5.5.5.0-7lQXtpK7ThcLvpQEItJUcV.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-5.5.5.0-7lQXtpK7ThcLvpQEItJUcV.a"; sourceTree = "<group>"; };
5CB2084E28DA4B4800D024EC /* RTCServers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RTCServers.swift; sourceTree = "<group>"; }; 5CB2084E28DA4B4800D024EC /* RTCServers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RTCServers.swift; sourceTree = "<group>"; };
5CB2085428DE647400D024EC /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = de; path = de.lproj/Localizable.strings; sourceTree = "<group>"; }; 5CB2085428DE647400D024EC /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = de; path = de.lproj/Localizable.strings; sourceTree = "<group>"; };
5CB346E42868AA7F001FD2EF /* SuspendChat.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SuspendChat.swift; sourceTree = "<group>"; }; 5CB346E42868AA7F001FD2EF /* SuspendChat.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SuspendChat.swift; sourceTree = "<group>"; };
@ -514,13 +514,13 @@
isa = PBXFrameworksBuildPhase; isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
5CB1CE932B86660100963938 /* libgmpxx.a in Frameworks */, 5C29C3A62B6D09B2003DF84C /* libHSsimplex-chat-5.5.2.0-B3iqnZovI7Z5cYCa3ekeAz.a in Frameworks */,
5C29C3A52B6D09B2003DF84C /* libgmpxx.a in Frameworks */,
5CE2BA93284534B000EC33A6 /* libiconv.tbd in Frameworks */, 5CE2BA93284534B000EC33A6 /* libiconv.tbd in Frameworks */,
5CB1CE962B86660100963938 /* libHSsimplex-chat-5.5.5.0-7lQXtpK7ThcLvpQEItJUcV.a in Frameworks */,
5CB1CE922B86660100963938 /* libgmp.a in Frameworks */,
5CB1CE952B86660100963938 /* libHSsimplex-chat-5.5.5.0-7lQXtpK7ThcLvpQEItJUcV-ghc9.6.3.a in Frameworks */,
5CB1CE942B86660100963938 /* libffi.a in Frameworks */,
5CE2BA94284534BB00EC33A6 /* libz.tbd in Frameworks */, 5CE2BA94284534BB00EC33A6 /* libz.tbd in Frameworks */,
5C29C3A92B6D09B2003DF84C /* libffi.a in Frameworks */,
5C29C3A82B6D09B2003DF84C /* libgmp.a in Frameworks */,
5C29C3A72B6D09B2003DF84C /* libHSsimplex-chat-5.5.2.0-B3iqnZovI7Z5cYCa3ekeAz-ghc9.6.3.a in Frameworks */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
}; };
@ -582,11 +582,11 @@
5C764E5C279C70B7000C6508 /* Libraries */ = { 5C764E5C279C70B7000C6508 /* Libraries */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
5CB1CE8F2B86660100963938 /* libffi.a */, 5C29C3A42B6D09B2003DF84C /* libffi.a */,
5CB1CE8D2B86660100963938 /* libgmp.a */, 5C29C3A32B6D09B2003DF84C /* libgmp.a */,
5CB1CE8E2B86660100963938 /* libgmpxx.a */, 5C29C3A02B6D09B2003DF84C /* libgmpxx.a */,
5CB1CE902B86660100963938 /* libHSsimplex-chat-5.5.5.0-7lQXtpK7ThcLvpQEItJUcV-ghc9.6.3.a */, 5C29C3A22B6D09B2003DF84C /* libHSsimplex-chat-5.5.2.0-B3iqnZovI7Z5cYCa3ekeAz-ghc9.6.3.a */,
5CB1CE912B86660100963938 /* libHSsimplex-chat-5.5.5.0-7lQXtpK7ThcLvpQEItJUcV.a */, 5C29C3A12B6D09B2003DF84C /* libHSsimplex-chat-5.5.2.0-B3iqnZovI7Z5cYCa3ekeAz.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 = 200; CURRENT_PROJECT_VERSION = 196;
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.5; MARKETING_VERSION = 5.5.2;
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 = 200; CURRENT_PROJECT_VERSION = 196;
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.5; MARKETING_VERSION = 5.5.2;
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 = 200; CURRENT_PROJECT_VERSION = 196;
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.5; MARKETING_VERSION = 5.5.2;
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 = 200; CURRENT_PROJECT_VERSION = 196;
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.5; MARKETING_VERSION = 5.5.2;
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 = 200; CURRENT_PROJECT_VERSION = 196;
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.5; MARKETING_VERSION = 5.5.2;
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 = 200; CURRENT_PROJECT_VERSION = 196;
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.5; MARKETING_VERSION = 5.5.2;
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;

View File

@ -31,6 +31,7 @@ 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 apiExportArchive(config: ArchiveConfig) case apiExportArchive(config: ArchiveConfig)
case apiImportArchive(config: ArchiveConfig) case apiImportArchive(config: ArchiveConfig)
@ -161,6 +162,11 @@ 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 .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))"
@ -305,6 +311,7 @@ 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 .apiExportArchive: return "apiExportArchive" case .apiExportArchive: return "apiExportArchive"
case .apiImportArchive: return "apiImportArchive" case .apiImportArchive: return "apiImportArchive"
@ -998,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?

View File

@ -265,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

View File

@ -103,11 +103,14 @@
</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="singleTask"/> android:launchMode="singleInstance"
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"
@ -133,6 +136,18 @@
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"

View File

@ -0,0 +1,176 @@
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)
.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))
}
}
}

View File

@ -1,14 +1,15 @@
package chat.simplex.app package chat.simplex.app
import android.app.Application import android.app.*
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.app.UiModeManager import android.content.Intent
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.*
@ -18,6 +19,7 @@ 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
@ -71,7 +73,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
@ -184,6 +186,10 @@ 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)
@ -254,6 +260,28 @@ 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()

View File

@ -34,12 +34,13 @@ import kotlin.system.exitProcess
class SimplexService: Service() { class SimplexService: Service() {
private var wakeLock: PowerManager.WakeLock? = null private var wakeLock: PowerManager.WakeLock? = null
private var isStartingService = false private var isCheckingNewMessages = 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")
@ -71,6 +72,7 @@ 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) {
@ -89,6 +91,7 @@ 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)
@ -101,9 +104,9 @@ class SimplexService: Service() {
private fun startService() { private fun startService() {
Log.d(TAG, "SimplexService startService") Log.d(TAG, "SimplexService startService")
if (wakeLock != null || isStartingService) return if (wakeLock != null || isCheckingNewMessages) return
val self = this val self = this
isStartingService = true isCheckingNewMessages = true
withLongRunningApi { withLongRunningApi {
val chatController = ChatController val chatController = ChatController
waitDbMigrationEnds(chatController) waitDbMigrationEnds(chatController)
@ -123,7 +126,7 @@ class SimplexService: Service() {
} }
} }
} finally { } finally {
isStartingService = false isCheckingNewMessages = false
} }
} }
} }
@ -262,6 +265,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
var isServiceStarted = false var isServiceStarted = false
private var stopAfterStart = false private var stopAfterStart = false
@ -281,7 +285,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 { } else if (isServiceStarting) {
stopAfterStart = true stopAfterStart = true
} }
} }
@ -291,6 +295,7 @@ 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)
} }
} }

View File

@ -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.IncomingCallActivity import chat.simplex.app.views.call.CallActivity
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,6 +33,7 @@ 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"
@ -157,7 +158,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, IncomingCallActivity::class.java) val fullScreenIntent = Intent(context, CallActivity::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)

View File

@ -1,17 +1,18 @@
package chat.simplex.app.views.call package chat.simplex.app.views.call
import android.app.Activity import android.app.*
import android.app.KeyguardManager import android.content.*
import android.content.Context import android.content.res.Configuration
import android.content.Intent import android.graphics.Rect
import android.os.Build import android.os.*
import android.os.Bundle import android.util.Rational
import chat.simplex.common.platform.Log import android.view.*
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.*
@ -22,34 +23,116 @@ 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.app.model.NtfManager.OpenChatAction import chat.simplex.common.platform.*
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 IncomingCallActivity: ComponentActivity() { class CallActivity: ComponentActivity(), ServiceConnection {
var boundService: CallService? = null
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setContent { IncomingCallActivityView(ChatModel) } callActivity = WeakReference(this)
when (intent?.action) {
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() 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() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) {
@ -72,6 +155,23 @@ class IncomingCallActivity: ComponentActivity() {
} }
} }
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
} }
@ -80,38 +180,96 @@ class IncomingCallActivity: ComponentActivity() {
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 IncomingCallActivityView(m: ChatModel) { fun CallActivityView() {
val switchingCall = m.switchingCall.value val switchingCall = m.switchingCall.value
val invitation = m.activeCallInvitation.value val invitation = m.activeCallInvitation.value
val call = m.activeCall.value val call = remember { m.activeCall }.value
val showCallView = m.showCallView.value val showCallView = m.showCallView.value
val activity = LocalContext.current as Activity val activity = LocalContext.current as CallActivity
LaunchedEffect(invitation, call, switchingCall, showCallView) { LaunchedEffect(Unit) {
if (!switchingCall && invitation == null && (!showCallView || call == null)) { snapshotFlow { m.activeCallViewIsCollapsed.value }
Log.d(TAG, "IncomingCallActivityView: finishing activity") .collect { collapsed ->
activity.finish() 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 { 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( Surface(
Modifier Modifier
.fillMaxSize(), .fillMaxSize(),
color = MaterialTheme.colors.background, color = MaterialTheme.colors.background,
contentColor = LocalContentColor.current contentColor = LocalContentColor.current
) { ) {
if (showCallView) {
Box {
ActiveCallView()
if (invitation != null) IncomingCallAlertView(invitation, m)
}
} else if (invitation != null) {
IncomingCallLockScreenAlert(invitation, m) IncomingCallLockScreenAlert(invitation, m)
} }
} else {
IncomingCallAlertView(invitation, m)
}
}
}
}
LaunchedEffect(call == null) {
if (call != null) {
activity.startServiceAndBind()
}
}
LaunchedEffect(invitation, call, switchingCall, showCallView) {
if (!switchingCall && invitation == null && (!showCallView || call == null)) {
Log.d(TAG, "CallActivityView: finishing activity")
activity.finish()
} }
} }
} }
/**
* 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
@ -135,7 +293,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(OpenChatAction) .setAction(NtfManager.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)

View File

@ -4,6 +4,7 @@ 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.*
@ -25,7 +26,8 @@ val defaultLocale: Locale = Locale.getDefault()
@SuppressLint("StaticFieldLeak") @SuppressLint("StaticFieldLeak")
lateinit var androidAppContext: Context lateinit var androidAppContext: Context
lateinit var mainActivity: WeakReference<FragmentActivity> var mainActivity: WeakReference<FragmentActivity> = WeakReference(null)
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)

View File

@ -14,12 +14,8 @@ 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
for (i in 10 downTo 1) {
try {
val sendIntent: Intent = Intent().apply { val sendIntent: Intent = Intent().apply {
action = Intent.ACTION_SEND action = Intent.ACTION_SEND
putExtra(Intent.EXTRA_TEXT, text) putExtra(Intent.EXTRA_TEXT, text)
@ -29,12 +25,6 @@ actual fun ClipboardManager.shareText(text: String) {
val shareIntent = Intent.createChooser(sendIntent, null) val shareIntent = Intent.createChooser(sendIntent, null)
shareIntent.addFlags(FLAG_ACTIVITY_NEW_TASK) shareIntent.addFlags(FLAG_ACTIVITY_NEW_TASK)
androidAppContext.startActivity(shareIntent) 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))
}
}
} }
actual fun shareFile(text: String, fileSource: CryptoFile) { actual fun shareFile(text: String, fileSource: CryptoFile) {

View File

@ -114,8 +114,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
) )
} }
} }

View File

@ -28,6 +28,7 @@ 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
@ -50,20 +51,30 @@ 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 ntfModeService = remember { chatModel.controller.appPrefs.notificationsMode.get() == NotificationsMode.SERVICE } val proximityLock = remember {
LaunchedEffect(Unit) { val pm = (androidAppContext.getSystemService(Context.POWER_SERVICE) as PowerManager)
// Start service when call happening since it's not already started. if (pm.isWakeLockLevelSupported(PROXIMITY_SCREEN_OFF_WAKE_LOCK)) {
// It's needed to prevent Android from shutting down a microphone after a minute or so when screen is off pm.newWakeLock(PROXIMITY_SCREEN_OFF_WAKE_LOCK, androidAppContext.packageName + ":proximityLock")
if (!ntfModeService) platform.androidServiceStart() } else {
null
}
} }
DisposableEffect(Unit) { DisposableEffect(Unit) {
val am = androidAppContext.getSystemService(Context.AUDIO_SERVICE) as AudioManager val am = androidAppContext.getSystemService(Context.AUDIO_SERVICE) as AudioManager
@ -93,22 +104,24 @@ 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)
proximityLock?.release() if (proximityLock?.isHeld == true) {
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")
@ -156,7 +169,6 @@ actual fun ActiveCallView() {
is WCallResponse.Ended -> { is WCallResponse.Ended -> {
chatModel.activeCall.value = call.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 ->
@ -173,8 +185,9 @@ actual fun ActiveCallView() {
chatModel.callCommand.add(WCallCommand.Media(CallMediaType.Audio, enable = false)) chatModel.callCommand.add(WCallCommand.Media(CallMediaType.Audio, enable = false))
} }
} }
is WCallCommand.End -> is WCallCommand.End -> {
chatModel.showCallView.value = false withBGApi { chatModel.callManager.endCall(call) }
}
else -> {} else -> {}
} }
is WCallResponse.Error -> { is WCallResponse.Error -> {
@ -183,8 +196,16 @@ actual fun ActiveCallView() {
} }
} }
} }
val call = chatModel.activeCall.value val showOverlay = when {
if (call != null) ActiveCallOverlay(call, chatModel, audioViaBluetooth) call == null -> false
!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
@ -229,6 +250,20 @@ 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")
@ -271,34 +306,43 @@ 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(Modifier.padding(DEFAULT_PADDING)) { Column {
when (call.peerMedia ?: call.localMedia) { val media = call.peerMedia ?: call.localMedia
CloseSheetBar({ chatModel.activeCallViewIsCollapsed.value = true }, true, tintColor = Color(0xFFFFFFD8)) {
if (media == CallMediaType.Video) {
Text(call.contact.chatViewName, Modifier.fillMaxWidth().padding(end = DEFAULT_PADDING), color = Color(0xFFFFFFD8), style = MaterialTheme.typography.h2, overflow = TextOverflow.Ellipsis, maxLines = 1)
}
}
Column(Modifier.padding(horizontal = DEFAULT_PADDING)) {
when (media) {
CallMediaType.Video -> { CallMediaType.Video -> {
CallInfoView(call, alignment = Alignment.Start) VideoCallInfoView(call)
Box(Modifier.fillMaxWidth().fillMaxHeight().weight(1f), contentAlignment = Alignment.BottomCenter) { Box(Modifier.fillMaxWidth().fillMaxHeight().weight(1f), contentAlignment = Alignment.BottomCenter) {
DisabledBackgroundCallsButton() DisabledBackgroundCallsButton()
} }
Row(Modifier.fillMaxWidth().padding(horizontal = 6.dp), horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically) { Row(Modifier.fillMaxWidth().padding(horizontal = 6.dp), horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically) {
ToggleAudioButton(call, toggleAudio) ToggleAudioButton(call, enabled, toggleAudio)
Spacer(Modifier.size(40.dp)) Spacer(Modifier.size(40.dp))
IconButton(onClick = dismiss) { IconButton(onClick = dismiss, enabled = enabled) {
Icon(painterResource(MR.images.ic_call_end_filled), stringResource(MR.strings.icon_descr_hang_up), tint = Color.Red, modifier = Modifier.size(64.dp)) 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))
} }
if (call.videoEnabled) { if (call.videoEnabled) {
ControlButton(call, painterResource(MR.images.ic_flip_camera_android_filled), MR.strings.icon_descr_flip_camera, flipCamera) 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, toggleVideo) ControlButton(call, painterResource(MR.images.ic_videocam_filled), MR.strings.icon_descr_video_off, enabled, toggleVideo)
} else { } else {
Spacer(Modifier.size(48.dp)) Spacer(Modifier.size(48.dp))
ControlButton(call, painterResource(MR.images.ic_videocam_off), MR.strings.icon_descr_video_on, toggleVideo) 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(
@ -307,23 +351,24 @@ private fun ActiveCallOverlayLayout(
verticalArrangement = Arrangement.Center verticalArrangement = Arrangement.Center
) { ) {
ProfileImage(size = 192.dp, image = call.contact.profile.image) ProfileImage(size = 192.dp, image = call.contact.profile.image)
CallInfoView(call, alignment = Alignment.CenterHorizontally) AudioCallInfoView(call)
} }
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) { IconButton(onClick = dismiss, enabled = enabled) {
Icon(painterResource(MR.images.ic_call_end_filled), stringResource(MR.strings.icon_descr_hang_up), tint = Color.Red, modifier = Modifier.size(64.dp)) 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))
} }
} }
Box(Modifier.padding(start = 32.dp)) { Box(Modifier.padding(start = 32.dp)) {
ToggleAudioButton(call, toggleAudio) ToggleAudioButton(call, enabled, toggleAudio)
} }
Box(Modifier.fillMaxWidth(), contentAlignment = Alignment.CenterEnd) { Box(Modifier.fillMaxWidth(), contentAlignment = Alignment.CenterEnd) {
Box(Modifier.padding(end = 32.dp)) { Box(Modifier.padding(end = 32.dp)) {
ToggleSoundButton(call, speakerCanBeEnabled, toggleSound) ToggleSoundButton(call, speakerCanBeEnabled && enabled, toggleSound)
}
} }
} }
} }
@ -333,7 +378,7 @@ private fun ActiveCallOverlayLayout(
} }
@Composable @Composable
private fun ControlButton(call: Call, icon: Painter, iconText: StringResource, action: () -> Unit, enabled: Boolean = true) { private fun ControlButton(call: Call, icon: Painter, iconText: StringResource, enabled: Boolean = true, action: () -> Unit) {
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))
@ -344,28 +389,26 @@ private fun ControlButton(call: Call, icon: Painter, iconText: StringResource, a
} }
@Composable @Composable
private fun ToggleAudioButton(call: Call, toggleAudio: () -> Unit) { private fun ToggleAudioButton(call: Call, enabled: Boolean = true, toggleAudio: () -> Unit) {
if (call.audioEnabled) { if (call.audioEnabled) {
ControlButton(call, painterResource(MR.images.ic_mic), MR.strings.icon_descr_audio_off, toggleAudio) ControlButton(call, painterResource(MR.images.ic_mic), MR.strings.icon_descr_audio_off, enabled, toggleAudio)
} else { } else {
ControlButton(call, painterResource(MR.images.ic_mic_off), MR.strings.icon_descr_audio_on, toggleAudio) ControlButton(call, painterResource(MR.images.ic_mic_off), MR.strings.icon_descr_audio_on, enabled, 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, toggleSound, enabled) ControlButton(call, painterResource(MR.images.ic_volume_up), MR.strings.icon_descr_speaker_off, enabled, toggleSound)
} else { } else {
ControlButton(call, painterResource(MR.images.ic_volume_down), MR.strings.icon_descr_speaker_on, toggleSound, enabled) ControlButton(call, painterResource(MR.images.ic_volume_down), MR.strings.icon_descr_speaker_on, enabled, toggleSound)
} }
} }
@Composable @Composable
fun CallInfoView(call: Call, alignment: Alignment.Horizontal) { fun AudioCallInfoView(call: Call) {
@Composable fun InfoText(text: String, style: TextStyle = MaterialTheme.typography.body2) = Column(horizontalAlignment = Alignment.CenterHorizontally) {
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)
@ -375,6 +418,21 @@ fun CallInfoView(call: Call, alignment: Alignment.Horizontal) {
} }
} }
@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()) }
@ -452,7 +510,6 @@ 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(
@ -475,10 +532,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)
webView.value?.destroy() // val wv = webView.value
// if (wv != null) processCommand(wv, WCallCommand.End)
// webView.value?.destroy()
webView.value = null webView.value = null
} }
} }
@ -505,7 +562,7 @@ fun WebRTCView(callCommand: SnapshotStateList<WCallCommand>, onResponse: (WVAPIM
Box(Modifier.fillMaxSize()) { Box(Modifier.fillMaxSize()) {
AndroidView( AndroidView(
factory = { AndroidViewContext -> factory = { AndroidViewContext ->
WebView(AndroidViewContext).apply { (staticWebView ?: WebView(androidAppContext)).apply {
layoutParams = ViewGroup.LayoutParams( layoutParams = ViewGroup.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT,
@ -530,7 +587,11 @@ 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 */ }
@ -566,6 +627,7 @@ 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)
@ -579,6 +641,7 @@ 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,
@ -605,6 +668,7 @@ 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,

View File

@ -1,8 +1,112 @@
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 DesktopActiveCallOverlayLayout(newChatSheetState: MutableStateFlow<AnimatedViewState>) {} actual fun ActiveCallInteractiveArea(call: Call, 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)
}
}

View File

@ -1,16 +1,19 @@
package chat.simplex.common package chat.simplex.common
import androidx.compose.animation.core.Animatable import androidx.compose.animation.core.Animatable
import androidx.compose.foundation.background import androidx.compose.foundation.*
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
@ -20,8 +23,7 @@ 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.ActiveCallView import chat.simplex.common.views.call.*
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
@ -169,7 +171,17 @@ fun MainScreen() {
} }
} else { } else {
if (chatModel.showCallView.value) { if (chatModel.showCallView.value) {
if (appPlatform.isAndroid) {
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() 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()
@ -206,9 +218,13 @@ 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(
@ -216,6 +232,7 @@ 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)
} }
@ -242,11 +259,17 @@ fun AndroidScreen(settingsState: SettingsViewState) {
} }
} }
} }
Box(Modifier.graphicsLayer { translationX = maxWidth.toPx() - offset.value.dp.toPx() }) Box2@{ Box(Modifier
.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) })
}
} }
} }

View File

@ -96,6 +96,7 @@ 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)

View File

@ -451,21 +451,7 @@ object ChatController {
} }
try { try {
val msg = recvMsg(ctrl) val msg = recvMsg(ctrl)
if (msg != null) { if (msg != null) 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,10 +1900,8 @@ object ChatController {
if (invitation != null) { if (invitation != null) {
chatModel.callManager.reportCallRemoteEnded(invitation = invitation) chatModel.callManager.reportCallRemoteEnded(invitation = invitation)
} }
withCall(r, r.contact) { _ -> withCall(r, r.contact) { call ->
chatModel.callCommand.add(WCallCommand.End) withBGApi { chatModel.callManager.endCall(call) }
chatModel.activeCall.value = null
chatModel.showCallView.value = false
} }
} }
is CR.ContactSwitch -> is CR.ContactSwitch ->
@ -2167,6 +2157,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()
@ -2275,6 +2269,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()
@ -2404,6 +2399,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)}"
@ -2538,6 +2534,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"
@ -2703,6 +2700,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)

View File

@ -91,6 +91,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)

View File

@ -1,16 +1,21 @@
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
} }
/** /**

View File

@ -1,6 +1,6 @@
package chat.simplex.common.views.call package chat.simplex.common.views.call
import chat.simplex.common.model.ChatModel import chat.simplex.common.model.*
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,27 +23,29 @@ class CallManager(val chatModel: ChatModel) {
} }
} }
fun acceptIncomingCall(invitation: RcvCallInvitation) { fun acceptIncomingCall(invitation: RcvCallInvitation) = withBGApi {
val call = chatModel.activeCall.value val call = chatModel.activeCall.value
if (call == null) { val contactInfo = chatModel.controller.apiContactInfo(invitation.remoteHostId, invitation.contact.contactId)
justAcceptIncomingCall(invitation = invitation) val profile = contactInfo?.second ?: invitation.user.profile.toProfile()
// 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 {
withBGApi {
chatModel.switchingCall.value = true chatModel.switchingCall.value = true
try { try {
endCall(call = call) endCall(call = call)
justAcceptIncomingCall(invitation = invitation) justAcceptIncomingCall(invitation = invitation, profile)
} finally { } finally {
chatModel.switchingCall.value = false chatModel.switchingCall.value = false
} }
} }
} }
}
private fun justAcceptIncomingCall(invitation: RcvCallInvitation) { private fun justAcceptIncomingCall(invitation: RcvCallInvitation, userProfile: Profile) {
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,
@ -68,17 +70,23 @@ 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
} }
} }
} }

View File

@ -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,6 +36,9 @@ 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 {
@ -75,6 +78,7 @@ 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()
} }
@ -167,6 +171,13 @@ 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,

View File

@ -301,7 +301,9 @@ 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) {
chatModel.activeCall.value = Call(remoteHostId = chatRh, contact = cInfo.contact, callState = CallState.WaitCapabilities, localMedia = media) val contactInfo = chatModel.controller.apiContactInfo(chat.remoteHostId, cInfo.contact.contactId)
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))
} }
@ -673,7 +675,7 @@ fun ChatInfoToolbar(
} }
} }
} }
} else if (activeCall?.contact?.id == chat.id) { } else if (activeCall?.contact?.id == chat.id && appPlatform.isDesktop) {
barButtons.add { barButtons.add {
val call = remember { chatModel.activeCall }.value val call = remember { chatModel.activeCall }.value
val connectedAt = call?.connectedAt val connectedAt = call?.connectedAt

View File

@ -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)
} }
} }

View File

@ -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 = 120_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) {

View File

@ -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()
} }

View File

@ -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)

View File

@ -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)

View File

@ -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()

View File

@ -29,6 +29,7 @@ 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.*
@ -121,7 +122,12 @@ fun ChatListView(chatModel: ChatModel, settingsState: SettingsViewState, setPerf
} }
} }
if (searchText.value.text.isEmpty()) { if (searchText.value.text.isEmpty()) {
DesktopActiveCallOverlayLayout(newChatSheetState) if (appPlatform.isDesktop) {
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)
@ -314,7 +320,7 @@ private fun ToggleFilterDisabledButton() {
} }
@Composable @Composable
expect fun DesktopActiveCallOverlayLayout(newChatSheetState: MutableStateFlow<AnimatedViewState>) expect fun ActiveCallInteractiveArea(call: Call, 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")

View File

@ -85,7 +85,7 @@ private fun ShareListToolbar(chatModel: ChatModel, userPickerState: MutableState
userPickerState.value = AnimatedViewState.VISIBLE userPickerState.value = AnimatedViewState.VISIBLE
} }
} }
else -> NavigationButtonBack { chatModel.sharedContent.value = null } else -> NavigationButtonBack(onButtonClicked = { chatModel.sharedContent.value = null })
} }
} }
if (chatModel.chats.size >= 8) { if (chatModel.chats.size >= 8) {

View File

@ -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,7 +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.delay
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
@ -191,7 +189,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(
@ -205,19 +202,10 @@ class AlertManager {
delay(200) 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()

View File

@ -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, endButtons: @Composable RowScope.() -> Unit = {}) { fun CloseSheetBar(close: (() -> Unit)?, showClose: Boolean = true, tintColor: Color = if (close != null) MaterialTheme.colors.primary else MaterialTheme.colors.secondary, endButtons: @Composable RowScope.() -> Unit = {}) {
Column( Column(
Modifier Modifier
.fillMaxWidth() .fillMaxWidth()
@ -35,7 +35,7 @@ fun CloseSheetBar(close: (() -> Unit)?, showClose: Boolean = true, endButtons: @
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
if (showClose) { if (showClose) {
NavigationButtonBack(onButtonClicked = close) NavigationButtonBack(tintColor = tintColor, onButtonClicked = close)
} else { } else {
Spacer(Modifier) Spacer(Modifier)
} }

View File

@ -44,10 +44,10 @@ fun DefaultTopAppBar(
} }
@Composable @Composable
fun NavigationButtonBack(onButtonClicked: (() -> Unit)?) { fun NavigationButtonBack(onButtonClicked: (() -> Unit)?, tintColor: Color = if (onButtonClicked != null) MaterialTheme.colors.primary else MaterialTheme.colors.secondary) {
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 = if (onButtonClicked != null) MaterialTheme.colors.primary else MaterialTheme.colors.secondary painterResource(MR.images.ic_arrow_back_ios_new), stringResource(MR.strings.back), tint = tintColor
) )
} }
} }

View File

@ -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) CloseSheetBar(close, showClose, endButtons = endButtons)
Box(modifier) { content() } Box(modifier) { content() }
} }
} }

View File

@ -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)

View File

@ -37,22 +37,30 @@ 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 { 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()
job.cancel()
if (appPreferences.developerTools.get() && appPreferences.showSlowApiCalls.get()) {
val end = System.currentTimeMillis() val end = System.currentTimeMillis()
if (end - start > slow) { if (end - start > slow) {
Log.e(TAG, "Possible problem with execution of the thread, took ${(end - start) / 1000}s:\n${exception.stackTraceToString()}") Log.e(TAG, "Possible problem with execution of the thread, took ${(end - start) / 1000}s:\n${exception.stackTraceToString()}")
if (appPreferences.developerTools.get() && appPreferences.showSlowApiCalls.get()) {
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
) )
} }
} }

View File

@ -96,7 +96,7 @@ fun PrivacySettingsView(
val currentUser = chatModel.currentUser.value val currentUser = chatModel.currentUser.value
if (currentUser != null) { if (currentUser != null) {
fun setSendReceiptsContacts(enable: Boolean, clearOverrides: Boolean) { fun setSendReceiptsContacts(enable: Boolean, clearOverrides: Boolean) {
withLongRunningApi(slow = 60_000) { withLongRunningApi(slow = 30_000, deadlock = 60_000) {
val mrs = UserMsgReceiptSettings(enable, clearOverrides) val mrs = UserMsgReceiptSettings(enable, clearOverrides)
chatModel.controller.apiSetUserContactReceipts(currentUser, mrs) chatModel.controller.apiSetUserContactReceipts(currentUser, mrs)
chatModel.controller.appPrefs.privacyDeliveryReceiptsSet.set(true) chatModel.controller.appPrefs.privacyDeliveryReceiptsSet.set(true)
@ -119,7 +119,7 @@ fun PrivacySettingsView(
} }
fun setSendReceiptsGroups(enable: Boolean, clearOverrides: Boolean) { fun setSendReceiptsGroups(enable: Boolean, clearOverrides: Boolean) {
withLongRunningApi(slow = 60_000) { withLongRunningApi(slow = 30_000, deadlock = 60_000) {
val mrs = UserMsgReceiptSettings(enable, clearOverrides) val mrs = UserMsgReceiptSettings(enable, clearOverrides)
chatModel.controller.apiSetUserGroupReceipts(currentUser, mrs) chatModel.controller.apiSetUserGroupReceipts(currentUser, mrs)
chatModel.controller.appPrefs.privacyDeliveryReceiptsSet.set(true) chatModel.controller.appPrefs.privacyDeliveryReceiptsSet.set(true)

View File

@ -1588,6 +1588,8 @@
<string name="remote_ctrl_error_busy">سطح المكتب مشغول</string> <string name="remote_ctrl_error_busy">سطح المكتب مشغول</string>
<string name="remote_ctrl_error_bad_version">يحتوي سطح المكتب على إصدار غير مدعوم. يُرجى التأكد من استخدام نفس الإصدار على كلا الجهازين</string> <string name="remote_ctrl_error_bad_version">يحتوي سطح المكتب على إصدار غير مدعوم. يُرجى التأكد من استخدام نفس الإصدار على كلا الجهازين</string>
<string name="past_member_vName">العضو السابق %1$s</string> <string name="past_member_vName">العضو السابق %1$s</string>
<string name="possible_deadlock_title">مأزق</string>
<string name="possible_deadlock_desc">يستغرق تنفيذ التعليمات البرمجية وقتًا طويلاً جدًا: %1$d ثانية. من المحتمل أن التطبيق مجمّد: %2$s</string>
<string name="possible_slow_function_title">وظيفة بطيئة</string> <string name="possible_slow_function_title">وظيفة بطيئة</string>
<string name="developer_options_section">خيارات المطور</string> <string name="developer_options_section">خيارات المطور</string>
<string name="profile_update_event_member_name_changed">تغيّر العضو %1$s إلى %2$s</string> <string name="profile_update_event_member_name_changed">تغيّر العضو %1$s إلى %2$s</string>

View File

@ -147,6 +147,8 @@
<string name="smp_server_test_delete_file">Delete file</string> <string name="smp_server_test_delete_file">Delete file</string>
<string name="error_deleting_user">Error deleting user profile</string> <string name="error_deleting_user">Error deleting user profile</string>
<string name="error_updating_user_privacy">Error updating user privacy</string> <string name="error_updating_user_privacy">Error updating user privacy</string>
<string name="possible_deadlock_title">Deadlock</string>
<string name="possible_deadlock_desc">Execution of code takes too long time: %1$d seconds. Probably, the app is frozen: %2$s</string>
<string name="possible_slow_function_title">Slow function</string> <string name="possible_slow_function_title">Slow function</string>
<string name="possible_slow_function_desc">Execution of function takes too long time: %1$d seconds: %2$s</string> <string name="possible_slow_function_desc">Execution of function takes too long time: %1$d seconds: %2$s</string>
@ -177,6 +179,9 @@
<!-- SimpleX Chat foreground Service --> <!-- SimpleX Chat foreground Service -->
<string name="simplex_service_notification_title">SimpleX Chat service</string> <string name="simplex_service_notification_title">SimpleX Chat service</string>
<string name="simplex_service_notification_text">Receiving messages…</string> <string name="simplex_service_notification_text">Receiving messages…</string>
<string name="call_service_notification_audio_call">Audio call</string>
<string name="call_service_notification_video_call">Video call</string>
<string name="call_service_notification_end_call">End call</string>
<string name="hide_notification">Hide</string> <string name="hide_notification">Hide</string>
<!-- Notification channels --> <!-- Notification channels -->

View File

@ -1555,6 +1555,7 @@
<string name="chat_is_stopped_you_should_transfer_database">Чатът е спрян. Ако вече сте използвали тази база данни на друго устройство, трябва да я прехвърлите обратно, преди да стартирате чата отново.</string> <string name="chat_is_stopped_you_should_transfer_database">Чатът е спрян. Ако вече сте използвали тази база данни на друго устройство, трябва да я прехвърлите обратно, преди да стартирате чата отново.</string>
<string name="remote_ctrl_error_bad_invitation">Настолното устройство има грешен код за връзка</string> <string name="remote_ctrl_error_bad_invitation">Настолното устройство има грешен код за връзка</string>
<string name="remote_ctrl_error_bad_version">Настолното устройство е с неподдържана версия. Моля, уверете се, че използвате една и съща версия и на двете устройства</string> <string name="remote_ctrl_error_bad_version">Настолното устройство е с неподдържана версия. Моля, уверете се, че използвате една и съща версия и на двете устройства</string>
<string name="possible_deadlock_desc">Изпълнението на кода отнема твърде много време: %1$d секунди. Вероятно приложението е замразено: %2$s</string>
<string name="possible_slow_function_title">Бавна функция</string> <string name="possible_slow_function_title">Бавна функция</string>
<string name="possible_slow_function_desc">Изпълнението на функцията отнема твърде много време: %1$d секунди: %2$s</string> <string name="possible_slow_function_desc">Изпълнението на функцията отнема твърде много време: %1$d секунди: %2$s</string>
<string name="show_internal_errors">Покажи вътрешните грешки</string> <string name="show_internal_errors">Покажи вътрешните грешки</string>
@ -1590,4 +1591,5 @@
\nПрепоръчително е да рестартирате приложението.</string> \nПрепоръчително е да рестартирате приложението.</string>
<string name="developer_options_section">Опции за разработчици</string> <string name="developer_options_section">Опции за разработчици</string>
<string name="show_slow_api_calls">Показване на бавни API заявки</string> <string name="show_slow_api_calls">Показване на бавни API заявки</string>
<string name="possible_deadlock_title">Грешка в заключено положение</string>
</resources> </resources>

View File

@ -1672,7 +1672,9 @@
<string name="possible_slow_function_title">Langsame Funktion</string> <string name="possible_slow_function_title">Langsame Funktion</string>
<string name="show_slow_api_calls">Zeige langsame API-Aufrufe an</string> <string name="show_slow_api_calls">Zeige langsame API-Aufrufe an</string>
<string name="group_member_status_unknown_short">unbekannt</string> <string name="group_member_status_unknown_short">unbekannt</string>
<string name="possible_deadlock_title">Blockade</string>
<string name="developer_options_section">Optionen für Entwickler</string> <string name="developer_options_section">Optionen für Entwickler</string>
<string name="possible_deadlock_desc">Die Code-Ausführung dauert zu lange: %1$d Sekunden. Wahrscheinlich ist die App eingefroren: %2$s</string>
<string name="group_member_status_unknown">unbekannter Gruppenmitglieds-Status</string> <string name="group_member_status_unknown">unbekannter Gruppenmitglieds-Status</string>
<string name="v5_5_private_notes_descr">Mit verschlüsselten Dateien und Medien.</string> <string name="v5_5_private_notes_descr">Mit verschlüsselten Dateien und Medien.</string>
<string name="v5_5_private_notes">Private Notizen</string> <string name="v5_5_private_notes">Private Notizen</string>

View File

@ -1559,9 +1559,11 @@
<string name="remote_host_error_bad_state"><![CDATA[État médiocre de la connexion au mobile <b>%s</b>.]]></string> <string name="remote_host_error_bad_state"><![CDATA[État médiocre de la connexion au mobile <b>%s</b>.]]></string>
<string name="remote_ctrl_was_disconnected_title">Connexion interrompue</string> <string name="remote_ctrl_was_disconnected_title">Connexion interrompue</string>
<string name="remote_ctrl_error_bad_state">État médiocre de la connexion avec le bureau</string> <string name="remote_ctrl_error_bad_state">État médiocre de la connexion avec le bureau</string>
<string name="possible_deadlock_title">Impasse</string>
<string name="remote_ctrl_error_bad_version">La version de l\'ordinateur de bureau n\'est pas prise en charge. Veillez à utiliser la même version sur les deux appareils.</string> <string name="remote_ctrl_error_bad_version">La version de l\'ordinateur de bureau n\'est pas prise en charge. Veillez à utiliser la même version sur les deux appareils.</string>
<string name="remote_ctrl_error_disconnected">Le bureau a été déconnecté</string> <string name="remote_ctrl_error_disconnected">Le bureau a été déconnecté</string>
<string name="developer_options_section">Options pour les développeurs</string> <string name="developer_options_section">Options pour les développeurs</string>
<string name="possible_deadlock_desc">Le code prend trop de temps à s\'exécuter: %1$d secondes. Il est probable que l\'application soit figée: %2$s</string>
<string name="agent_internal_error_title">Erreur interne</string> <string name="agent_internal_error_title">Erreur interne</string>
<string name="remote_host_error_bad_version"><![CDATA[La version du mobile <b>%s</b> n\'est pas prise en charge. Veillez à utiliser la même version sur les deux appareils.]]></string> <string name="remote_host_error_bad_version"><![CDATA[La version du mobile <b>%s</b> n\'est pas prise en charge. Veillez à utiliser la même version sur les deux appareils.]]></string>
<string name="show_internal_errors">Afficher les erreurs internes</string> <string name="show_internal_errors">Afficher les erreurs internes</string>

View File

@ -1583,7 +1583,9 @@
<string name="possible_slow_function_title">Lassú funkció</string> <string name="possible_slow_function_title">Lassú funkció</string>
<string name="show_slow_api_calls">Lassú API-hívások megjelenítése</string> <string name="show_slow_api_calls">Lassú API-hívások megjelenítése</string>
<string name="remote_host_error_inactive"><![CDATA[A(z) <b>%s</b> mobil eszköz inaktív]]></string> <string name="remote_host_error_inactive"><![CDATA[A(z) <b>%s</b> mobil eszköz inaktív]]></string>
<string name="possible_deadlock_title">Elakadt</string>
<string name="developer_options_section">Fejlesztői beállítások</string> <string name="developer_options_section">Fejlesztői beállítások</string>
<string name="possible_deadlock_desc">A kód végrehajtása túl sokáig tart: %1$d másodperc. Valószínűleg az alkalmazás lefagyott: %2$s</string>
<string name="possible_slow_function_desc">A funkció végrehajtása túl sokáig tart: %1$d másodperc: %2$s</string> <string name="possible_slow_function_desc">A funkció végrehajtása túl sokáig tart: %1$d másodperc: %2$s</string>
<string name="remote_host_error_busy"><![CDATA[A(z) <b>%s</b> mobil eszköz elfoglalt]]></string> <string name="remote_host_error_busy"><![CDATA[A(z) <b>%s</b> mobil eszköz elfoglalt]]></string>
<string name="past_member_vName">Legutóbbi tag %1$s</string> <string name="past_member_vName">Legutóbbi tag %1$s</string>

View File

@ -1591,7 +1591,9 @@
<string name="possible_slow_function_title">Funzione lenta</string> <string name="possible_slow_function_title">Funzione lenta</string>
<string name="show_slow_api_calls">Mostra chiamate API lente</string> <string name="show_slow_api_calls">Mostra chiamate API lente</string>
<string name="group_member_status_unknown_short">sconosciuto</string> <string name="group_member_status_unknown_short">sconosciuto</string>
<string name="possible_deadlock_desc">L\'esecuzione del codice impiega troppo tempo: %1$d secondi. Probabilmente l\'app è congelata: %2$s</string>
<string name="group_member_status_unknown">stato sconosciuto</string> <string name="group_member_status_unknown">stato sconosciuto</string>
<string name="possible_deadlock_title">Stallo</string>
<string name="developer_options_section">Opzioni sviluppatore</string> <string name="developer_options_section">Opzioni sviluppatore</string>
<string name="v5_5_private_notes">Note private</string> <string name="v5_5_private_notes">Note private</string>
<string name="v5_5_new_interface_languages">Interfaccia in ungherese e turco</string> <string name="v5_5_new_interface_languages">Interfaccia in ungherese e turco</string>

View File

@ -1571,7 +1571,9 @@
<string name="remote_ctrl_error_busy">PC版が処理中</string> <string name="remote_ctrl_error_busy">PC版が処理中</string>
<string name="remote_ctrl_error_disconnected">PC版が切断されました</string> <string name="remote_ctrl_error_disconnected">PC版が切断されました</string>
<string name="remote_ctrl_error_bad_version">ご利用のPC版のバージョンがサポートされてません。両端末が同じバージョンかどうか、ご確認ください。</string> <string name="remote_ctrl_error_bad_version">ご利用のPC版のバージョンがサポートされてません。両端末が同じバージョンかどうか、ご確認ください。</string>
<string name="possible_deadlock_title">デッドロック状態</string>
<string name="developer_options_section">開発者向けの設定</string> <string name="developer_options_section">開発者向けの設定</string>
<string name="possible_deadlock_desc">処理時間が異常にかかるようです: %1$d 秒。アプリが固まった恐れがあります: %2$s</string>
<string name="remote_host_error_busy"><![CDATA[携帯版 <b>%s</b> がただいま処理中]]></string> <string name="remote_host_error_busy"><![CDATA[携帯版 <b>%s</b> がただいま処理中]]></string>
<string name="possible_slow_function_desc">機能の処理時間が以上にかかってます: %1$d 秒: %2$s</string> <string name="possible_slow_function_desc">機能の処理時間が以上にかかってます: %1$d 秒: %2$s</string>
<string name="show_internal_errors">内部エラーを表示</string> <string name="show_internal_errors">内部エラーを表示</string>

View File

@ -1574,6 +1574,7 @@
<string name="remote_host_error_missing"><![CDATA[Mobiel <b>%s</b> ontbreekt]]></string> <string name="remote_host_error_missing"><![CDATA[Mobiel <b>%s</b> ontbreekt]]></string>
<string name="remote_host_error_bad_state"><![CDATA[De verbinding met de mobiel <b>%s</b> is in slechte staat]]></string> <string name="remote_host_error_bad_state"><![CDATA[De verbinding met de mobiel <b>%s</b> is in slechte staat]]></string>
<string name="remote_ctrl_error_disconnected">De verbinding met desktop is verbroken</string> <string name="remote_ctrl_error_disconnected">De verbinding met desktop is verbroken</string>
<string name="possible_deadlock_title">Impasse</string>
<string name="possible_slow_function_desc">Uitvoering van functie duurt te lang: %1$d seconden: %2$s</string> <string name="possible_slow_function_desc">Uitvoering van functie duurt te lang: %1$d seconden: %2$s</string>
<string name="possible_slow_function_title">Langzame functie</string> <string name="possible_slow_function_title">Langzame functie</string>
<string name="developer_options_section">Ontwikkelaars opties</string> <string name="developer_options_section">Ontwikkelaars opties</string>
@ -1587,6 +1588,7 @@
<string name="restart_chat_button">Chat opnieuw starten</string> <string name="restart_chat_button">Chat opnieuw starten</string>
<string name="remote_host_error_timeout"><![CDATA[Time-out bereikt tijdens het verbinden met de mobiel <b>%s</b>]]></string> <string name="remote_host_error_timeout"><![CDATA[Time-out bereikt tijdens het verbinden met de mobiel <b>%s</b>]]></string>
<string name="remote_ctrl_error_bad_state">De verbinding met de desktop is in slechte staat</string> <string name="remote_ctrl_error_bad_state">De verbinding met de desktop is in slechte staat</string>
<string name="possible_deadlock_desc">Het uitvoeren van de code duurt te lang: %1$d seconden. Waarschijnlijk is de app vastgelopen: %2$s</string>
<string name="remote_ctrl_error_bad_invitation">Desktop heeft verkeerde uitnodigingscode</string> <string name="remote_ctrl_error_bad_invitation">Desktop heeft verkeerde uitnodigingscode</string>
<string name="remote_host_error_bad_version"><![CDATA[Mobiel <b>%s</b> heeft een niet-ondersteunde versie. Zorg ervoor dat u op beide apparaten dezelfde versie gebruikt]]></string> <string name="remote_host_error_bad_version"><![CDATA[Mobiel <b>%s</b> heeft een niet-ondersteunde versie. Zorg ervoor dat u op beide apparaten dezelfde versie gebruikt]]></string>
<string name="remote_ctrl_error_timeout">Time-out bereikt tijdens het verbinden met de desktop</string> <string name="remote_ctrl_error_timeout">Time-out bereikt tijdens het verbinden met de desktop</string>

View File

@ -1606,6 +1606,7 @@
<string name="remote_ctrl_error_bad_version">Komputer ma niewspieraną wersję. Proszę upewnić się, że używasz tych samych wersji na obu urządzeniach</string> <string name="remote_ctrl_error_bad_version">Komputer ma niewspieraną wersję. Proszę upewnić się, że używasz tych samych wersji na obu urządzeniach</string>
<string name="blocked_by_admin_items_description">%d wiadomości zablokowanych przez admina</string> <string name="blocked_by_admin_items_description">%d wiadomości zablokowanych przez admina</string>
<string name="error_creating_message">Błąd tworzenia wiadomości</string> <string name="error_creating_message">Błąd tworzenia wiadomości</string>
<string name="possible_deadlock_desc">Wykonanie kodu zajmuje za dużo czasu: %1$d sekund. Prawdopodobnie aplikacja jest zamrożona: %2$s</string>
<string name="possible_slow_function_desc">Wykonanie kodu zajmuje za dużo czasu: %1$d sekund: %2$s</string> <string name="possible_slow_function_desc">Wykonanie kodu zajmuje za dużo czasu: %1$d sekund: %2$s</string>
<string name="note_folder_local_display_name">Prywatne notatki</string> <string name="note_folder_local_display_name">Prywatne notatki</string>
<string name="group_member_status_unknown">nieznany status</string> <string name="group_member_status_unknown">nieznany status</string>
@ -1620,6 +1621,7 @@
<string name="remote_host_error_inactive"><![CDATA[Telefon <b>%s</b> jest nieaktywny]]></string> <string name="remote_host_error_inactive"><![CDATA[Telefon <b>%s</b> jest nieaktywny]]></string>
<string name="remote_host_error_bad_version"><![CDATA[Telefon <b>%s</b> ma niewspieraną wersję. Proszę, upewnij się, że używasz tej samej wersji na obydwu urządzeniach]]></string> <string name="remote_host_error_bad_version"><![CDATA[Telefon <b>%s</b> ma niewspieraną wersję. Proszę, upewnij się, że używasz tej samej wersji na obydwu urządzeniach]]></string>
<string name="group_member_status_unknown_short">nieznany</string> <string name="group_member_status_unknown_short">nieznany</string>
<string name="possible_deadlock_title">Blokada</string>
<string name="profile_update_event_contact_name_changed">kontakt %1$s zmieniony na %2$s</string> <string name="profile_update_event_contact_name_changed">kontakt %1$s zmieniony na %2$s</string>
<string name="profile_update_event_removed_address">usunięto adres kontaktu</string> <string name="profile_update_event_removed_address">usunięto adres kontaktu</string>
<string name="profile_update_event_removed_picture">usunięto zdjęcie profilu</string> <string name="profile_update_event_removed_picture">usunięto zdjęcie profilu</string>

View File

@ -1680,6 +1680,8 @@
<string name="error_showing_message">ошибка отображения сообщения</string> <string name="error_showing_message">ошибка отображения сообщения</string>
<string name="error_showing_content">ошибка отображения содержания</string> <string name="error_showing_content">ошибка отображения содержания</string>
<string name="remote_ctrl_disconnected_with_reason">Отсоединён по причине: %s</string> <string name="remote_ctrl_disconnected_with_reason">Отсоединён по причине: %s</string>
<string name="possible_deadlock_title">Взаимная блокировка</string>
<string name="possible_deadlock_desc">Выполнение задачи занимает долгое время: %1$d секунд. Возможно, приложение заблокировано: %2$s</string>
<string name="possible_slow_function_desc">Выполнение задачи занимает долгое время: %1$d секунд: %2$s</string> <string name="possible_slow_function_desc">Выполнение задачи занимает долгое время: %1$d секунд: %2$s</string>
<string name="possible_slow_function_title">Медленный вызов</string> <string name="possible_slow_function_title">Медленный вызов</string>
<string name="profile_update_event_contact_name_changed">контакт %1$s изменён на %2$s</string> <string name="profile_update_event_contact_name_changed">контакт %1$s изменён на %2$s</string>

View File

@ -1586,6 +1586,8 @@
<string name="remote_host_error_bad_state"><![CDATA[到移动主机 <b>%s</b>的连接状态不佳]]></string> <string name="remote_host_error_bad_state"><![CDATA[到移动主机 <b>%s</b>的连接状态不佳]]></string>
<string name="remote_host_error_timeout"><![CDATA[连接到移动主机<b>%s</b>时超时]]></string> <string name="remote_host_error_timeout"><![CDATA[连接到移动主机<b>%s</b>时超时]]></string>
<string name="failed_to_create_user_invalid_desc">显示名无效。请另选一个名称。</string> <string name="failed_to_create_user_invalid_desc">显示名无效。请另选一个名称。</string>
<string name="possible_deadlock_title">死锁</string>
<string name="possible_deadlock_desc">代码执行花费的时间过久:%1$d秒。应用可能卡住了%2$s</string>
<string name="possible_slow_function_title">慢函数</string> <string name="possible_slow_function_title">慢函数</string>
<string name="show_slow_api_calls">显示缓慢的 API 调用</string> <string name="show_slow_api_calls">显示缓慢的 API 调用</string>
<string name="past_member_vName">过往成员 %1$s</string> <string name="past_member_vName">过往成员 %1$s</string>

View File

@ -8,6 +8,7 @@
<body> <body>
<video <video
id="remote-video-stream" id="remote-video-stream"
class="inline"
autoplay autoplay
playsinline playsinline
poster="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAQAAAAnOwc2AAAAEUlEQVR42mNk+M+AARiHsiAAcCIKAYwFoQ8AAAAASUVORK5CYII=" poster="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAQAAAAnOwc2AAAAEUlEQVR42mNk+M+AARiHsiAAcCIKAYwFoQ8AAAAASUVORK5CYII="
@ -15,6 +16,7 @@
></video> ></video>
<video <video
id="local-video-stream" id="local-video-stream"
class="inline"
muted muted
autoplay autoplay
playsinline playsinline

View File

@ -5,14 +5,14 @@ body {
background-color: black; background-color: black;
} }
#remote-video-stream { #remote-video-stream.inline {
position: absolute; position: absolute;
width: 100%; width: 100%;
height: 100%; height: 100%;
object-fit: cover; object-fit: cover;
} }
#local-video-stream { #local-video-stream.inline {
position: absolute; position: absolute;
width: 30%; width: 30%;
max-width: 30%; max-width: 30%;
@ -23,6 +23,20 @@ body {
right: 0; right: 0;
} }
#remote-video-stream.fullscreen {
position: absolute;
height: 100%;
width: 100%;
object-fit: cover;
}
#local-video-stream.fullscreen {
position: absolute;
height: 100%;
width: 100%;
object-fit: cover;
}
*::-webkit-media-controls { *::-webkit-media-controls {
display: none !important; display: none !important;
-webkit-appearance: none !important; -webkit-appearance: none !important;

View File

@ -11,6 +11,12 @@ var VideoCamera;
VideoCamera["User"] = "user"; VideoCamera["User"] = "user";
VideoCamera["Environment"] = "environment"; VideoCamera["Environment"] = "environment";
})(VideoCamera || (VideoCamera = {})); })(VideoCamera || (VideoCamera = {}));
var LayoutType;
(function (LayoutType) {
LayoutType["Default"] = "default";
LayoutType["LocalVideo"] = "localVideo";
LayoutType["RemoteVideo"] = "remoteVideo";
})(LayoutType || (LayoutType = {}));
// for debugging // for debugging
// var sendMessageToNative = ({resp}: WVApiMessage) => console.log(JSON.stringify({command: resp})) // var sendMessageToNative = ({resp}: WVApiMessage) => console.log(JSON.stringify({command: resp}))
var sendMessageToNative = (msg) => console.log(JSON.stringify(msg)); var sendMessageToNative = (msg) => console.log(JSON.stringify(msg));
@ -319,6 +325,10 @@ const processCommand = (function () {
localizedDescription = command.description; localizedDescription = command.description;
resp = { type: "ok" }; resp = { type: "ok" };
break; break;
case "layout":
changeLayout(command.layout);
resp = { type: "ok" };
break;
case "end": case "end":
endCall(); endCall();
resp = { type: "ok" }; resp = { type: "ok" };
@ -607,6 +617,28 @@ function toggleMedia(s, media) {
} }
return res; return res;
} }
function changeLayout(layout) {
const local = document.getElementById("local-video-stream");
const remote = document.getElementById("remote-video-stream");
switch (layout) {
case LayoutType.Default:
local.className = "inline";
remote.className = "inline";
local.style.visibility = "visible";
remote.style.visibility = "visible";
break;
case LayoutType.LocalVideo:
local.className = "fullscreen";
local.style.visibility = "visible";
remote.style.visibility = "hidden";
break;
case LayoutType.RemoteVideo:
remote.className = "fullscreen";
local.style.visibility = "hidden";
remote.style.visibility = "visible";
break;
}
}
// Cryptography function - it is loaded both in the main window and in worker context (if the worker is used) // Cryptography function - it is loaded both in the main window and in worker context (if the worker is used)
function callCryptoFunction() { function callCryptoFunction() {
const initialPlainTextRequired = { const initialPlainTextRequired = {

View File

@ -9,6 +9,7 @@
<body> <body>
<video <video
id="remote-video-stream" id="remote-video-stream"
class="inline"
autoplay autoplay
playsinline playsinline
poster="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAQAAAAnOwc2AAAAEUlEQVR42mNk+M+AARiHsiAAcCIKAYwFoQ8AAAAASUVORK5CYII=" poster="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAQAAAAnOwc2AAAAEUlEQVR42mNk+M+AARiHsiAAcCIKAYwFoQ8AAAAASUVORK5CYII="
@ -16,6 +17,7 @@
></video> ></video>
<video <video
id="local-video-stream" id="local-video-stream"
class="inline"
muted muted
autoplay autoplay
playsinline playsinline

View File

@ -5,14 +5,14 @@ body {
background-color: black; background-color: black;
} }
#remote-video-stream { #remote-video-stream.inline {
position: absolute; position: absolute;
width: 100%; width: 100%;
height: 100%; height: 100%;
object-fit: cover; object-fit: cover;
} }
#local-video-stream { #local-video-stream.inline {
position: absolute; position: absolute;
width: 20%; width: 20%;
max-width: 20%; max-width: 20%;
@ -23,6 +23,20 @@ body {
right: 0; right: 0;
} }
#remote-video-stream.fullscreen {
position: absolute;
height: 100%;
width: 100%;
object-fit: cover;
}
#local-video-stream.fullscreen {
position: absolute;
height: 100%;
width: 100%;
object-fit: cover;
}
*::-webkit-media-controls { *::-webkit-media-controls {
display: none !important; display: none !important;
-webkit-appearance: none !important; -webkit-appearance: none !important;

View File

@ -39,8 +39,7 @@ fun showApp() {
WindowExceptionHandler { e -> WindowExceptionHandler { e ->
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
) )
Log.e(TAG, "App crashed, thread name: " + Thread.currentThread().name + ", exception: " + e.stackTraceToString()) Log.e(TAG, "App crashed, thread name: " + Thread.currentThread().name + ", exception: " + e.stackTraceToString())
window.dispatchEvent(WindowEvent(window, WindowEvent.WINDOW_CLOSING)) window.dispatchEvent(WindowEvent(window, WindowEvent.WINDOW_CLOSING))

View File

@ -11,7 +11,15 @@ data class WindowPositionSize(
val height: Int = 768, val height: Int = 768,
val x: Int = 0, val x: Int = 0,
val y: Int = 0, val y: Int = 0,
) ) {
fun safeValues(): WindowPositionSize =
copy(
x = x.coerceIn(-500, 10000),
y = x.coerceIn(-100, 10000),
width = width.coerceIn(100, 10000),
height = height.coerceIn(100, 10000)
)
}
fun getStoredWindowState(): WindowPositionSize = fun getStoredWindowState(): WindowPositionSize =
try { try {
@ -19,7 +27,7 @@ fun getStoredWindowState(): WindowPositionSize =
var state = if (str == null) { var state = if (str == null) {
WindowPositionSize() WindowPositionSize()
} else { } else {
json.decodeFromString(str) json.decodeFromString<WindowPositionSize>(str).safeValues()
} }
// For some reason on Linux actual width will be 10.dp less after specifying it here. If we specify 1366, // For some reason on Linux actual width will be 10.dp less after specifying it here. If we specify 1366,
@ -33,4 +41,4 @@ fun getStoredWindowState(): WindowPositionSize =
} }
fun storeWindowState(state: WindowPositionSize) = fun storeWindowState(state: WindowPositionSize) =
appPreferences.desktopWindowState.set(json.encodeToString(state)) appPreferences.desktopWindowState.set(json.encodeToString(state.safeValues()))

View File

@ -42,7 +42,7 @@ actual fun SaveContentItemAction(cItem: ChatItem, saveFileLauncher: FileChooserL
} }
var fileSource = getLoadedFileSource(cItem.file) var fileSource = getLoadedFileSource(cItem.file)
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)
saveIfExists() saveIfExists()
@ -51,7 +51,7 @@ actual fun SaveContentItemAction(cItem: ChatItem, saveFileLauncher: FileChooserL
}) })
} }
actual fun copyItemToClipboard(cItem: ChatItem, clipboard: ClipboardManager) = withLongRunningApi(slow = 600_000) { actual fun copyItemToClipboard(cItem: ChatItem, clipboard: ClipboardManager) = withLongRunningApi(slow = 60_000, deadlock = 600_000) {
var fileSource = getLoadedFileSource(cItem.file) var fileSource = getLoadedFileSource(cItem.file)
if (chatModel.connectedToRemote() && fileSource == null) { if (chatModel.connectedToRemote() && fileSource == null) {
cItem.file?.loadRemoteFile(true) cItem.file?.loadRemoteFile(true)

View File

@ -3,7 +3,6 @@ package chat.simplex.common.views.chatlist
import androidx.compose.foundation.* import androidx.compose.foundation.*
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.Icon import androidx.compose.material.Icon
import androidx.compose.material.MaterialTheme import androidx.compose.material.MaterialTheme
import androidx.compose.runtime.* import androidx.compose.runtime.*
@ -13,6 +12,7 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import chat.simplex.common.platform.* import chat.simplex.common.platform.*
import chat.simplex.common.ui.theme.* import chat.simplex.common.ui.theme.*
import chat.simplex.common.views.call.Call
import chat.simplex.common.views.call.CallMediaType import chat.simplex.common.views.call.CallMediaType
import chat.simplex.common.views.chat.item.ItemAction import chat.simplex.common.views.chat.item.ItemAction
import chat.simplex.common.views.helpers.* import chat.simplex.common.views.helpers.*
@ -22,10 +22,9 @@ import dev.icerock.moko.resources.compose.stringResource
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
@Composable @Composable
actual fun DesktopActiveCallOverlayLayout(newChatSheetState: MutableStateFlow<AnimatedViewState>) { actual fun ActiveCallInteractiveArea(call: Call, newChatSheetState: MutableStateFlow<AnimatedViewState>) {
val call = remember { chatModel.activeCall}.value // if (call.callState == CallState.Connected && !newChatSheetState.collectAsState().value.isVisible()) {
// if (call?.callState == CallState.Connected && !newChatSheetState.collectAsState().value.isVisible()) { if (!newChatSheetState.collectAsState().value.isVisible()) {
if (call != null && !newChatSheetState.collectAsState().value.isVisible()) {
val showMenu = remember { mutableStateOf(false) } val showMenu = remember { mutableStateOf(false) }
val media = call.peerMedia ?: call.localMedia val media = call.peerMedia ?: call.localMedia
CompositionLocalProvider( CompositionLocalProvider(

View File

@ -25,11 +25,11 @@ android.nonTransitiveRClass=true
android.enableJetifier=true android.enableJetifier=true
kotlin.mpp.androidSourceSetLayoutVersion=2 kotlin.mpp.androidSourceSetLayoutVersion=2
android.version_name=5.5.5 android.version_name=5.5.2
android.version_code=185 android.version_code=179
desktop.version_name=5.5.5 desktop.version_name=5.5.2
desktop.version_code=31 desktop.version_code=28
kotlin.version=1.8.20 kotlin.version=1.8.20
gradle.plugin.version=7.4.2 gradle.plugin.version=7.4.2

View File

@ -10,7 +10,7 @@ permalink: "/blog/20210512-simplex-chat-terminal-ui.html"
**Published:** May 12, 2021 **Published:** May 12, 2021
For the last six months [me](https://github.com/epoberezkin) and my son Efim have been working to bring you a working prototype of SimpleX Chat. We're excited to announce SimpleX Chat terminal client is now available [here](https://github.com/simplex-chat/simplex-chat) on Linux, Windows and Mac (you can either build from source or download the binary for Linux, Windows or Mac from the latest release). For the last six months [me](https://github.com/epoberezkin) and my son [Efim](https://github.com/efim-poberezkin) have been working to bring you a working prototype of SimpleX Chat. We're excited to announce SimpleX Chat terminal client is now available [here](https://github.com/simplex-chat/simplex-chat) on Linux, Windows and Mac (you can either build from source or download the binary for Linux, Windows or Mac from the latest release).
Weve been using the terminal client between us and a few other people for a couple of months now, eating our own “dog food”, and have developed up to version 0.3.1, with most of the messaging protocol features we originally planned Weve been using the terminal client between us and a few other people for a couple of months now, eating our own “dog food”, and have developed up to version 0.3.1, with most of the messaging protocol features we originally planned

View File

@ -78,7 +78,7 @@ You can run SimpleX Chat CLI as a local WebSockets server on any port, we use 52
simplex-chat -p 5225 simplex-chat -p 5225
``` ```
Then you can create a JavaScript or TypeScript application that would connect to it and control it via a simple WebSocket API. TypeScript SDK defines all necessary types and convenience functions to use in your applications. See this [sample bot](https://github.com/simplex-chat/simplex-chat/blob/stable/packages/simplex-chat-client/typescript/examples/squaring-bot.js) and README page. Then you can create a JavaScript or TypeScript application that would connect to it and control it via a simple WebSocket API. TypeScript SDK defines all necessary types and convenience functions to use in your applications. See this [sample bot](https://github.com/simplex-chat/simplex-chat/blob/stable/packages/simplex-chat-client/typescript/examples/squaring-bot.js) and [README page](https://github.com/simplex-chat/simplex-chat/tree/ep/blog-v4/packages/simplex-chat-client/typescript).
SimpleX Chat API allows you to: SimpleX Chat API allows you to:

View File

@ -18,7 +18,7 @@ Since we published [the security assessment of SimpleX Chat](https://simplex.cha
- Privacy Guides added SimpleX Chat to [the recommended private and secure messengers](https://www.privacyguides.org/real-time-communication/#simplex-chat). - Privacy Guides added SimpleX Chat to [the recommended private and secure messengers](https://www.privacyguides.org/real-time-communication/#simplex-chat).
- Mike Kuketz a well-known security expert published [the review of SimpleX Chat](https://www.kuketz-blog.de/simplex-eindruecke-vom-messenger-ohne-identifier/) and added it to [the messenger matrix](https://www.messenger-matrix.de). - Mike Kuketz a well-known security expert published [the review of SimpleX Chat](https://www.kuketz-blog.de/simplex-eindruecke-vom-messenger-ohne-identifier/) and added it to [the messenger matrix](https://www.messenger-matrix.de).
- Supernova published [the review](https://supernovas.space/detailed_reviews.html#simplex) and increased [SimpleX Chat recommendation ratings](https://supernovas.space/messengers.html). - Supernova published [the review](https://supernova.tilde.team/detailed_reviews.html#simplex) and increased [SimpleX Chat recommendation ratings](https://supernova.tilde.team/messengers.html).
## What's new in v4.3 ## What's new in v4.3

View File

@ -146,7 +146,7 @@ November reviews:
- [Privacy Guides](https://www.privacyguides.org/real-time-communication/#simplex-chat) recommendations. - [Privacy Guides](https://www.privacyguides.org/real-time-communication/#simplex-chat) recommendations.
- [Review by Mike Kuketz](https://www.kuketz-blog.de/simplex-eindruecke-vom-messenger-ohne-identifier/). - [Review by Mike Kuketz](https://www.kuketz-blog.de/simplex-eindruecke-vom-messenger-ohne-identifier/).
- [The messenger matrix](https://www.messenger-matrix.de). - [The messenger matrix](https://www.messenger-matrix.de).
- [Supernova review](https://supernovas.space/detailed_reviews.html#simplex) and [messenger ratings](https://supernovas.space/messengers.html). - [Supernova review](https://supernova.tilde.team/detailed_reviews.html#simplex) and [messenger ratings](https://supernova.tilde.team/messengers.html).
--- ---

View File

@ -26,7 +26,7 @@ Critiques de novembre :
- Recommandations de [Privacy Guides](https://www.privacyguides.org/real-time-communication/#simplex-chat). - Recommandations de [Privacy Guides](https://www.privacyguides.org/real-time-communication/#simplex-chat).
- [Revue par Mike Kuketz](https://www.kuketz-blog.de/simplex-eindruecke-vom-messenger-ohne-identifier/). - [Revue par Mike Kuketz](https://www.kuketz-blog.de/simplex-eindruecke-vom-messenger-ohne-identifier/).
- [La matrice des messageries](https://www.messenger-matrix.de). - [La matrice des messageries](https://www.messenger-matrix.de).
- [Revue de Supernova](https://supernovas.space/detailed_reviews.html#simplex) et [évaluations des messageries](https://supernovas.space/messengers.html). - [Revue de Supernova](https://supernova.tilde.team/detailed_reviews.html#simplex) et [évaluations des messageries](https://supernova.tilde.team/messengers.html).
Sortie de la v4.3 : Sortie de la v4.3 :

View File

@ -12,7 +12,7 @@ constraints: zip +disable-bzip2 +disable-zstd
source-repository-package source-repository-package
type: git type: git
location: https://github.com/simplex-chat/simplexmq.git location: https://github.com/simplex-chat/simplexmq.git
tag: 32c94df040b7921584a4685a814818daec3bf209 tag: a516c2f72c81bb4a433c4065b1b5aa484b8292b1
source-repository-package source-repository-package
type: git type: git

View File

@ -1,13 +1,13 @@
--- ---
title: Download SimpleX apps title: Download SimpleX apps
permalink: /downloads/index.html permalink: /downloads/index.html
revision: 11.02.2024 revision: 25.11.2023
--- ---
| Updated 11.02.2024 | Languages: EN | | Updated 25.11.2023 | Languages: EN |
# Download SimpleX apps # Download SimpleX apps
The latest stable version is v5.5.3. The latest stable version is v5.5.
You can get the latest beta releases from [GitHub](https://github.com/simplex-chat/simplex-chat/releases). You can get the latest beta releases from [GitHub](https://github.com/simplex-chat/simplex-chat/releases).
@ -21,24 +21,24 @@ You can get the latest beta releases from [GitHub](https://github.com/simplex-ch
Using the same profile as on mobile device is not yet supported you need to create a separate profile to use desktop apps. Using the same profile as on mobile device is not yet supported you need to create a separate profile to use desktop apps.
**Linux**: [AppImage](https://github.com/simplex-chat/simplex-chat/releases/latest/download/simplex-desktop-x86_64.AppImage) (most Linux distros), [Ubuntu 20.04](https://github.com/simplex-chat/simplex-chat/releases/latest/download/simplex-desktop-ubuntu-20_04-x86_64.deb) (and Debian-based distros), [Ubuntu 22.04](https://github.com/simplex-chat/simplex-chat/releases/latest/download/simplex-desktop-ubuntu-22_04-x86_64.deb). **Linux**: [AppImage](https://github.com/simplex-chat/simplex-chat/releases/download/v5.5.0/simplex-desktop-x86_64.AppImage) (most Linux distros), [Ubuntu 20.04](https://github.com/simplex-chat/simplex-chat/releases/download/v5.5.0/simplex-desktop-ubuntu-20_04-x86_64.deb) (and Debian-based distros), [Ubuntu 22.04](https://github.com/simplex-chat/simplex-chat/releases/download/v5.5.0/simplex-desktop-ubuntu-22_04-x86_64.deb).
**Mac**: [aarch64](https://github.com/simplex-chat/simplex-chat/releases/latest/download/simplex-desktop-macos-aarch64.dmg) (Apple Silicon), [x86_64](https://github.com/simplex-chat/simplex-chat/releases/latest/download/simplex-desktop-macos-x86_64.dmg) (Intel). **Mac**: [x86_64](https://github.com/simplex-chat/simplex-chat/releases/download/v5.5.0/simplex-desktop-macos-x86_64.dmg) (Intel), [aarch64](https://github.com/simplex-chat/simplex-chat/releases/download/v5.5.0/simplex-desktop-macos-aarch64.dmg) (Apple Silicon).
**Windows**: [x86_64](https://github.com/simplex-chat/simplex-chat/releases/latest/download/simplex-desktop-windows-x86_64.msi). **Windows**: [x86_64](https://github.com/simplex-chat/simplex-chat/releases/download/v5.5.0/simplex-desktop-windows-x86_64.msi).
## Mobile apps ## Mobile apps
**iOS**: [App store](https://apps.apple.com/us/app/simplex-chat/id1605771084), [TestFlight](https://testflight.apple.com/join/DWuT2LQu). **iOS**: [App store](https://apps.apple.com/us/app/simplex-chat/id1605771084), [TestFlight](https://testflight.apple.com/join/DWuT2LQu).
**Android**: [Play store](https://play.google.com/store/apps/details?id=chat.simplex.app), [F-Droid](https://simplex.chat/fdroid/), [APK aarch64](https://github.com/simplex-chat/simplex-chat/releases/latest/download/simplex.apk), [APK armv7](https://github.com/simplex-chat/simplex-chat/releases/latest/download/simplex-armv7a.apk). **Android**: [Play store](https://play.google.com/store/apps/details?id=chat.simplex.app), [F-Droid](https://simplex.chat/fdroid/), [APK aarch64](https://github.com/simplex-chat/simplex-chat/releases/download/v5.5.0/simplex.apk), [APK armv7](https://github.com/simplex-chat/simplex-chat/releases/download/v5.5.0/simplex-armv7a.apk).
## Terminal (console) app ## Terminal (console) app
See [Using terminal app](/docs/CLI.md). See [Using terminal app](/docs/CLI.md).
**Linux**: [Ubuntu 20.04](https://github.com/simplex-chat/simplex-chat/releases/latest/download/simplex-chat-ubuntu-20_04-x86-64), [Ubuntu 22.04](https://github.com/simplex-chat/simplex-chat/releases/latest/download/simplex-chat-ubuntu-22_04-x86-64). **Linux**: [Ubuntu 20.04](https://github.com/simplex-chat/simplex-chat/releases/download/v5.5.0/simplex-chat-ubuntu-20_04-x86-64), [Ubuntu 22.04](https://github.com/simplex-chat/simplex-chat/releases/download/v5.5.0/simplex-chat-ubuntu-22_04-x86-64).
**Mac** [x86_64](https://github.com/simplex-chat/simplex-chat/releases/latest/download/simplex-chat-macos-x86-64), aarch64 - [compile from source](/docs/CLI.md#). **Mac** [x86_64](https://github.com/simplex-chat/simplex-chat/releases/download/v5.5.0/simplex-chat-macos-x86-64), aarch64 - [compile from source](/docs/CLI.md#).
**Windows**: [x86_64](https://github.com/simplex-chat/simplex-chat/releases/latest/download/simplex-chat-windows-x86-64). **Windows**: [x86_64](https://github.com/simplex-chat/simplex-chat/releases/download/v5.5.0/simplex-chat-windows-x86-64).

View File

@ -143,12 +143,6 @@ SimpleX Clients also form a network using SMP relays and IP or some other overla
[Wikipedia](https://en.wikipedia.org/wiki/Overlay_network) [Wikipedia](https://en.wikipedia.org/wiki/Overlay_network)
# Non-repudiation
The property of the cryptographic or communication system that allows the recipient of the message to prove to any third party that the sender identified by some cryptographic key sent the message. It is the opposite to [repudiation](#repudiation). While in some context non-repudiation may be desirable (e.g., for contractually binding messages), in the context of private communications it may be undesirable.
[Wikipedia](https://en.wikipedia.org/wiki/Non-repudiation)
## Pairwise pseudonymous identifier ## Pairwise pseudonymous identifier
Generalizing [the definition](https://csrc.nist.gov/glossary/term/pairwise_pseudonymous_identifier) from NIST Digital Identity Guidelines, it is an opaque unguessable identifier generated by a service used to access a resource by only one party. Generalizing [the definition](https://csrc.nist.gov/glossary/term/pairwise_pseudonymous_identifier) from NIST Digital Identity Guidelines, it is an opaque unguessable identifier generated by a service used to access a resource by only one party.
@ -191,12 +185,6 @@ Network topology of the communication system when peers communicate via proxies
[Post-compromise security](#post-compromise-security). [Post-compromise security](#post-compromise-security).
## Repudiation
The property of the cryptographic or communication system that allows the sender of the message to plausibly deny having sent the message, because while the recipient can verify that the message was sent by the sender, they cannot prove it to any third party - the recipient has a technical ability to forge the same encrypted message. This is an important quality of private communications, as it allows to have the conversation that can later be denied, similarly to having a private face-to-face conversation.
See also [non-repudiation](#non-repudiation).
## User identity ## User identity
In a communication system it refers to anything that uniquely identifies the users to the network. Depending on the communication network, it can be a phone number, email address, username, public key or a random opaque identifier. Most messaging networks rely on some form of user identity. SimpleX appears to be the only messaging network that does not rely on any kind of user identity - see [this comparison](https://en.wikipedia.org/wiki/Comparison_of_instant_messaging_protocols). In a communication system it refers to anything that uniquely identifies the users to the network. Depending on the communication network, it can be a phone number, email address, username, public key or a random opaque identifier. Most messaging networks rely on some form of user identity. SimpleX appears to be the only messaging network that does not rely on any kind of user identity - see [this comparison](https://en.wikipedia.org/wiki/Comparison_of_instant_messaging_protocols).

View File

@ -70,7 +70,7 @@ Knowledge of Android and Kotlin Multiplatform would be a bonus - we use Kotlin J
## How to join the team ## How to join the team
1. [Install the app](https://github.com/simplex-chat/simplex-chat#install-the-app), try using it with the friends and [join some user groups](https://github.com/simplex-chat/simplex-chat#join-user-groups) you will discover a lot of things that need improvements. 1. [Install the app](../README.md#install-the-app), try using it with the friends and [join some user groups](https://github.com/simplex-chat/simplex-chat#join-user-groups) you will discover a lot of things that need improvements.
2. Also look through [GitHub issues](https://github.com/simplex-chat/simplex-chat/issues) submitted by the users to see what would you want to contribute as a test. 2. Also look through [GitHub issues](https://github.com/simplex-chat/simplex-chat/issues) submitted by the users to see what would you want to contribute as a test.

View File

@ -1,5 +1,5 @@
name: simplex-chat name: simplex-chat
version: 5.5.5.0 version: 5.5.2.0
#synopsis: #synopsis:
#description: #description:
homepage: https://github.com/simplex-chat/simplex-chat#readme homepage: https://github.com/simplex-chat/simplex-chat#readme

View File

@ -12,6 +12,7 @@ export type ChatCommand =
| APIStopChat | APIStopChat
| SetTempFolder | SetTempFolder
| SetFilesFolder | SetFilesFolder
| APISetXFTPConfig
| SetIncognito | SetIncognito
| APIExportArchive | APIExportArchive
| APIImportArchive | APIImportArchive
@ -111,6 +112,7 @@ type ChatCommandTag =
| "apiStopChat" | "apiStopChat"
| "setTempFolder" | "setTempFolder"
| "setFilesFolder" | "setFilesFolder"
| "apiSetXFTPConfig"
| "setIncognito" | "setIncognito"
| "apiExportArchive" | "apiExportArchive"
| "apiImportArchive" | "apiImportArchive"
@ -240,6 +242,15 @@ export interface SetFilesFolder extends IChatCommand {
filePath: string filePath: string
} }
export interface APISetXFTPConfig extends IChatCommand {
type: "apiSetXFTPConfig"
config?: XFTPFileConfig
}
export interface XFTPFileConfig {
minFileSize: number
}
export interface SetIncognito extends IChatCommand { export interface SetIncognito extends IChatCommand {
type: "setIncognito" type: "setIncognito"
incognito: boolean incognito: boolean
@ -696,6 +707,8 @@ export function cmdString(cmd: ChatCommand): string {
return `/_temp_folder ${cmd.tempFolder}` return `/_temp_folder ${cmd.tempFolder}`
case "setFilesFolder": case "setFilesFolder":
return `/_files_folder ${cmd.filePath}` return `/_files_folder ${cmd.filePath}`
case "apiSetXFTPConfig":
return `/_xftp ${onOff(cmd.config)}${maybeJSON(cmd.config)}`
case "setIncognito": case "setIncognito":
return `/incognito ${onOff(cmd.incognito)}` return `/incognito ${onOff(cmd.incognito)}`
case "apiExportArchive": case "apiExportArchive":

View File

@ -8,6 +8,7 @@
<body> <body>
<video <video
id="remote-video-stream" id="remote-video-stream"
class="inline"
autoplay autoplay
playsinline playsinline
poster="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAQAAAAnOwc2AAAAEUlEQVR42mNk+M+AARiHsiAAcCIKAYwFoQ8AAAAASUVORK5CYII=" poster="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAQAAAAnOwc2AAAAEUlEQVR42mNk+M+AARiHsiAAcCIKAYwFoQ8AAAAASUVORK5CYII="
@ -15,6 +16,7 @@
></video> ></video>
<video <video
id="local-video-stream" id="local-video-stream"
class="inline"
muted muted
autoplay autoplay
playsinline playsinline

View File

@ -5,14 +5,14 @@ body {
background-color: black; background-color: black;
} }
#remote-video-stream { #remote-video-stream.inline {
position: absolute; position: absolute;
width: 100%; width: 100%;
height: 100%; height: 100%;
object-fit: cover; object-fit: cover;
} }
#local-video-stream { #local-video-stream.inline {
position: absolute; position: absolute;
width: 30%; width: 30%;
max-width: 30%; max-width: 30%;
@ -23,6 +23,20 @@ body {
right: 0; right: 0;
} }
#remote-video-stream.fullscreen {
position: absolute;
height: 100%;
width: 100%;
object-fit: cover;
}
#local-video-stream.fullscreen {
position: absolute;
height: 100%;
width: 100%;
object-fit: cover;
}
*::-webkit-media-controls { *::-webkit-media-controls {
display: none !important; display: none !important;
-webkit-appearance: none !important; -webkit-appearance: none !important;

View File

@ -16,6 +16,7 @@ type WCallCommand =
| WCEnableMedia | WCEnableMedia
| WCToggleCamera | WCToggleCamera
| WCDescription | WCDescription
| WCLayout
| WCEndCall | WCEndCall
type WCallResponse = type WCallResponse =
@ -31,7 +32,7 @@ type WCallResponse =
| WRError | WRError
| WCAcceptOffer | WCAcceptOffer
type WCallCommandTag = "capabilities" | "start" | "offer" | "answer" | "ice" | "media" | "camera" | "description" | "end" type WCallCommandTag = "capabilities" | "start" | "offer" | "answer" | "ice" | "media" | "camera" | "description" | "layout" | "end"
type WCallResponseTag = "capabilities" | "offer" | "answer" | "ice" | "connection" | "connected" | "end" | "ended" | "ok" | "error" type WCallResponseTag = "capabilities" | "offer" | "answer" | "ice" | "connection" | "connected" | "end" | "ended" | "ok" | "error"
@ -45,6 +46,12 @@ enum VideoCamera {
Environment = "environment", Environment = "environment",
} }
enum LayoutType {
Default = "default",
LocalVideo = "localVideo",
RemoteVideo = "remoteVideo",
}
interface IWCallCommand { interface IWCallCommand {
type: WCallCommandTag type: WCallCommandTag
} }
@ -115,6 +122,11 @@ interface WCDescription extends IWCallCommand {
description: string description: string
} }
interface WCLayout extends IWCallCommand {
type: "layout"
layout: LayoutType
}
interface WRCapabilities extends IWCallResponse { interface WRCapabilities extends IWCallResponse {
type: "capabilities" type: "capabilities"
capabilities: CallCapabilities capabilities: CallCapabilities
@ -515,6 +527,10 @@ const processCommand = (function () {
localizedDescription = command.description localizedDescription = command.description
resp = {type: "ok"} resp = {type: "ok"}
break break
case "layout":
changeLayout(command.layout)
resp = {type: "ok"}
break
case "end": case "end":
endCall() endCall()
resp = {type: "ok"} resp = {type: "ok"}
@ -824,6 +840,29 @@ function toggleMedia(s: MediaStream, media: CallMediaType): boolean {
return res return res
} }
function changeLayout(layout: LayoutType) {
const local = document.getElementById("local-video-stream")!
const remote = document.getElementById("remote-video-stream")!
switch (layout) {
case LayoutType.Default:
local.className = "inline"
remote.className = "inline"
local.style.visibility = "visible"
remote.style.visibility = "visible"
break
case LayoutType.LocalVideo:
local.className = "fullscreen"
local.style.visibility = "visible"
remote.style.visibility = "hidden"
break
case LayoutType.RemoteVideo:
remote.className = "fullscreen"
local.style.visibility = "hidden"
remote.style.visibility = "visible"
break
}
}
type TransformFrameFunc = (key: CryptoKey) => (frame: RTCEncodedVideoFrame, controller: TransformStreamDefaultController) => Promise<void> type TransformFrameFunc = (key: CryptoKey) => (frame: RTCEncodedVideoFrame, controller: TransformStreamDefaultController) => Promise<void>
interface CallCrypto { interface CallCrypto {

View File

@ -9,6 +9,7 @@
<body> <body>
<video <video
id="remote-video-stream" id="remote-video-stream"
class="inline"
autoplay autoplay
playsinline playsinline
poster="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAQAAAAnOwc2AAAAEUlEQVR42mNk+M+AARiHsiAAcCIKAYwFoQ8AAAAASUVORK5CYII=" poster="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAQAAAAnOwc2AAAAEUlEQVR42mNk+M+AARiHsiAAcCIKAYwFoQ8AAAAASUVORK5CYII="
@ -16,6 +17,7 @@
></video> ></video>
<video <video
id="local-video-stream" id="local-video-stream"
class="inline"
muted muted
autoplay autoplay
playsinline playsinline

View File

@ -5,14 +5,14 @@ body {
background-color: black; background-color: black;
} }
#remote-video-stream { #remote-video-stream.inline {
position: absolute; position: absolute;
width: 100%; width: 100%;
height: 100%; height: 100%;
object-fit: cover; object-fit: cover;
} }
#local-video-stream { #local-video-stream.inline {
position: absolute; position: absolute;
width: 20%; width: 20%;
max-width: 20%; max-width: 20%;
@ -23,6 +23,20 @@ body {
right: 0; right: 0;
} }
#remote-video-stream.fullscreen {
position: absolute;
height: 100%;
width: 100%;
object-fit: cover;
}
#local-video-stream.fullscreen {
position: absolute;
height: 100%;
width: 100%;
object-fit: cover;
}
*::-webkit-media-controls { *::-webkit-media-controls {
display: none !important; display: none !important;
-webkit-appearance: none !important; -webkit-appearance: none !important;

View File

@ -1,5 +1,5 @@
{ {
"https://github.com/simplex-chat/simplexmq.git"."32c94df040b7921584a4685a814818daec3bf209" = "0bfyzra8x67zwqr7g8hkglxpy503qwn0xni0sjnbjmvh7wlh6pyz"; "https://github.com/simplex-chat/simplexmq.git"."a516c2f72c81bb4a433c4065b1b5aa484b8292b1" = "05ny2i262c236li5w040i1nd3l037cpzgbzjknlla9dd139f3al3";
"https://github.com/simplex-chat/hs-socks.git"."a30cc7a79a08d8108316094f8f2f82a0c5e1ac51" = "0yasvnr7g91k76mjkamvzab2kvlb1g5pspjyjn2fr6v83swjhj38"; "https://github.com/simplex-chat/hs-socks.git"."a30cc7a79a08d8108316094f8f2f82a0c5e1ac51" = "0yasvnr7g91k76mjkamvzab2kvlb1g5pspjyjn2fr6v83swjhj38";
"https://github.com/simplex-chat/direct-sqlcipher.git"."f814ee68b16a9447fbb467ccc8f29bdd3546bfd9" = "1ql13f4kfwkbaq7nygkxgw84213i0zm7c1a8hwvramayxl38dq5d"; "https://github.com/simplex-chat/direct-sqlcipher.git"."f814ee68b16a9447fbb467ccc8f29bdd3546bfd9" = "1ql13f4kfwkbaq7nygkxgw84213i0zm7c1a8hwvramayxl38dq5d";
"https://github.com/simplex-chat/sqlcipher-simple.git"."a46bd361a19376c5211f1058908fc0ae6bf42446" = "1z0r78d8f0812kxbgsm735qf6xx8lvaz27k1a0b4a2m0sshpd5gl"; "https://github.com/simplex-chat/sqlcipher-simple.git"."a46bd361a19376c5211f1058908fc0ae6bf42446" = "1z0r78d8f0812kxbgsm735qf6xx8lvaz27k1a0b4a2m0sshpd5gl";

View File

@ -5,7 +5,7 @@ cabal-version: 1.12
-- see: https://github.com/sol/hpack -- see: https://github.com/sol/hpack
name: simplex-chat name: simplex-chat
version: 5.5.5.0 version: 5.5.2.0
category: Web, System, Services, Cryptography category: Web, System, Services, Cryptography
homepage: https://github.com/simplex-chat/simplex-chat#readme homepage: https://github.com/simplex-chat/simplex-chat#readme
author: simplex.chat author: simplex.chat

View File

@ -81,7 +81,7 @@ import Simplex.Chat.Types.Util
import Simplex.Chat.Util (encryptFile, shuffle) import Simplex.Chat.Util (encryptFile, shuffle)
import Simplex.FileTransfer.Client.Main (maxFileSize) import Simplex.FileTransfer.Client.Main (maxFileSize)
import Simplex.FileTransfer.Client.Presets (defaultXFTPServers) import Simplex.FileTransfer.Client.Presets (defaultXFTPServers)
import Simplex.FileTransfer.Description (ValidFileDescription) import Simplex.FileTransfer.Description (ValidFileDescription, gb, kb, mb)
import Simplex.FileTransfer.Protocol (FileParty (..), FilePartyI) import Simplex.FileTransfer.Protocol (FileParty (..), FilePartyI)
import Simplex.Messaging.Agent as Agent import Simplex.Messaging.Agent as Agent
import Simplex.Messaging.Agent.Client (AgentStatsKey (..), SubInfo (..), agentClientStore, getAgentWorkersDetails, getAgentWorkersSummary, temporaryAgentError) import Simplex.Messaging.Agent.Client (AgentStatsKey (..), SubInfo (..), agentClientStore, getAgentWorkersDetails, getAgentWorkersSummary, temporaryAgentError)
@ -142,6 +142,8 @@ defaultChatConfig =
xftpDescrPartSize = 14000, xftpDescrPartSize = 14000,
inlineFiles = defaultInlineFilesConfig, inlineFiles = defaultInlineFilesConfig,
autoAcceptFileSize = 0, autoAcceptFileSize = 0,
xftpFileConfig = Just defaultXFTPFileConfig,
tempDir = Nothing,
showReactions = False, showReactions = False,
showReceipts = False, showReceipts = False,
logLevel = CLLImportant, logLevel = CLLImportant,
@ -199,7 +201,7 @@ newChatController :: ChatDatabase -> Maybe User -> ChatConfig -> ChatOpts -> Boo
newChatController newChatController
ChatDatabase {chatStore, agentStore} ChatDatabase {chatStore, agentStore}
user user
cfg@ChatConfig {agentConfig = aCfg, defaultServers, inlineFiles, deviceNameForRemote} cfg@ChatConfig {agentConfig = aCfg, defaultServers, inlineFiles, tempDir, deviceNameForRemote}
ChatOpts {coreOptions = CoreChatOpts {smpServers, xftpServers, networkConfig, logLevel, logConnections, logServerHosts, logFile, tbqSize, highlyAvailable}, deviceName, optFilesFolder, showReactions, allowInstantFiles, autoAcceptFileSize} ChatOpts {coreOptions = CoreChatOpts {smpServers, xftpServers, networkConfig, logLevel, logConnections, logServerHosts, logFile, tbqSize, highlyAvailable}, deviceName, optFilesFolder, showReactions, allowInstantFiles, autoAcceptFileSize}
backgroundMode = do backgroundMode = do
let inlineFiles' = if allowInstantFiles || autoAcceptFileSize > 0 then inlineFiles else inlineFiles {sendChunks = 0, receiveInstant = False} let inlineFiles' = if allowInstantFiles || autoAcceptFileSize > 0 then inlineFiles else inlineFiles {sendChunks = 0, receiveInstant = False}
@ -234,7 +236,8 @@ newChatController
chatActivated <- newTVarIO True chatActivated <- newTVarIO True
showLiveItems <- newTVarIO False showLiveItems <- newTVarIO False
encryptLocalFiles <- newTVarIO False encryptLocalFiles <- newTVarIO False
tempDirectory <- newTVarIO Nothing userXFTPFileConfig <- newTVarIO $ xftpFileConfig cfg
tempDirectory <- newTVarIO tempDir
contactMergeEnabled <- newTVarIO True contactMergeEnabled <- newTVarIO True
pure pure
ChatController ChatController
@ -269,6 +272,7 @@ newChatController
chatActivated, chatActivated,
showLiveItems, showLiveItems,
encryptLocalFiles, encryptLocalFiles,
userXFTPFileConfig,
tempDirectory, tempDirectory,
logFilePath = logFile, logFilePath = logFile,
contactMergeEnabled contactMergeEnabled
@ -578,6 +582,9 @@ processChatCommand' vr = \case
createDirectoryIfMissing True rf createDirectoryIfMissing True rf
chatWriteVar remoteHostsFolder $ Just rf chatWriteVar remoteHostsFolder $ Just rf
ok_ ok_
APISetXFTPConfig cfg -> do
asks userXFTPFileConfig >>= atomically . (`writeTVar` cfg)
ok_
APISetEncryptLocalFiles on -> chatWriteVar encryptLocalFiles on >> ok_ APISetEncryptLocalFiles on -> chatWriteVar encryptLocalFiles on >> ok_
SetContactMergeEnabled onOff -> do SetContactMergeEnabled onOff -> do
asks contactMergeEnabled >>= atomically . (`writeTVar` onOff) asks contactMergeEnabled >>= atomically . (`writeTVar` onOff)
@ -638,7 +645,7 @@ processChatCommand' vr = \case
memStatuses -> pure $ Just $ map (uncurry MemberDeliveryStatus) memStatuses memStatuses -> pure $ Just $ map (uncurry MemberDeliveryStatus) memStatuses
_ -> pure Nothing _ -> pure Nothing
pure $ CRChatItemInfo user aci ChatItemInfo {itemVersions, memberDeliveryStatuses} pure $ CRChatItemInfo user aci ChatItemInfo {itemVersions, memberDeliveryStatuses}
APISendMessage (ChatRef cType chatId) live itemTTL (ComposedMessage file_ quotedItemId_ mc) -> withUser $ \user -> withChatLock "sendMessage" $ case cType of APISendMessage (ChatRef cType chatId) live itemTTL (ComposedMessage file_ quotedItemId_ mc) -> withUser $ \user@User {userId} -> withChatLock "sendMessage" $ case cType of
CTDirect -> do CTDirect -> do
ct@Contact {contactId, contactUsed} <- withStore $ \db -> getContact db user chatId ct@Contact {contactId, contactUsed} <- withStore $ \db -> getContact db user chatId
assertDirectAllowed user MDSnd ct XMsgNew_ assertDirectAllowed user MDSnd ct XMsgNew_
@ -646,19 +653,45 @@ processChatCommand' vr = \case
if isVoice mc && not (featureAllowed SCFVoice forUser ct) if isVoice mc && not (featureAllowed SCFVoice forUser ct)
then pure $ chatCmdError (Just user) ("feature not allowed " <> T.unpack (chatFeatureNameText CFVoice)) then pure $ chatCmdError (Just user) ("feature not allowed " <> T.unpack (chatFeatureNameText CFVoice))
else do else do
(fInv_, ciFile_) <- L.unzip <$> setupSndFileTransfer ct (fInv_, ciFile_, ft_) <- unzipMaybe3 <$> setupSndFileTransfer ct
timed_ <- sndContactCITimed live ct itemTTL timed_ <- sndContactCITimed live ct itemTTL
(msgContainer, quotedItem_) <- prepareMsg fInv_ timed_ (msgContainer, quotedItem_) <- prepareMsg fInv_ timed_
(msg, _) <- sendDirectContactMessage ct (XMsgNew msgContainer) (msg@SndMessage {sharedMsgId}, _) <- sendDirectContactMessage ct (XMsgNew msgContainer)
ci <- saveSndChatItem' user (CDDirectSnd ct) msg (CISndMsgContent mc) ciFile_ quotedItem_ timed_ live ci <- saveSndChatItem' user (CDDirectSnd ct) msg (CISndMsgContent mc) ciFile_ quotedItem_ timed_ live
case ft_ of
Just ft@FileTransferMeta {fileInline = Just IFMSent} ->
sendDirectFileInline ct ft sharedMsgId
_ -> pure ()
forM_ (timed_ >>= timedDeleteAt') $ forM_ (timed_ >>= timedDeleteAt') $
startProximateTimedItemThread user (ChatRef CTDirect contactId, chatItemId' ci) startProximateTimedItemThread user (ChatRef CTDirect contactId, chatItemId' ci)
pure $ CRNewChatItem user (AChatItem SCTDirect SMDSnd (DirectChat ct) ci) pure $ CRNewChatItem user (AChatItem SCTDirect SMDSnd (DirectChat ct) ci)
where where
setupSndFileTransfer :: Contact -> m (Maybe (FileInvitation, CIFile 'MDSnd)) setupSndFileTransfer :: Contact -> m (Maybe (FileInvitation, CIFile 'MDSnd, FileTransferMeta))
setupSndFileTransfer ct = forM file_ $ \file -> do setupSndFileTransfer ct = forM file_ $ \file -> do
fileSize <- checkSndFile file (fileSize, fileMode) <- checkSndFile mc file 1
xftpSndFileTransfer user file fileSize 1 $ CGContact ct case fileMode of
SendFileSMP fileInline -> smpSndFileTransfer file fileSize fileInline
SendFileXFTP -> xftpSndFileTransfer user file fileSize 1 $ CGContact ct
where
smpSndFileTransfer :: CryptoFile -> Integer -> Maybe InlineFileMode -> m (FileInvitation, CIFile 'MDSnd, FileTransferMeta)
smpSndFileTransfer (CryptoFile _ (Just _)) _ _ = throwChatError $ CEFileInternal "locally encrypted files can't be sent via SMP" -- can only happen if XFTP is disabled
smpSndFileTransfer (CryptoFile file Nothing) fileSize fileInline = do
subMode <- chatReadVar subscriptionMode
(agentConnId_, fileConnReq) <-
if isJust fileInline
then pure (Nothing, Nothing)
else bimap Just Just <$> withAgent (\a -> createConnection a (aUserId user) True SCMInvitation Nothing subMode)
let fileName = takeFileName file
fileInvitation = FileInvitation {fileName, fileSize, fileDigest = Nothing, fileConnReq, fileInline, fileDescr = Nothing}
chSize <- asks $ fileChunkSize . config
withStore $ \db -> do
ft@FileTransferMeta {fileId} <- liftIO $ createSndDirectFileTransfer db userId ct file fileInvitation agentConnId_ chSize subMode
fileStatus <- case fileInline of
Just IFMSent -> createSndDirectInlineFT db ct ft $> CIFSSndTransfer 0 1
_ -> pure CIFSSndStored
let fileSource = Just $ CF.plain file
ciFile = CIFile {fileId, fileName, fileSize, fileSource, fileStatus, fileProtocol = FPSMP}
pure (fileInvitation, ciFile, ft)
prepareMsg :: Maybe FileInvitation -> Maybe CITimed -> m (MsgContainer, Maybe (CIQuote 'CTDirect)) prepareMsg :: Maybe FileInvitation -> Maybe CITimed -> m (MsgContainer, Maybe (CIQuote 'CTDirect))
prepareMsg fInv_ timed_ = case quotedItemId_ of prepareMsg fInv_ timed_ = case quotedItemId_ of
Nothing -> pure (MCSimple (ExtMsgContent mc fInv_ (ttl' <$> timed_) (justTrue live)), Nothing) Nothing -> pure (MCSimple (ExtMsgContent mc fInv_ (ttl' <$> timed_) (justTrue live)), Nothing)
@ -685,27 +718,53 @@ processChatCommand' vr = \case
| isVoice mc && not (groupFeatureAllowed SGFVoice gInfo) = notAllowedError GFVoice | isVoice mc && not (groupFeatureAllowed SGFVoice gInfo) = notAllowedError GFVoice
| not (isVoice mc) && isJust file_ && not (groupFeatureAllowed SGFFiles gInfo) = notAllowedError GFFiles | not (isVoice mc) && isJust file_ && not (groupFeatureAllowed SGFFiles gInfo) = notAllowedError GFFiles
| otherwise = do | otherwise = do
(fInv_, ciFile_) <- L.unzip <$> setupSndFileTransfer g (length $ filter memberCurrent ms) (fInv_, ciFile_, ft_) <- unzipMaybe3 <$> setupSndFileTransfer g (length $ filter memberCurrent ms)
timed_ <- sndGroupCITimed live gInfo itemTTL timed_ <- sndGroupCITimed live gInfo itemTTL
(msgContainer, quotedItem_) <- prepareGroupMsg user gInfo mc quotedItemId_ fInv_ timed_ live (msgContainer, quotedItem_) <- prepareGroupMsg user gInfo mc quotedItemId_ fInv_ timed_ live
(msg, sentToMembers) <- sendGroupMessage user gInfo ms (XMsgNew msgContainer) (msg@SndMessage {sharedMsgId}, sentToMembers) <- sendGroupMessage user gInfo ms (XMsgNew msgContainer)
ci <- saveSndChatItem' user (CDGroupSnd gInfo) msg (CISndMsgContent mc) ciFile_ quotedItem_ timed_ live ci <- saveSndChatItem' user (CDGroupSnd gInfo) msg (CISndMsgContent mc) ciFile_ quotedItem_ timed_ live
withStore' $ \db -> withStore' $ \db ->
forM_ sentToMembers $ \GroupMember {groupMemberId} -> forM_ sentToMembers $ \GroupMember {groupMemberId} ->
createGroupSndStatus db (chatItemId' ci) groupMemberId CISSndNew createGroupSndStatus db (chatItemId' ci) groupMemberId CISSndNew
mapM_ (sendGroupFileInline ms sharedMsgId) ft_
forM_ (timed_ >>= timedDeleteAt') $ forM_ (timed_ >>= timedDeleteAt') $
startProximateTimedItemThread user (ChatRef CTGroup groupId, chatItemId' ci) startProximateTimedItemThread user (ChatRef CTGroup groupId, chatItemId' ci)
pure $ CRNewChatItem user (AChatItem SCTGroup SMDSnd (GroupChat gInfo) ci) pure $ CRNewChatItem user (AChatItem SCTGroup SMDSnd (GroupChat gInfo) ci)
notAllowedError f = pure $ chatCmdError (Just user) ("feature not allowed " <> T.unpack (groupFeatureNameText f)) notAllowedError f = pure $ chatCmdError (Just user) ("feature not allowed " <> T.unpack (groupFeatureNameText f))
setupSndFileTransfer :: Group -> Int -> m (Maybe (FileInvitation, CIFile 'MDSnd)) setupSndFileTransfer :: Group -> Int -> m (Maybe (FileInvitation, CIFile 'MDSnd, FileTransferMeta))
setupSndFileTransfer g n = forM file_ $ \file -> do setupSndFileTransfer g@(Group gInfo _) n = forM file_ $ \file -> do
fileSize <- checkSndFile file (fileSize, fileMode) <- checkSndFile mc file $ fromIntegral n
xftpSndFileTransfer user file fileSize n $ CGGroup g case fileMode of
SendFileSMP fileInline -> smpSndFileTransfer file fileSize fileInline
SendFileXFTP -> xftpSndFileTransfer user file fileSize n $ CGGroup g
where
smpSndFileTransfer :: CryptoFile -> Integer -> Maybe InlineFileMode -> m (FileInvitation, CIFile 'MDSnd, FileTransferMeta)
smpSndFileTransfer (CryptoFile _ (Just _)) _ _ = throwChatError $ CEFileInternal "locally encrypted files can't be sent via SMP" -- can only happen if XFTP is disabled
smpSndFileTransfer (CryptoFile file Nothing) fileSize fileInline = do
let fileName = takeFileName file
fileInvitation = FileInvitation {fileName, fileSize, fileDigest = Nothing, fileConnReq = Nothing, fileInline, fileDescr = Nothing}
fileStatus = if fileInline == Just IFMSent then CIFSSndTransfer 0 1 else CIFSSndStored
chSize <- asks $ fileChunkSize . config
withStore' $ \db -> do
ft@FileTransferMeta {fileId} <- createSndGroupFileTransfer db userId gInfo file fileInvitation chSize
let fileSource = Just $ CF.plain file
ciFile = CIFile {fileId, fileName, fileSize, fileSource, fileStatus, fileProtocol = FPSMP}
pure (fileInvitation, ciFile, ft)
sendGroupFileInline :: [GroupMember] -> SharedMsgId -> FileTransferMeta -> m ()
sendGroupFileInline ms sharedMsgId ft@FileTransferMeta {fileInline} =
when (fileInline == Just IFMSent) . forM_ ms $ \m ->
processMember m `catchChatError` (toView . CRChatError (Just user))
where
processMember m@GroupMember {activeConn = Just conn@Connection {connStatus}} =
when (connStatus == ConnReady || connStatus == ConnSndReady) $ do
void . withStore' $ \db -> createSndGroupInlineFT db m conn ft
sendMemberFileInline m conn ft sharedMsgId
processMember _ = pure ()
CTLocal -> pure $ chatCmdError (Just user) "not supported" CTLocal -> pure $ chatCmdError (Just user) "not supported"
CTContactRequest -> pure $ chatCmdError (Just user) "not supported" CTContactRequest -> pure $ chatCmdError (Just user) "not supported"
CTContactConnection -> pure $ chatCmdError (Just user) "not supported" CTContactConnection -> pure $ chatCmdError (Just user) "not supported"
where where
xftpSndFileTransfer :: User -> CryptoFile -> Integer -> Int -> ContactOrGroup -> m (FileInvitation, CIFile 'MDSnd) xftpSndFileTransfer :: User -> CryptoFile -> Integer -> Int -> ContactOrGroup -> m (FileInvitation, CIFile 'MDSnd, FileTransferMeta)
xftpSndFileTransfer user file@(CryptoFile filePath cfArgs) fileSize n contactOrGroup = do xftpSndFileTransfer user file@(CryptoFile filePath cfArgs) fileSize n contactOrGroup = do
let fileName = takeFileName filePath let fileName = takeFileName filePath
fileDescr = FileDescr {fileDescrText = "", fileDescrPartNo = 0, fileDescrComplete = False} fileDescr = FileDescr {fileDescrText = "", fileDescrPartNo = 0, fileDescrComplete = False}
@ -729,7 +788,10 @@ processChatCommand' vr = \case
withStore' $ withStore' $
\db -> createSndFTDescrXFTP db user (Just m) conn ft fileDescr \db -> createSndFTDescrXFTP db user (Just m) conn ft fileDescr
saveMemberFD _ = pure () saveMemberFD _ = pure ()
pure (fInv, ciFile) pure (fInv, ciFile, ft)
unzipMaybe3 :: Maybe (a, b, c) -> (Maybe a, Maybe b, Maybe c)
unzipMaybe3 (Just (a, b, c)) = (Just a, Just b, Just c)
unzipMaybe3 _ = (Nothing, Nothing, Nothing)
APICreateChatItem folderId (ComposedMessage file_ quotedItemId_ mc) -> withUser $ \user -> do APICreateChatItem folderId (ComposedMessage file_ quotedItemId_ mc) -> withUser $ \user -> do
forM_ quotedItemId_ $ \_ -> throwError $ ChatError $ CECommandError "not supported" forM_ quotedItemId_ $ \_ -> throwError $ ChatError $ CECommandError "not supported"
nf <- withStore $ \db -> getNoteFolder db user folderId nf <- withStore $ \db -> getNoteFolder db user folderId
@ -947,7 +1009,7 @@ processChatCommand' vr = \case
-- functions below are called in separate transactions to prevent crashes on android -- functions below are called in separate transactions to prevent crashes on android
-- (possibly, race condition on integrity check?) -- (possibly, race condition on integrity check?)
withStore' $ \db -> deleteContactConnectionsAndFiles db userId ct withStore' $ \db -> deleteContactConnectionsAndFiles db userId ct
withStore $ \db -> deleteContact db user ct withStore' $ \db -> deleteContact db user ct
pure $ CRContactDeleted user ct pure $ CRContactDeleted user ct
CTContactConnection -> withChatLock "deleteChat contactConnection" . procCmd $ do CTContactConnection -> withChatLock "deleteChat contactConnection" . procCmd $ do
conn@PendingContactConnection {pccAgentConnId = AgentConnId acId} <- withStore $ \db -> getPendingContactConnection db userId chatId conn@PendingContactConnection {pccAgentConnId = AgentConnId acId} <- withStore $ \db -> getPendingContactConnection db userId chatId
@ -988,7 +1050,7 @@ processChatCommand' vr = \case
Just _ -> pure [] Just _ -> pure []
Nothing -> do Nothing -> do
conns <- withStore' $ \db -> getContactConnections db userId ct conns <- withStore' $ \db -> getContactConnections db userId ct
withStore (\db -> setContactDeleted db user ct) withStore' (\db -> setContactDeleted db user ct)
`catchChatError` (toView . CRChatError (Just user)) `catchChatError` (toView . CRChatError (Just user))
pure $ map aConnId conns pure $ map aConnId conns
CTLocal -> pure $ chatCmdError (Just user) "not supported" CTLocal -> pure $ chatCmdError (Just user) "not supported"
@ -2140,13 +2202,27 @@ processChatCommand' vr = \case
contactMember Contact {contactId} = contactMember Contact {contactId} =
find $ \GroupMember {memberContactId = cId, memberStatus = s} -> find $ \GroupMember {memberContactId = cId, memberStatus = s} ->
cId == Just contactId && s /= GSMemRemoved && s /= GSMemLeft cId == Just contactId && s /= GSMemRemoved && s /= GSMemLeft
checkSndFile :: CryptoFile -> m Integer checkSndFile :: MsgContent -> CryptoFile -> Integer -> m (Integer, SendFileMode)
checkSndFile (CryptoFile f cfArgs) = do checkSndFile mc (CryptoFile f cfArgs) n = do
fsFilePath <- toFSFilePath f fsFilePath <- toFSFilePath f
unlessM (doesFileExist fsFilePath) . throwChatError $ CEFileNotFound f unlessM (doesFileExist fsFilePath) . throwChatError $ CEFileNotFound f
ChatConfig {fileChunkSize, inlineFiles} <- asks config
xftpCfg <- readTVarIO =<< asks userXFTPFileConfig
fileSize <- liftIO $ CF.getFileContentsSize $ CryptoFile fsFilePath cfArgs fileSize <- liftIO $ CF.getFileContentsSize $ CryptoFile fsFilePath cfArgs
when (fromInteger fileSize > maxFileSize) $ throwChatError $ CEFileSize f when (fromInteger fileSize > maxFileSize) $ throwChatError $ CEFileSize f
pure fileSize let chunks = -((-fileSize) `div` fileChunkSize)
fileInline = inlineFileMode mc inlineFiles chunks n
fileMode = case xftpCfg of
Just cfg
| isJust cfArgs -> SendFileXFTP
| fileInline == Just IFMSent || fileSize < minFileSize cfg || n <= 0 -> SendFileSMP fileInline
| otherwise -> SendFileXFTP
_ -> SendFileSMP fileInline
pure (fileSize, fileMode)
inlineFileMode mc InlineFilesConfig {offerChunks, sendChunks, totalSendChunks} chunks n
| chunks > offerChunks = Nothing
| chunks <= sendChunks && chunks * n <= totalSendChunks && isVoice mc = Just IFMSent
| otherwise = Just IFMOffer
updateProfile :: User -> Profile -> m ChatResponse updateProfile :: User -> Profile -> m ChatResponse
updateProfile user p' = updateProfile_ user p' $ withStore $ \db -> updateUserProfile db user p' updateProfile user p' = updateProfile_ user p' $ withStore $ \db -> updateUserProfile db user p'
updateProfile_ :: User -> Profile -> m User -> m ChatResponse updateProfile_ :: User -> Profile -> m User -> m ChatResponse
@ -3056,7 +3132,7 @@ cleanupManager = do
cleanupDeletedContacts user = do cleanupDeletedContacts user = do
contacts <- withStore' (`getDeletedContacts` user) contacts <- withStore' (`getDeletedContacts` user)
forM_ contacts $ \ct -> forM_ contacts $ \ct ->
withStore (\db -> deleteContactWithoutGroups db user ct) withStore' (\db -> deleteContactWithoutGroups db user ct)
`catchChatError` (toView . CRChatError (Just user)) `catchChatError` (toView . CRChatError (Just user))
cleanupMessages = do cleanupMessages = do
ts <- liftIO getCurrentTime ts <- liftIO getCurrentTime
@ -4836,7 +4912,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
else do else do
contactConns <- withStore' $ \db -> getContactConnections db userId c contactConns <- withStore' $ \db -> getContactConnections db userId c
deleteAgentConnectionsAsync user $ map aConnId contactConns deleteAgentConnectionsAsync user $ map aConnId contactConns
withStore $ \db -> deleteContact db user c withStore' $ \db -> deleteContact db user c
where where
brokerTs = metaBrokerTs msgMeta brokerTs = metaBrokerTs msgMeta
@ -6419,6 +6495,8 @@ chatCommandP =
"/_temp_folder " *> (SetTempFolder <$> filePath), "/_temp_folder " *> (SetTempFolder <$> filePath),
("/_files_folder " <|> "/files_folder ") *> (SetFilesFolder <$> filePath), ("/_files_folder " <|> "/files_folder ") *> (SetFilesFolder <$> filePath),
"/remote_hosts_folder " *> (SetRemoteHostsFolder <$> filePath), "/remote_hosts_folder " *> (SetRemoteHostsFolder <$> filePath),
"/_xftp " *> (APISetXFTPConfig <$> ("on " *> (Just <$> jsonP) <|> ("off" $> Nothing))),
"/xftp " *> (APISetXFTPConfig <$> ("on" *> (Just <$> xftpCfgP) <|> ("off" $> Nothing))),
"/_files_encrypt " *> (APISetEncryptLocalFiles <$> onOffP), "/_files_encrypt " *> (APISetEncryptLocalFiles <$> onOffP),
"/contact_merge " *> (SetContactMergeEnabled <$> onOffP), "/contact_merge " *> (SetContactMergeEnabled <$> onOffP),
"/_db export " *> (APIExportArchive <$> jsonP), "/_db export " *> (APIExportArchive <$> jsonP),
@ -6790,6 +6868,14 @@ chatCommandP =
logErrors <- " log=" *> onOffP <|> pure False logErrors <- " log=" *> onOffP <|> pure False
let tcpTimeout = 1000000 * fromMaybe (maybe 5 (const 10) socksProxy) t_ let tcpTimeout = 1000000 * fromMaybe (maybe 5 (const 10) socksProxy) t_
pure $ fullNetworkConfig socksProxy tcpTimeout logErrors pure $ fullNetworkConfig socksProxy tcpTimeout logErrors
xftpCfgP = XFTPFileConfig <$> (" size=" *> fileSizeP <|> pure 0)
fileSizeP =
A.choice
[ gb <$> A.decimal <* "gb",
mb <$> A.decimal <* "mb",
kb <$> A.decimal <* "kb",
A.decimal
]
dbKeyP = nonEmptyKey <$?> strP dbKeyP = nonEmptyKey <$?> strP
nonEmptyKey k@(DBEncryptionKey s) = if BA.null s then Left "empty key" else Right k nonEmptyKey k@(DBEncryptionKey s) = if BA.null s then Left "empty key" else Right k
dbEncryptionConfig currentKey newKey = DBEncryptionConfig {currentKey, newKey, keepKey = Just False} dbEncryptionConfig currentKey newKey = DBEncryptionConfig {currentKey, newKey, keepKey = Just False}

View File

@ -128,6 +128,8 @@ data ChatConfig = ChatConfig
xftpDescrPartSize :: Int, xftpDescrPartSize :: Int,
inlineFiles :: InlineFilesConfig, inlineFiles :: InlineFilesConfig,
autoAcceptFileSize :: Integer, autoAcceptFileSize :: Integer,
xftpFileConfig :: Maybe XFTPFileConfig, -- Nothing - XFTP is disabled
tempDir :: Maybe FilePath,
showReactions :: Bool, showReactions :: Bool,
showReceipts :: Bool, showReceipts :: Bool,
subscriptionEvents :: Bool, subscriptionEvents :: Bool,
@ -202,6 +204,7 @@ data ChatController = ChatController
timedItemThreads :: TMap (ChatRef, ChatItemId) (TVar (Maybe (Weak ThreadId))), timedItemThreads :: TMap (ChatRef, ChatItemId) (TVar (Maybe (Weak ThreadId))),
showLiveItems :: TVar Bool, showLiveItems :: TVar Bool,
encryptLocalFiles :: TVar Bool, encryptLocalFiles :: TVar Bool,
userXFTPFileConfig :: TVar (Maybe XFTPFileConfig),
tempDirectory :: TVar (Maybe FilePath), tempDirectory :: TVar (Maybe FilePath),
logFilePath :: Maybe FilePath, logFilePath :: Maybe FilePath,
contactMergeEnabled :: TVar Bool contactMergeEnabled :: TVar Bool
@ -239,6 +242,7 @@ data ChatCommand
| SetTempFolder FilePath | SetTempFolder FilePath
| SetFilesFolder FilePath | SetFilesFolder FilePath
| SetRemoteHostsFolder FilePath | SetRemoteHostsFolder FilePath
| APISetXFTPConfig (Maybe XFTPFileConfig)
| APISetEncryptLocalFiles Bool | APISetEncryptLocalFiles Bool
| SetContactMergeEnabled Bool | SetContactMergeEnabled Bool
| APIExportArchive ArchiveConfig | APIExportArchive ArchiveConfig
@ -469,6 +473,7 @@ allowRemoteCommand = \case
SetTempFolder _ -> False SetTempFolder _ -> False
SetFilesFolder _ -> False SetFilesFolder _ -> False
SetRemoteHostsFolder _ -> False SetRemoteHostsFolder _ -> False
APISetXFTPConfig _ -> False
APISetEncryptLocalFiles _ -> False APISetEncryptLocalFiles _ -> False
APIExportArchive _ -> False APIExportArchive _ -> False
APIImportArchive _ -> False APIImportArchive _ -> False
@ -929,6 +934,14 @@ instance FromJSON ComposedMessage where
parseJSON invalid = parseJSON invalid =
JT.prependFailure "bad ComposedMessage, " (JT.typeMismatch "Object" invalid) JT.prependFailure "bad ComposedMessage, " (JT.typeMismatch "Object" invalid)
data XFTPFileConfig = XFTPFileConfig
{ minFileSize :: Integer
}
deriving (Show)
defaultXFTPFileConfig :: XFTPFileConfig
defaultXFTPFileConfig = XFTPFileConfig {minFileSize = 0}
data NtfMsgInfo = NtfMsgInfo {msgId :: Text, msgTs :: UTCTime} data NtfMsgInfo = NtfMsgInfo {msgId :: Text, msgTs :: UTCTime}
deriving (Show) deriving (Show)
@ -988,6 +1001,11 @@ data CoreVersionInfo = CoreVersionInfo
} }
deriving (Show) deriving (Show)
data SendFileMode
= SendFileSMP (Maybe InlineFileMode)
| SendFileXFTP
deriving (Show)
data SlowSQLQuery = SlowSQLQuery data SlowSQLQuery = SlowSQLQuery
{ query :: Text, { query :: Text,
queryStats :: SlowQueryStats queryStats :: SlowQueryStats
@ -1391,4 +1409,6 @@ $(JQ.deriveFromJSON defaultJSON ''ArchiveConfig)
$(JQ.deriveFromJSON defaultJSON ''DBEncryptionConfig) $(JQ.deriveFromJSON defaultJSON ''DBEncryptionConfig)
$(JQ.deriveJSON defaultJSON ''XFTPFileConfig)
$(JQ.deriveToJSON defaultJSON ''ComposedMessage) $(JQ.deriveToJSON defaultJSON ''ComposedMessage)

View File

@ -229,16 +229,13 @@ deleteContactConnectionsAndFiles db userId Contact {contactId} = do
(userId, contactId) (userId, contactId)
DB.execute db "DELETE FROM files WHERE user_id = ? AND contact_id = ?" (userId, contactId) DB.execute db "DELETE FROM files WHERE user_id = ? AND contact_id = ?" (userId, contactId)
deleteContact :: DB.Connection -> User -> Contact -> ExceptT StoreError IO () deleteContact :: DB.Connection -> User -> Contact -> IO ()
deleteContact db user@User {userId} ct@Contact {contactId, localDisplayName, activeConn} = do deleteContact db user@User {userId} Contact {contactId, localDisplayName, activeConn} = do
assertNotUser db user ct
liftIO $ do
DB.execute db "DELETE FROM chat_items WHERE user_id = ? AND contact_id = ?" (userId, contactId) DB.execute db "DELETE FROM chat_items WHERE user_id = ? AND contact_id = ?" (userId, contactId)
ctMember :: (Maybe ContactId) <- maybeFirstRow fromOnly $ DB.query db "SELECT contact_id FROM group_members WHERE user_id = ? AND contact_id = ? LIMIT 1" (userId, contactId) ctMember :: (Maybe ContactId) <- maybeFirstRow fromOnly $ DB.query db "SELECT contact_id FROM group_members WHERE user_id = ? AND contact_id = ? LIMIT 1" (userId, contactId)
if isNothing ctMember if isNothing ctMember
then do then do
deleteContactProfile_ db userId contactId deleteContactProfile_ db userId contactId
-- user's local display name already checked in assertNotUser
DB.execute db "DELETE FROM display_names WHERE user_id = ? AND local_display_name = ?" (userId, localDisplayName) DB.execute db "DELETE FROM display_names WHERE user_id = ? AND local_display_name = ?" (userId, localDisplayName)
else do else do
currentTs <- getCurrentTime currentTs <- getCurrentTime
@ -249,23 +246,18 @@ deleteContact db user@User {userId} ct@Contact {contactId, localDisplayName, act
deleteUnusedIncognitoProfileById_ db user profileId deleteUnusedIncognitoProfileById_ db user profileId
-- should only be used if contact is not member of any groups -- should only be used if contact is not member of any groups
deleteContactWithoutGroups :: DB.Connection -> User -> Contact -> ExceptT StoreError IO () deleteContactWithoutGroups :: DB.Connection -> User -> Contact -> IO ()
deleteContactWithoutGroups db user@User {userId} ct@Contact {contactId, localDisplayName, activeConn} = do deleteContactWithoutGroups db user@User {userId} Contact {contactId, localDisplayName, activeConn} = do
assertNotUser db user ct
liftIO $ do
DB.execute db "DELETE FROM chat_items WHERE user_id = ? AND contact_id = ?" (userId, contactId) DB.execute db "DELETE FROM chat_items WHERE user_id = ? AND contact_id = ?" (userId, contactId)
deleteContactProfile_ db userId contactId deleteContactProfile_ db userId contactId
-- user's local display name already checked in assertNotUser
DB.execute db "DELETE FROM display_names WHERE user_id = ? AND local_display_name = ?" (userId, localDisplayName) DB.execute db "DELETE FROM display_names WHERE user_id = ? AND local_display_name = ?" (userId, localDisplayName)
DB.execute db "DELETE FROM contacts WHERE user_id = ? AND contact_id = ?" (userId, contactId) DB.execute db "DELETE FROM contacts WHERE user_id = ? AND contact_id = ?" (userId, contactId)
forM_ activeConn $ \Connection {customUserProfileId} -> forM_ activeConn $ \Connection {customUserProfileId} ->
forM_ customUserProfileId $ \profileId -> forM_ customUserProfileId $ \profileId ->
deleteUnusedIncognitoProfileById_ db user profileId deleteUnusedIncognitoProfileById_ db user profileId
setContactDeleted :: DB.Connection -> User -> Contact -> ExceptT StoreError IO () setContactDeleted :: DB.Connection -> User -> Contact -> IO ()
setContactDeleted db user@User {userId} ct@Contact {contactId} = do setContactDeleted db User {userId} Contact {contactId} = do
assertNotUser db user ct
liftIO $ do
currentTs <- getCurrentTime currentTs <- getCurrentTime
DB.execute db "UPDATE contacts SET deleted = 1, updated_at = ? WHERE user_id = ? AND contact_id = ?" (currentTs, userId, contactId) DB.execute db "UPDATE contacts SET deleted = 1, updated_at = ? WHERE user_id = ? AND contact_id = ?" (currentTs, userId, contactId)
@ -328,7 +320,7 @@ updateContactProfile db user@User {userId} c p'
ExceptT . withLocalDisplayName db userId newName $ \ldn -> do ExceptT . withLocalDisplayName db userId newName $ \ldn -> do
currentTs <- getCurrentTime currentTs <- getCurrentTime
updateContactProfile_' db userId profileId p' currentTs updateContactProfile_' db userId profileId p' currentTs
updateContactLDN_ db user contactId localDisplayName ldn currentTs updateContactLDN_ db userId contactId localDisplayName ldn currentTs
pure $ Right c {localDisplayName = ldn, profile, mergedPreferences} pure $ Right c {localDisplayName = ldn, profile, mergedPreferences}
where where
Contact {contactId, localDisplayName, profile = LocalProfile {profileId, displayName, localAlias}, userPreferences} = c Contact {contactId, localDisplayName, profile = LocalProfile {profileId, displayName, localAlias}, userPreferences} = c
@ -499,8 +491,8 @@ updateMemberContactProfile_' db userId profileId Profile {displayName, fullName,
|] |]
(displayName, fullName, image, updatedAt, userId, profileId) (displayName, fullName, image, updatedAt, userId, profileId)
updateContactLDN_ :: DB.Connection -> User -> Int64 -> ContactName -> ContactName -> UTCTime -> IO () updateContactLDN_ :: DB.Connection -> UserId -> Int64 -> ContactName -> ContactName -> UTCTime -> IO ()
updateContactLDN_ db user@User {userId} contactId displayName newName updatedAt = do updateContactLDN_ db userId contactId displayName newName updatedAt = do
DB.execute DB.execute
db db
"UPDATE contacts SET local_display_name = ?, updated_at = ? WHERE user_id = ? AND contact_id = ?" "UPDATE contacts SET local_display_name = ?, updated_at = ? WHERE user_id = ? AND contact_id = ?"
@ -509,7 +501,7 @@ updateContactLDN_ db user@User {userId} contactId displayName newName updatedAt
db db
"UPDATE group_members SET local_display_name = ?, updated_at = ? WHERE user_id = ? AND contact_id = ?" "UPDATE group_members SET local_display_name = ?, updated_at = ? WHERE user_id = ? AND contact_id = ?"
(newName, updatedAt, userId, contactId) (newName, updatedAt, userId, contactId)
safeDeleteLDN db user displayName DB.execute db "DELETE FROM display_names WHERE local_display_name = ? AND user_id = ?" (displayName, userId)
getContactByName :: DB.Connection -> User -> ContactName -> ExceptT StoreError IO Contact getContactByName :: DB.Connection -> User -> ContactName -> ExceptT StoreError IO Contact
getContactByName db user localDisplayName = do getContactByName db user localDisplayName = do
@ -622,7 +614,7 @@ createOrUpdateContactRequest db user@User {userId} userContactLinkId invId (Vers
WHERE user_id = ? AND contact_request_id = ? WHERE user_id = ? AND contact_request_id = ?
|] |]
(invId, minV, maxV, ldn, currentTs, userId, cReqId) (invId, minV, maxV, ldn, currentTs, userId, cReqId)
safeDeleteLDN db user oldLdn DB.execute db "DELETE FROM display_names WHERE local_display_name = ? AND user_id = ?" (oldLdn, userId)
where where
updateProfile currentTs = updateProfile currentTs =
DB.execute DB.execute
@ -692,9 +684,8 @@ deleteContactRequest db User {userId} contactRequestId = do
SELECT local_display_name FROM contact_requests SELECT local_display_name FROM contact_requests
WHERE user_id = ? AND contact_request_id = ? WHERE user_id = ? AND contact_request_id = ?
) )
AND local_display_name NOT IN (SELECT local_display_name FROM users WHERE user_id = ?)
|] |]
(userId, userId, contactRequestId, userId) (userId, userId, contactRequestId)
DB.execute db "DELETE FROM contact_requests WHERE user_id = ? AND contact_request_id = ?" (userId, contactRequestId) DB.execute db "DELETE FROM contact_requests WHERE user_id = ? AND contact_request_id = ?" (userId, contactRequestId)
createAcceptedContact :: DB.Connection -> User -> ConnId -> VersionRange -> ContactName -> ProfileId -> Profile -> Int64 -> Maybe XContactId -> Maybe IncognitoProfile -> SubscriptionMode -> Bool -> IO Contact createAcceptedContact :: DB.Connection -> User -> ConnId -> VersionRange -> ContactName -> ProfileId -> Profile -> Int64 -> Maybe XContactId -> Maybe IncognitoProfile -> SubscriptionMode -> Bool -> IO Contact

View File

@ -14,6 +14,7 @@ module Simplex.Chat.Store.Files
( getLiveSndFileTransfers, ( getLiveSndFileTransfers,
getLiveRcvFileTransfers, getLiveRcvFileTransfers,
getPendingSndChunks, getPendingSndChunks,
createSndDirectFileTransfer,
createSndDirectFTConnection, createSndDirectFTConnection,
createSndGroupFileTransfer, createSndGroupFileTransfer,
createSndGroupFileTransferConnection, createSndGroupFileTransferConnection,
@ -168,6 +169,23 @@ getPendingSndChunks db fileId connId =
|] |]
(fileId, connId) (fileId, connId)
createSndDirectFileTransfer :: DB.Connection -> UserId -> Contact -> FilePath -> FileInvitation -> Maybe ConnId -> Integer -> SubscriptionMode -> IO FileTransferMeta
createSndDirectFileTransfer db userId Contact {contactId} filePath FileInvitation {fileName, fileSize, fileInline} acId_ chunkSize subMode = do
currentTs <- getCurrentTime
DB.execute
db
"INSERT INTO files (user_id, contact_id, file_name, file_path, file_size, chunk_size, file_inline, ci_file_status, protocol, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?,?,?,?)"
((userId, contactId, fileName, filePath, fileSize, chunkSize) :. (fileInline, CIFSSndStored, FPSMP, currentTs, currentTs))
fileId <- insertedRowId db
forM_ acId_ $ \acId -> do
Connection {connId} <- createSndFileConnection_ db userId fileId acId subMode
let fileStatus = FSNew
DB.execute
db
"INSERT INTO snd_files (file_id, file_status, file_inline, connection_id, created_at, updated_at) VALUES (?,?,?,?,?,?)"
(fileId, fileStatus, fileInline, connId, currentTs, currentTs)
pure FileTransferMeta {fileId, xftpSndFile = Nothing, fileName, filePath, fileSize, fileInline, chunkSize, cancelled = False}
createSndDirectFTConnection :: DB.Connection -> User -> Int64 -> (CommandId, ConnId) -> SubscriptionMode -> IO () createSndDirectFTConnection :: DB.Connection -> User -> Int64 -> (CommandId, ConnId) -> SubscriptionMode -> IO ()
createSndDirectFTConnection db user@User {userId} fileId (cmdId, acId) subMode = do createSndDirectFTConnection db user@User {userId} fileId (cmdId, acId) subMode = do
currentTs <- getCurrentTime currentTs <- getCurrentTime

View File

@ -225,9 +225,8 @@ deleteGroupLink db User {userId} GroupInfo {groupId} = do
JOIN user_contact_links uc USING (user_contact_link_id) JOIN user_contact_links uc USING (user_contact_link_id)
WHERE uc.user_id = ? AND uc.group_id = ? WHERE uc.user_id = ? AND uc.group_id = ?
) )
AND local_display_name NOT IN (SELECT local_display_name FROM users WHERE user_id = ?)
|] |]
(userId, userId, groupId, userId) (userId, userId, groupId)
DB.execute DB.execute
db db
[sql| [sql|
@ -587,7 +586,7 @@ deleteGroup :: DB.Connection -> User -> GroupInfo -> IO ()
deleteGroup db user@User {userId} g@GroupInfo {groupId, localDisplayName} = do deleteGroup db user@User {userId} g@GroupInfo {groupId, localDisplayName} = do
deleteGroupProfile_ db userId groupId deleteGroupProfile_ db userId groupId
DB.execute db "DELETE FROM groups WHERE user_id = ? AND group_id = ?" (userId, groupId) DB.execute db "DELETE FROM groups WHERE user_id = ? AND group_id = ?" (userId, groupId)
safeDeleteLDN db user localDisplayName DB.execute db "DELETE FROM display_names WHERE user_id = ? AND local_display_name = ?" (userId, localDisplayName)
forM_ (incognitoMembershipProfile g) $ deleteUnusedIncognitoProfileById_ db user . localProfileId forM_ (incognitoMembershipProfile g) $ deleteUnusedIncognitoProfileById_ db user . localProfileId
deleteGroupProfile_ :: DB.Connection -> UserId -> GroupId -> IO () deleteGroupProfile_ :: DB.Connection -> UserId -> GroupId -> IO ()
@ -1045,14 +1044,14 @@ deleteGroupMember db user@User {userId} m@GroupMember {groupMemberId, groupId, m
when (memberIncognito m) $ deleteUnusedIncognitoProfileById_ db user $ localProfileId memberProfile when (memberIncognito m) $ deleteUnusedIncognitoProfileById_ db user $ localProfileId memberProfile
cleanupMemberProfileAndName_ :: DB.Connection -> User -> GroupMember -> IO () cleanupMemberProfileAndName_ :: DB.Connection -> User -> GroupMember -> IO ()
cleanupMemberProfileAndName_ db user@User {userId} GroupMember {groupMemberId, memberContactId, memberContactProfileId, localDisplayName} = cleanupMemberProfileAndName_ db User {userId} GroupMember {groupMemberId, memberContactId, memberContactProfileId, localDisplayName} =
-- check record has no memberContactId (contact_id) - it means contact has been deleted and doesn't use profile & ldn -- check record has no memberContactId (contact_id) - it means contact has been deleted and doesn't use profile & ldn
when (isNothing memberContactId) $ do when (isNothing memberContactId) $ do
-- check other group member records don't use profile & ldn -- check other group member records don't use profile & ldn
sameProfileMember :: (Maybe GroupMemberId) <- maybeFirstRow fromOnly $ DB.query db "SELECT group_member_id FROM group_members WHERE user_id = ? AND contact_profile_id = ? AND group_member_id != ? LIMIT 1" (userId, memberContactProfileId, groupMemberId) sameProfileMember :: (Maybe GroupMemberId) <- maybeFirstRow fromOnly $ DB.query db "SELECT group_member_id FROM group_members WHERE user_id = ? AND contact_profile_id = ? AND group_member_id != ? LIMIT 1" (userId, memberContactProfileId, groupMemberId)
when (isNothing sameProfileMember) $ do when (isNothing sameProfileMember) $ do
DB.execute db "DELETE FROM contact_profiles WHERE user_id = ? AND contact_profile_id = ?" (userId, memberContactProfileId) DB.execute db "DELETE FROM contact_profiles WHERE user_id = ? AND contact_profile_id = ?" (userId, memberContactProfileId)
safeDeleteLDN db user localDisplayName DB.execute db "DELETE FROM display_names WHERE user_id = ? AND local_display_name = ?" (userId, localDisplayName)
deleteGroupMemberConnection :: DB.Connection -> User -> GroupMember -> IO () deleteGroupMemberConnection :: DB.Connection -> User -> GroupMember -> IO ()
deleteGroupMemberConnection db User {userId} GroupMember {groupMemberId} = deleteGroupMemberConnection db User {userId} GroupMember {groupMemberId} =
@ -1331,7 +1330,7 @@ getViaGroupContact db user@User {userId} GroupMember {groupMemberId} = do
maybe (pure Nothing) (fmap eitherToMaybe . runExceptT . getContact db user) contactId_ maybe (pure Nothing) (fmap eitherToMaybe . runExceptT . getContact db user) contactId_
updateGroupProfile :: DB.Connection -> User -> GroupInfo -> GroupProfile -> ExceptT StoreError IO GroupInfo updateGroupProfile :: DB.Connection -> User -> GroupInfo -> GroupProfile -> ExceptT StoreError IO GroupInfo
updateGroupProfile db user@User {userId} g@GroupInfo {groupId, localDisplayName, groupProfile = GroupProfile {displayName}} p'@GroupProfile {displayName = newName, fullName, description, image, groupPreferences} updateGroupProfile db User {userId} g@GroupInfo {groupId, localDisplayName, groupProfile = GroupProfile {displayName}} p'@GroupProfile {displayName = newName, fullName, description, image, groupPreferences}
| displayName == newName = liftIO $ do | displayName == newName = liftIO $ do
currentTs <- getCurrentTime currentTs <- getCurrentTime
updateGroupProfile_ currentTs updateGroupProfile_ currentTs
@ -1362,7 +1361,7 @@ updateGroupProfile db user@User {userId} g@GroupInfo {groupId, localDisplayName,
db db
"UPDATE groups SET local_display_name = ?, updated_at = ? WHERE user_id = ? AND group_id = ?" "UPDATE groups SET local_display_name = ?, updated_at = ? WHERE user_id = ? AND group_id = ?"
(ldn, currentTs, userId, groupId) (ldn, currentTs, userId, groupId)
safeDeleteLDN db user localDisplayName DB.execute db "DELETE FROM display_names WHERE local_display_name = ? AND user_id = ?" (localDisplayName, userId)
getGroupInfo :: DB.Connection -> VersionRange -> User -> Int64 -> ExceptT StoreError IO GroupInfo getGroupInfo :: DB.Connection -> VersionRange -> User -> Int64 -> ExceptT StoreError IO GroupInfo
getGroupInfo db vr User {userId, userContactId} groupId = getGroupInfo db vr User {userId, userContactId} groupId =
@ -1465,7 +1464,7 @@ getMatchingContacts db user@User {userId} Contact {contactId, profile = LocalPro
FROM contacts ct FROM contacts ct
JOIN contact_profiles p ON ct.contact_profile_id = p.contact_profile_id JOIN contact_profiles p ON ct.contact_profile_id = p.contact_profile_id
WHERE ct.user_id = ? AND ct.contact_id != ? WHERE ct.user_id = ? AND ct.contact_id != ?
AND ct.contact_status = ? AND ct.deleted = 0 AND ct.is_user = 0 AND ct.contact_status = ? AND ct.deleted = 0
AND p.display_name = ? AND p.full_name = ? AND p.display_name = ? AND p.full_name = ?
|] |]
@ -1503,7 +1502,7 @@ getMatchingMemberContacts db user@User {userId} GroupMember {memberProfile = Loc
FROM contacts ct FROM contacts ct
JOIN contact_profiles p ON ct.contact_profile_id = p.contact_profile_id JOIN contact_profiles p ON ct.contact_profile_id = p.contact_profile_id
WHERE ct.user_id = ? WHERE ct.user_id = ?
AND ct.contact_status = ? AND ct.deleted = 0 AND ct.is_user = 0 AND ct.contact_status = ? AND ct.deleted = 0
AND p.display_name = ? AND p.full_name = ? AND p.display_name = ? AND p.full_name = ?
|] |]
@ -1616,8 +1615,6 @@ mergeContactRecords db user@User {userId} to@Contact {localDisplayName = keepLDN
let (toCt, fromCt) = toFromContacts to from let (toCt, fromCt) = toFromContacts to from
Contact {contactId = toContactId, localDisplayName = toLDN} = toCt Contact {contactId = toContactId, localDisplayName = toLDN} = toCt
Contact {contactId = fromContactId, localDisplayName = fromLDN} = fromCt Contact {contactId = fromContactId, localDisplayName = fromLDN} = fromCt
assertNotUser db user toCt
assertNotUser db user fromCt
liftIO $ do liftIO $ do
currentTs <- getCurrentTime currentTs <- getCurrentTime
-- next query fixes incorrect unused contacts deletion -- next query fixes incorrect unused contacts deletion
@ -2021,7 +2018,7 @@ createMemberContactConn_
pure Connection {connId, agentConnId = AgentConnId acId, peerChatVRange, connType = ConnContact, contactConnInitiated = False, entityId = Just contactId, viaContact = Nothing, viaUserContactLink = Nothing, viaGroupLink = False, groupLinkId = Nothing, customUserProfileId, connLevel, connStatus = ConnJoined, localAlias = "", createdAt = currentTs, connectionCode = Nothing, authErrCounter = 0} pure Connection {connId, agentConnId = AgentConnId acId, peerChatVRange, connType = ConnContact, contactConnInitiated = False, entityId = Just contactId, viaContact = Nothing, viaUserContactLink = Nothing, viaGroupLink = False, groupLinkId = Nothing, customUserProfileId, connLevel, connStatus = ConnJoined, localAlias = "", createdAt = currentTs, connectionCode = Nothing, authErrCounter = 0}
updateMemberProfile :: DB.Connection -> User -> GroupMember -> Profile -> ExceptT StoreError IO GroupMember updateMemberProfile :: DB.Connection -> User -> GroupMember -> Profile -> ExceptT StoreError IO GroupMember
updateMemberProfile db user@User {userId} m p' updateMemberProfile db User {userId} m p'
| displayName == newName = do | displayName == newName = do
liftIO $ updateMemberContactProfileReset_ db userId profileId p' liftIO $ updateMemberContactProfileReset_ db userId profileId p'
pure m {memberProfile = profile} pure m {memberProfile = profile}
@ -2033,7 +2030,7 @@ updateMemberProfile db user@User {userId} m p'
db db
"UPDATE group_members SET local_display_name = ?, updated_at = ? WHERE user_id = ? AND group_member_id = ?" "UPDATE group_members SET local_display_name = ?, updated_at = ? WHERE user_id = ? AND group_member_id = ?"
(ldn, currentTs, userId, groupMemberId) (ldn, currentTs, userId, groupMemberId)
safeDeleteLDN db user localDisplayName DB.execute db "DELETE FROM display_names WHERE local_display_name = ? AND user_id = ?" (localDisplayName, userId)
pure $ Right m {localDisplayName = ldn, memberProfile = profile} pure $ Right m {localDisplayName = ldn, memberProfile = profile}
where where
GroupMember {groupMemberId, localDisplayName, memberProfile = LocalProfile {profileId, displayName, localAlias}} = m GroupMember {groupMemberId, localDisplayName, memberProfile = LocalProfile {profileId, displayName, localAlias}} = m
@ -2041,7 +2038,7 @@ updateMemberProfile db user@User {userId} m p'
profile = toLocalProfile profileId p' localAlias profile = toLocalProfile profileId p' localAlias
updateContactMemberProfile :: DB.Connection -> User -> GroupMember -> Contact -> Profile -> ExceptT StoreError IO (GroupMember, Contact) updateContactMemberProfile :: DB.Connection -> User -> GroupMember -> Contact -> Profile -> ExceptT StoreError IO (GroupMember, Contact)
updateContactMemberProfile db user@User {userId} m ct@Contact {contactId} p' updateContactMemberProfile db User {userId} m ct@Contact {contactId} p'
| displayName == newName = do | displayName == newName = do
liftIO $ updateMemberContactProfile_ db userId profileId p' liftIO $ updateMemberContactProfile_ db userId profileId p'
pure (m {memberProfile = profile}, ct {profile} :: Contact) pure (m {memberProfile = profile}, ct {profile} :: Contact)
@ -2049,7 +2046,7 @@ updateContactMemberProfile db user@User {userId} m ct@Contact {contactId} p'
ExceptT . withLocalDisplayName db userId newName $ \ldn -> do ExceptT . withLocalDisplayName db userId newName $ \ldn -> do
currentTs <- getCurrentTime currentTs <- getCurrentTime
updateMemberContactProfile_' db userId profileId p' currentTs updateMemberContactProfile_' db userId profileId p' currentTs
updateContactLDN_ db user contactId localDisplayName ldn currentTs updateContactLDN_ db userId contactId localDisplayName ldn currentTs
pure $ Right (m {localDisplayName = ldn, memberProfile = profile}, ct {localDisplayName = ldn, profile} :: Contact) pure $ Right (m {localDisplayName = ldn, memberProfile = profile}, ct {localDisplayName = ldn, profile} :: Contact)
where where
GroupMember {localDisplayName, memberProfile = LocalProfile {profileId, displayName, localAlias}} = m GroupMember {localDisplayName, memberProfile = LocalProfile {profileId, displayName, localAlias}} = m

View File

@ -267,7 +267,7 @@ updateUserProfile db user p'
"INSERT INTO display_names (local_display_name, ldn_base, user_id, created_at, updated_at) VALUES (?,?,?,?,?)" "INSERT INTO display_names (local_display_name, ldn_base, user_id, created_at, updated_at) VALUES (?,?,?,?,?)"
(newName, newName, userId, currentTs, currentTs) (newName, newName, userId, currentTs, currentTs)
updateContactProfile_' db userId profileId p' currentTs updateContactProfile_' db userId profileId p' currentTs
updateContactLDN_ db user userContactId localDisplayName newName currentTs updateContactLDN_ db userId userContactId localDisplayName newName currentTs
pure user {localDisplayName = newName, profile, fullPreferences, userMemberProfileUpdatedAt = userMemberProfileUpdatedAt'} pure user {localDisplayName = newName, profile, fullPreferences, userMemberProfileUpdatedAt = userMemberProfileUpdatedAt'}
where where
updateUserMemberProfileUpdatedAt_ currentTs updateUserMemberProfileUpdatedAt_ currentTs
@ -388,7 +388,6 @@ deleteUserAddress db user@User {userId} = do
JOIN user_contact_links uc USING (user_contact_link_id) JOIN user_contact_links uc USING (user_contact_link_id)
WHERE uc.user_id = :user_id AND uc.local_display_name = '' AND uc.group_id IS NULL WHERE uc.user_id = :user_id AND uc.local_display_name = '' AND uc.group_id IS NULL
) )
AND local_display_name NOT IN (SELECT local_display_name FROM users WHERE user_id = :user_id)
|] |]
[":user_id" := userId] [":user_id" := userId]
DB.executeNamed DB.executeNamed

View File

@ -110,7 +110,6 @@ data StoreError
| SERemoteHostDuplicateCA | SERemoteHostDuplicateCA
| SERemoteCtrlNotFound {remoteCtrlId :: RemoteCtrlId} | SERemoteCtrlNotFound {remoteCtrlId :: RemoteCtrlId}
| SERemoteCtrlDuplicateCA | SERemoteCtrlDuplicateCA
| SEProhibitedDeleteUser {userId :: UserId, contactId :: ContactId}
deriving (Show, Exception) deriving (Show, Exception)
$(J.deriveJSON (sumTypeJSON $ dropPrefix "SE") ''StoreError) $(J.deriveJSON (sumTypeJSON $ dropPrefix "SE") ''StoreError)
@ -402,33 +401,3 @@ createWithRandomBytes' size gVar create = tryCreate 3
encodedRandomBytes :: TVar ChaChaDRG -> Int -> IO ByteString encodedRandomBytes :: TVar ChaChaDRG -> Int -> IO ByteString
encodedRandomBytes gVar n = atomically $ B64.encode <$> C.randomBytes n gVar encodedRandomBytes gVar n = atomically $ B64.encode <$> C.randomBytes n gVar
assertNotUser :: DB.Connection -> User -> Contact -> ExceptT StoreError IO ()
assertNotUser db User {userId} Contact {contactId, localDisplayName} = do
r :: (Maybe Int64) <-
-- This query checks that the foreign keys in the users table
-- are not referencing the contact about to be deleted.
-- With the current schema it would cause cascade delete of user,
-- with mofified schema (in v5.6.0-beta.0) it would cause foreign key violation error.
liftIO . maybeFirstRow fromOnly $
DB.query
db
[sql|
SELECT 1 FROM users
WHERE (user_id = ? AND local_display_name = ?)
OR contact_id = ?
LIMIT 1
|]
(userId, localDisplayName, contactId)
when (isJust r) $ throwError $ SEProhibitedDeleteUser userId contactId
safeDeleteLDN :: DB.Connection -> User -> ContactName -> IO ()
safeDeleteLDN db User {userId} localDisplayName = do
DB.execute
db
[sql|
DELETE FROM display_names
WHERE user_id = ? AND local_display_name = ?
AND local_display_name NOT IN (SELECT local_display_name FROM users WHERE user_id = ?)
|]
(userId, localDisplayName, userId)

View File

@ -15,7 +15,6 @@ import Control.Concurrent.STM
import Control.Exception (bracket, bracket_) import Control.Exception (bracket, bracket_)
import Control.Monad import Control.Monad
import Control.Monad.Except import Control.Monad.Except
import Control.Monad.Reader
import Data.ByteArray (ScrubbedBytes) import Data.ByteArray (ScrubbedBytes)
import Data.Functor (($>)) import Data.Functor (($>))
import Data.List (dropWhileEnd, find) import Data.List (dropWhileEnd, find)
@ -23,7 +22,7 @@ import Data.Maybe (isNothing)
import qualified Data.Text as T import qualified Data.Text as T
import Network.Socket import Network.Socket
import Simplex.Chat import Simplex.Chat
import Simplex.Chat.Controller (ChatCommand (..), ChatConfig (..), ChatController (..), ChatDatabase (..), ChatLogLevel (..)) import Simplex.Chat.Controller (ChatConfig (..), ChatController (..), ChatDatabase (..), ChatLogLevel (..))
import Simplex.Chat.Core import Simplex.Chat.Core
import Simplex.Chat.Options import Simplex.Chat.Options
import Simplex.Chat.Store import Simplex.Chat.Store
@ -130,7 +129,8 @@ testCfg =
{ agentConfig = testAgentCfg, { agentConfig = testAgentCfg,
showReceipts = False, showReceipts = False,
testView = True, testView = True,
tbqSize = 16 tbqSize = 16,
xftpFileConfig = Nothing
} }
testAgentCfgVPrev :: AgentConfig testAgentCfgVPrev :: AgentConfig
@ -209,7 +209,6 @@ startTestChat_ db cfg opts user = do
t <- withVirtualTerminal termSettings pure t <- withVirtualTerminal termSettings pure
ct <- newChatTerminal t opts ct <- newChatTerminal t opts
cc <- newChatController db (Just user) cfg opts False cc <- newChatController db (Just user) cfg opts False
void $ execChatCommand' (SetTempFolder "tests/tmp/tmp") `runReaderT` cc
chatAsync <- async . runSimplexChat opts user cc $ \_u cc' -> runChatTerminal ct cc' opts chatAsync <- async . runSimplexChat opts user cc $ \_u cc' -> runChatTerminal ct cc' opts
atomically . unless (maintenance opts) $ readTVar (agentAsync cc) >>= \a -> when (isNothing a) retry atomically . unless (maintenance opts) $ readTVar (agentAsync cc) >>= \a -> when (isNothing a) retry
termQ <- newTQueueIO termQ <- newTQueueIO

View File

@ -1067,7 +1067,7 @@ testChatWorking alice bob = do
alice <# "bob> hello too" alice <# "bob> hello too"
testMaintenanceModeWithFiles :: HasCallStack => FilePath -> IO () testMaintenanceModeWithFiles :: HasCallStack => FilePath -> IO ()
testMaintenanceModeWithFiles tmp = withXFTPServer $ do testMaintenanceModeWithFiles tmp = do
withNewTestChat tmp "bob" bobProfile $ \bob -> do withNewTestChat tmp "bob" bobProfile $ \bob -> do
withNewTestChatOpts tmp testOpts {maintenance = True} "alice" aliceProfile $ \alice -> do withNewTestChatOpts tmp testOpts {maintenance = True} "alice" aliceProfile $ \alice -> do
alice ##> "/_start" alice ##> "/_start"
@ -1075,26 +1075,12 @@ testMaintenanceModeWithFiles tmp = withXFTPServer $ do
alice ##> "/_files_folder ./tests/tmp/alice_files" alice ##> "/_files_folder ./tests/tmp/alice_files"
alice <## "ok" alice <## "ok"
connectUsers alice bob connectUsers alice bob
startFileTransferWithDest' bob alice "test.jpg" "136.5 KiB / 139737 bytes" Nothing
bob #> "/f @alice ./tests/fixtures/test.jpg" bob <## "completed sending file 1 (test.jpg) to alice"
bob <## "use /fc 1 to cancel sending"
alice <# "bob> sends file test.jpg (136.5 KiB / 139737 bytes)"
alice <## "use /fr 1 [<dir>/ | <path>] to receive it"
bob <## "completed uploading file 1 (test.jpg) for alice"
alice ##> "/fr 1"
alice
<### [ "saving file 1 from bob to test.jpg",
"started receiving file 1 (test.jpg) from bob"
]
alice <## "completed receiving file 1 (test.jpg) from bob" alice <## "completed receiving file 1 (test.jpg) from bob"
src <- B.readFile "./tests/fixtures/test.jpg" src <- B.readFile "./tests/fixtures/test.jpg"
dest <- B.readFile "./tests/tmp/alice_files/test.jpg" B.readFile "./tests/tmp/alice_files/test.jpg" `shouldReturn` src
dest `shouldBe` src
threadDelay 500000 threadDelay 500000
alice ##> "/_stop" alice ##> "/_stop"
alice <## "chat stopped" alice <## "chat stopped"
alice ##> "/_db export {\"archivePath\": \"./tests/tmp/alice-chat.zip\"}" alice ##> "/_db export {\"archivePath\": \"./tests/tmp/alice-chat.zip\"}"

File diff suppressed because it is too large Load Diff

View File

@ -11,7 +11,7 @@ import Control.Monad (void, when)
import qualified Data.ByteString as B import qualified Data.ByteString as B
import Data.List (isInfixOf) import Data.List (isInfixOf)
import qualified Data.Text as T import qualified Data.Text as T
import Simplex.Chat.Controller (ChatConfig (..)) import Simplex.Chat.Controller (ChatConfig (..), XFTPFileConfig (..))
import Simplex.Chat.Protocol (supportedChatVRange) import Simplex.Chat.Protocol (supportedChatVRange)
import Simplex.Chat.Store (agentStoreFile, chatStoreFile) import Simplex.Chat.Store (agentStoreFile, chatStoreFile)
import Simplex.Chat.Types (GroupMemberRole (..)) import Simplex.Chat.Types (GroupMemberRole (..))
@ -4321,7 +4321,7 @@ testGroupMsgForwardDeletion =
testGroupMsgForwardFile :: HasCallStack => FilePath -> IO () testGroupMsgForwardFile :: HasCallStack => FilePath -> IO ()
testGroupMsgForwardFile = testGroupMsgForwardFile =
testChat3 aliceProfile bobProfile cathProfile $ testChatCfg3 cfg aliceProfile bobProfile cathProfile $
\alice bob cath -> withXFTPServer $ do \alice bob cath -> withXFTPServer $ do
setupGroupForwarding3 "team" alice bob cath setupGroupForwarding3 "team" alice bob cath
@ -4343,6 +4343,8 @@ testGroupMsgForwardFile =
src <- B.readFile "./tests/fixtures/test.jpg" src <- B.readFile "./tests/fixtures/test.jpg"
dest <- B.readFile "./tests/tmp/test.jpg" dest <- B.readFile "./tests/tmp/test.jpg"
dest `shouldBe` src dest `shouldBe` src
where
cfg = testCfg {xftpFileConfig = Just $ XFTPFileConfig {minFileSize = 0}, tempDir = Just "./tests/tmp"}
testGroupMsgForwardChangeRole :: HasCallStack => FilePath -> IO () testGroupMsgForwardChangeRole :: HasCallStack => FilePath -> IO ()
testGroupMsgForwardChangeRole = testGroupMsgForwardChangeRole =
@ -4575,7 +4577,7 @@ testGroupHistoryPreferenceOff =
testGroupHistoryHostFile :: HasCallStack => FilePath -> IO () testGroupHistoryHostFile :: HasCallStack => FilePath -> IO ()
testGroupHistoryHostFile = testGroupHistoryHostFile =
testChat3 aliceProfile bobProfile cathProfile $ testChatCfg3 cfg aliceProfile bobProfile cathProfile $
\alice bob cath -> withXFTPServer $ do \alice bob cath -> withXFTPServer $ do
createGroup2 "team" alice bob createGroup2 "team" alice bob
@ -4611,10 +4613,12 @@ testGroupHistoryHostFile =
src <- B.readFile "./tests/fixtures/test.jpg" src <- B.readFile "./tests/fixtures/test.jpg"
dest <- B.readFile "./tests/tmp/test.jpg" dest <- B.readFile "./tests/tmp/test.jpg"
dest `shouldBe` src dest `shouldBe` src
where
cfg = testCfg {xftpFileConfig = Just $ XFTPFileConfig {minFileSize = 0}, tempDir = Just "./tests/tmp"}
testGroupHistoryMemberFile :: HasCallStack => FilePath -> IO () testGroupHistoryMemberFile :: HasCallStack => FilePath -> IO ()
testGroupHistoryMemberFile = testGroupHistoryMemberFile =
testChat3 aliceProfile bobProfile cathProfile $ testChatCfg3 cfg aliceProfile bobProfile cathProfile $
\alice bob cath -> withXFTPServer $ do \alice bob cath -> withXFTPServer $ do
createGroup2 "team" alice bob createGroup2 "team" alice bob
@ -4650,6 +4654,8 @@ testGroupHistoryMemberFile =
src <- B.readFile "./tests/fixtures/test.jpg" src <- B.readFile "./tests/fixtures/test.jpg"
dest <- B.readFile "./tests/tmp/test.jpg" dest <- B.readFile "./tests/tmp/test.jpg"
dest `shouldBe` src dest `shouldBe` src
where
cfg = testCfg {xftpFileConfig = Just $ XFTPFileConfig {minFileSize = 0}, tempDir = Just "./tests/tmp"}
testGroupHistoryLargeFile :: HasCallStack => FilePath -> IO () testGroupHistoryLargeFile :: HasCallStack => FilePath -> IO ()
testGroupHistoryLargeFile = testGroupHistoryLargeFile =
@ -4707,11 +4713,11 @@ testGroupHistoryLargeFile =
destCath <- B.readFile "./tests/tmp/testfile_2" destCath <- B.readFile "./tests/tmp/testfile_2"
destCath `shouldBe` src destCath `shouldBe` src
where where
cfg = testCfg {xftpDescrPartSize = 200} cfg = testCfg {xftpDescrPartSize = 200, xftpFileConfig = Just $ XFTPFileConfig {minFileSize = 0}, tempDir = Just "./tests/tmp"}
testGroupHistoryMultipleFiles :: HasCallStack => FilePath -> IO () testGroupHistoryMultipleFiles :: HasCallStack => FilePath -> IO ()
testGroupHistoryMultipleFiles = testGroupHistoryMultipleFiles =
testChat3 aliceProfile bobProfile cathProfile $ testChatCfg3 cfg aliceProfile bobProfile cathProfile $
\alice bob cath -> withXFTPServer $ do \alice bob cath -> withXFTPServer $ do
xftpCLI ["rand", "./tests/tmp/testfile_bob", "2mb"] `shouldReturn` ["File created: " <> "./tests/tmp/testfile_bob"] xftpCLI ["rand", "./tests/tmp/testfile_bob", "2mb"] `shouldReturn` ["File created: " <> "./tests/tmp/testfile_bob"]
xftpCLI ["rand", "./tests/tmp/testfile_alice", "1mb"] `shouldReturn` ["File created: " <> "./tests/tmp/testfile_alice"] xftpCLI ["rand", "./tests/tmp/testfile_alice", "1mb"] `shouldReturn` ["File created: " <> "./tests/tmp/testfile_alice"]
@ -4788,10 +4794,12 @@ testGroupHistoryMultipleFiles =
`shouldContain` [ ((0, "hi alice"), Just "./tests/tmp/testfile_bob_1"), `shouldContain` [ ((0, "hi alice"), Just "./tests/tmp/testfile_bob_1"),
((0, "hey bob"), Just "./tests/tmp/testfile_alice_1") ((0, "hey bob"), Just "./tests/tmp/testfile_alice_1")
] ]
where
cfg = testCfg {xftpFileConfig = Just $ XFTPFileConfig {minFileSize = 0}, tempDir = Just "./tests/tmp"}
testGroupHistoryFileCancel :: HasCallStack => FilePath -> IO () testGroupHistoryFileCancel :: HasCallStack => FilePath -> IO ()
testGroupHistoryFileCancel = testGroupHistoryFileCancel =
testChat3 aliceProfile bobProfile cathProfile $ testChatCfg3 cfg aliceProfile bobProfile cathProfile $
\alice bob cath -> withXFTPServer $ do \alice bob cath -> withXFTPServer $ do
xftpCLI ["rand", "./tests/tmp/testfile_bob", "2mb"] `shouldReturn` ["File created: " <> "./tests/tmp/testfile_bob"] xftpCLI ["rand", "./tests/tmp/testfile_bob", "2mb"] `shouldReturn` ["File created: " <> "./tests/tmp/testfile_bob"]
xftpCLI ["rand", "./tests/tmp/testfile_alice", "1mb"] `shouldReturn` ["File created: " <> "./tests/tmp/testfile_alice"] xftpCLI ["rand", "./tests/tmp/testfile_alice", "1mb"] `shouldReturn` ["File created: " <> "./tests/tmp/testfile_alice"]
@ -4843,10 +4851,12 @@ testGroupHistoryFileCancel =
bob <## "#team: alice added cath (Catherine) to the group (connecting...)" bob <## "#team: alice added cath (Catherine) to the group (connecting...)"
bob <## "#team: new member cath is connected" bob <## "#team: new member cath is connected"
] ]
where
cfg = testCfg {xftpFileConfig = Just $ XFTPFileConfig {minFileSize = 0}, tempDir = Just "./tests/tmp"}
testGroupHistoryFileCancelNoText :: HasCallStack => FilePath -> IO () testGroupHistoryFileCancelNoText :: HasCallStack => FilePath -> IO ()
testGroupHistoryFileCancelNoText = testGroupHistoryFileCancelNoText =
testChat3 aliceProfile bobProfile cathProfile $ testChatCfg3 cfg aliceProfile bobProfile cathProfile $
\alice bob cath -> withXFTPServer $ do \alice bob cath -> withXFTPServer $ do
xftpCLI ["rand", "./tests/tmp/testfile_bob", "2mb"] `shouldReturn` ["File created: " <> "./tests/tmp/testfile_bob"] xftpCLI ["rand", "./tests/tmp/testfile_bob", "2mb"] `shouldReturn` ["File created: " <> "./tests/tmp/testfile_bob"]
xftpCLI ["rand", "./tests/tmp/testfile_alice", "1mb"] `shouldReturn` ["File created: " <> "./tests/tmp/testfile_alice"] xftpCLI ["rand", "./tests/tmp/testfile_alice", "1mb"] `shouldReturn` ["File created: " <> "./tests/tmp/testfile_alice"]
@ -4902,6 +4912,8 @@ testGroupHistoryFileCancelNoText =
bob <## "#team: alice added cath (Catherine) to the group (connecting...)" bob <## "#team: alice added cath (Catherine) to the group (connecting...)"
bob <## "#team: new member cath is connected" bob <## "#team: new member cath is connected"
] ]
where
cfg = testCfg {xftpFileConfig = Just $ XFTPFileConfig {minFileSize = 0}, tempDir = Just "./tests/tmp"}
testGroupHistoryQuotes :: HasCallStack => FilePath -> IO () testGroupHistoryQuotes :: HasCallStack => FilePath -> IO ()
testGroupHistoryQuotes = testGroupHistoryQuotes =

View File

@ -12,6 +12,7 @@ import Simplex.Chat.Controller (ChatConfig (..), InlineFilesConfig (..), default
import System.Directory (copyFile, doesFileExist) import System.Directory (copyFile, doesFileExist)
import System.FilePath ((</>)) import System.FilePath ((</>))
import Test.Hspec hiding (it) import Test.Hspec hiding (it)
import UnliftIO.Async (concurrently_)
chatLocalChatsTests :: SpecWith FilePath chatLocalChatsTests :: SpecWith FilePath
chatLocalChatsTests = do chatLocalChatsTests = do
@ -157,24 +158,24 @@ testFiles tmp = withNewTestChat tmp "alice" aliceProfile $ \alice -> do
testOtherFiles :: FilePath -> IO () testOtherFiles :: FilePath -> IO ()
testOtherFiles = testOtherFiles =
testChatCfg2 cfg aliceProfile bobProfile $ \alice bob -> withXFTPServer $ do testChatCfg2 cfg aliceProfile bobProfile $ \alice bob -> do
connectUsers alice bob connectUsers alice bob
createCCNoteFolder bob createCCNoteFolder bob
bob ##> "/_files_folder ./tests/tmp/" bob ##> "/_files_folder ./tests/tmp/"
bob <## "ok" bob <## "ok"
alice ##> "/_send @2 json {\"msgContent\":{\"type\":\"voice\", \"duration\":10, \"text\":\"\"}, \"filePath\":\"./tests/fixtures/test.jpg\"}"
alice #> "/f @bob ./tests/fixtures/test.jpg" alice <# "@bob voice message (00:10)"
alice <## "use /fc 1 to cancel sending" alice <# "/f @bob ./tests/fixtures/test.jpg"
-- below is not shown in "sent" mode
-- alice <## "use /fc 1 to cancel sending"
bob <# "alice> voice message (00:10)"
bob <# "alice> sends file test.jpg (136.5 KiB / 139737 bytes)" bob <# "alice> sends file test.jpg (136.5 KiB / 139737 bytes)"
bob <## "use /fr 1 [<dir>/ | <path>] to receive it" -- below is not shown in "sent" mode
alice <## "completed uploading file 1 (test.jpg) for bob" -- bob <## "use /fr 1 [<dir>/ | <path>] to receive it"
bob <## "started receiving file 1 (test.jpg) from alice"
bob ##> "/fr 1" concurrently_
bob (alice <## "completed sending file 1 (test.jpg) to bob")
<### [ "saving file 1 from alice to test.jpg", (bob <## "completed receiving file 1 (test.jpg) from alice")
"started receiving file 1 (test.jpg) from alice"
]
bob <## "completed receiving file 1 (test.jpg) from alice"
bob /* "test" bob /* "test"
bob ##> "/tail *" bob ##> "/tail *"

View File

@ -1493,7 +1493,7 @@ testSetConnectionAlias = testChat2 aliceProfile bobProfile $
testSetContactPrefs :: HasCallStack => FilePath -> IO () testSetContactPrefs :: HasCallStack => FilePath -> IO ()
testSetContactPrefs = testChat2 aliceProfile bobProfile $ testSetContactPrefs = testChat2 aliceProfile bobProfile $
\alice bob -> withXFTPServer $ do \alice bob -> do
alice #$> ("/_files_folder ./tests/tmp/alice", id, "ok") alice #$> ("/_files_folder ./tests/tmp/alice", id, "ok")
bob #$> ("/_files_folder ./tests/tmp/bob", id, "ok") bob #$> ("/_files_folder ./tests/tmp/bob", id, "ok")
createDirectoryIfMissing True "./tests/tmp/alice" createDirectoryIfMissing True "./tests/tmp/alice"
@ -1528,24 +1528,15 @@ testSetContactPrefs = testChat2 aliceProfile bobProfile $
bob #$> ("/_get chat @2 count=100", chat, startFeatures <> [(0, "Voice messages: enabled for you")]) bob #$> ("/_get chat @2 count=100", chat, startFeatures <> [(0, "Voice messages: enabled for you")])
alice ##> sendVoice alice ##> sendVoice
alice <## voiceNotAllowed alice <## voiceNotAllowed
-- sending voice message allowed
bob ##> sendVoice bob ##> sendVoice
bob <# "@alice voice message (00:10)" bob <# "@alice voice message (00:10)"
bob <# "/f @alice test.txt" bob <# "/f @alice test.txt"
bob <## "use /fc 1 to cancel sending" bob <## "completed sending file 1 (test.txt) to alice"
alice <# "bob> voice message (00:10)" alice <# "bob> voice message (00:10)"
alice <# "bob> sends file test.txt (11 bytes / 11 bytes)" alice <# "bob> sends file test.txt (11 bytes / 11 bytes)"
alice <## "use /fr 1 [<dir>/ | <path>] to receive it" alice <## "started receiving file 1 (test.txt) from bob"
bob <## "completed uploading file 1 (test.txt) for alice"
alice ##> "/fr 1"
alice
<### [ "saving file 1 from bob to test_1.txt",
"started receiving file 1 (test.txt) from bob"
]
alice <## "completed receiving file 1 (test.txt) from bob" alice <## "completed receiving file 1 (test.txt) from bob"
(bob </) (bob </)
-- alice ##> "/_profile 1 {\"displayName\": \"alice\", \"fullName\": \"Alice\", \"preferences\": {\"voice\": {\"allow\": \"no\"}}}" -- alice ##> "/_profile 1 {\"displayName\": \"alice\", \"fullName\": \"Alice\", \"preferences\": {\"voice\": {\"allow\": \"no\"}}}"
alice ##> "/set voice no" alice ##> "/set voice no"
alice <## "updated preferences:" alice <## "updated preferences:"

View File

@ -19,7 +19,7 @@ import Data.Maybe (fromMaybe)
import Data.String import Data.String
import qualified Data.Text as T import qualified Data.Text as T
import Database.SQLite.Simple (Only (..)) import Database.SQLite.Simple (Only (..))
import Simplex.Chat.Controller (ChatConfig (..), ChatController (..)) import Simplex.Chat.Controller (ChatConfig (..), ChatController (..), InlineFilesConfig (..), defaultInlineFilesConfig)
import Simplex.Chat.Protocol import Simplex.Chat.Protocol
import Simplex.Chat.Store.NoteFolders (createNoteFolder) import Simplex.Chat.Store.NoteFolders (createNoteFolder)
import Simplex.Chat.Store.Profiles (getUserContactProfiles) import Simplex.Chat.Store.Profiles (getUserContactProfiles)
@ -32,6 +32,7 @@ import Simplex.Messaging.Encoding.String
import Simplex.Messaging.Version import Simplex.Messaging.Version
import System.Directory (doesFileExist) import System.Directory (doesFileExist)
import System.Environment (lookupEnv, withArgs) import System.Environment (lookupEnv, withArgs)
import System.FilePath ((</>))
import System.IO.Silently (capture_) import System.IO.Silently (capture_)
import System.Info (os) import System.Info (os)
import Test.Hspec hiding (it) import Test.Hspec hiding (it)
@ -95,6 +96,29 @@ versionTestMatrix3 runTest = do
it "curr to prev" $ runTestCfg3 testCfgVPrev testCfg testCfg runTest it "curr to prev" $ runTestCfg3 testCfgVPrev testCfg testCfg runTest
it "curr+prev to prev" $ runTestCfg3 testCfgVPrev testCfg testCfgVPrev runTest it "curr+prev to prev" $ runTestCfg3 testCfgVPrev testCfg testCfgVPrev runTest
inlineCfg :: Integer -> ChatConfig
inlineCfg n = testCfg {inlineFiles = defaultInlineFilesConfig {sendChunks = 0, offerChunks = n, receiveChunks = n}}
fileTestMatrix2 :: (HasCallStack => TestCC -> TestCC -> IO ()) -> SpecWith FilePath
fileTestMatrix2 runTest = do
it "via connection" $ runTestCfg2 viaConn viaConn runTest
it "inline (accepting)" $ runTestCfg2 inline inline runTest
it "via connection (inline offered)" $ runTestCfg2 inline viaConn runTest
it "via connection (inline supported)" $ runTestCfg2 viaConn inline runTest
where
inline = inlineCfg 100
viaConn = inlineCfg 0
fileTestMatrix3 :: (HasCallStack => TestCC -> TestCC -> TestCC -> IO ()) -> SpecWith FilePath
fileTestMatrix3 runTest = do
it "via connection" $ runTestCfg3 viaConn viaConn viaConn runTest
it "inline" $ runTestCfg3 inline inline inline runTest
it "via connection (inline offered)" $ runTestCfg3 inline viaConn viaConn runTest
it "via connection (inline supported)" $ runTestCfg3 viaConn inline inline runTest
where
inline = inlineCfg 100
viaConn = inlineCfg 0
runTestCfg2 :: ChatConfig -> ChatConfig -> (HasCallStack => TestCC -> TestCC -> IO ()) -> FilePath -> IO () runTestCfg2 :: ChatConfig -> ChatConfig -> (HasCallStack => TestCC -> TestCC -> IO ()) -> FilePath -> IO ()
runTestCfg2 aliceCfg bobCfg runTest tmp = runTestCfg2 aliceCfg bobCfg runTest tmp =
withNewTestChatCfg tmp aliceCfg "alice" aliceProfile $ \alice -> withNewTestChatCfg tmp aliceCfg "alice" aliceProfile $ \alice ->
@ -571,6 +595,20 @@ checkActionDeletesFile file action = do
fileExistsAfter <- doesFileExist file fileExistsAfter <- doesFileExist file
fileExistsAfter `shouldBe` False fileExistsAfter `shouldBe` False
startFileTransferWithDest' :: HasCallStack => TestCC -> TestCC -> String -> String -> Maybe String -> IO ()
startFileTransferWithDest' cc1 cc2 fileName fileSize fileDest_ = do
name1 <- userName cc1
name2 <- userName cc2
cc1 #> ("/f @" <> name2 <> " ./tests/fixtures/" <> fileName)
cc1 <## "use /fc 1 to cancel sending"
cc2 <# (name1 <> "> sends file " <> fileName <> " (" <> fileSize <> ")")
cc2 <## "use /fr 1 [<dir>/ | <path>] to receive it"
cc2 ##> ("/fr 1" <> maybe "" (" " <>) fileDest_)
cc2 <## ("saving file 1 from " <> name1 <> " to " <> maybe id (</>) fileDest_ fileName)
concurrently_
(cc2 <## ("started receiving file 1 (" <> fileName <> ") from " <> name1))
(cc1 <## ("started sending file 1 (" <> fileName <> ") to " <> name2))
currentChatVRangeInfo :: String currentChatVRangeInfo :: String
currentChatVRangeInfo = currentChatVRangeInfo =
"peer chat protocol version range: " <> vRangeStr supportedChatVRange "peer chat protocol version range: " <> vRangeStr supportedChatVRange

View File

@ -13,7 +13,7 @@ import qualified Data.ByteString as B
import qualified Data.ByteString.Lazy.Char8 as LB import qualified Data.ByteString.Lazy.Char8 as LB
import qualified Data.Map.Strict as M import qualified Data.Map.Strict as M
import Simplex.Chat.Archive (archiveFilesFolder) import Simplex.Chat.Archive (archiveFilesFolder)
import Simplex.Chat.Controller (versionNumber) import Simplex.Chat.Controller (ChatConfig (..), XFTPFileConfig (..), versionNumber)
import qualified Simplex.Chat.Controller as Controller import qualified Simplex.Chat.Controller as Controller
import Simplex.Chat.Mobile.File import Simplex.Chat.Mobile.File
import Simplex.Chat.Remote.Types import Simplex.Chat.Remote.Types
@ -194,7 +194,7 @@ remoteMessageTest = testChat3 aliceProfile aliceDesktopProfile bobProfile $ \mob
remoteStoreFileTest :: HasCallStack => FilePath -> IO () remoteStoreFileTest :: HasCallStack => FilePath -> IO ()
remoteStoreFileTest = remoteStoreFileTest =
testChat3 aliceProfile aliceDesktopProfile bobProfile $ \mobile desktop bob -> testChatCfg3 cfg aliceProfile aliceDesktopProfile bobProfile $ \mobile desktop bob ->
withXFTPServer $ do withXFTPServer $ do
let mobileFiles = "./tests/tmp/mobile_files" let mobileFiles = "./tests/tmp/mobile_files"
mobile ##> ("/_files_folder " <> mobileFiles) mobile ##> ("/_files_folder " <> mobileFiles)
@ -317,13 +317,15 @@ remoteStoreFileTest =
stopMobile mobile desktop stopMobile mobile desktop
where where
cfg = testCfg {xftpFileConfig = Just $ XFTPFileConfig {minFileSize = 0}, tempDir = Just "./tests/tmp/tmp"}
hostError cc err = do hostError cc err = do
r <- getTermLine cc r <- getTermLine cc
r `shouldStartWith` "remote host 1 error" r `shouldStartWith` "remote host 1 error"
r `shouldContain` err r `shouldContain` err
remoteCLIFileTest :: HasCallStack => FilePath -> IO () remoteCLIFileTest :: HasCallStack => FilePath -> IO ()
remoteCLIFileTest = testChat3 aliceProfile aliceDesktopProfile bobProfile $ \mobile desktop bob -> withXFTPServer $ do remoteCLIFileTest = testChatCfg3 cfg aliceProfile aliceDesktopProfile bobProfile $ \mobile desktop bob -> withXFTPServer $ do
createDirectoryIfMissing True "./tests/tmp/tmp/"
let mobileFiles = "./tests/tmp/mobile_files" let mobileFiles = "./tests/tmp/mobile_files"
mobile ##> ("/_files_folder " <> mobileFiles) mobile ##> ("/_files_folder " <> mobileFiles)
mobile <## "ok" mobile <## "ok"
@ -390,6 +392,8 @@ remoteCLIFileTest = testChat3 aliceProfile aliceDesktopProfile bobProfile $ \mob
B.readFile (bobFiles </> "test.jpg") `shouldReturn` src' B.readFile (bobFiles </> "test.jpg") `shouldReturn` src'
stopMobile mobile desktop stopMobile mobile desktop
where
cfg = testCfg {xftpFileConfig = Just $ XFTPFileConfig {minFileSize = 0}, tempDir = Just "./tests/tmp/tmp"}
switchRemoteHostTest :: FilePath -> IO () switchRemoteHostTest :: FilePath -> IO ()
switchRemoteHostTest = testChat3 aliceProfile aliceDesktopProfile bobProfile $ \mobile desktop bob -> do switchRemoteHostTest = testChat3 aliceProfile aliceDesktopProfile bobProfile $ \mobile desktop bob -> do

View File

@ -105,7 +105,7 @@
"simplex-network-overlay-card-1-li-3": "Le P2P ne résout pas <a href='https://fr.wikipedia.org/wiki/Attaque_de_l%27homme_du_milieu'>l'attaque MITM</a> et la plupart des implémentations existantes n'utilisent pas de messages hors bande pour l'échange de clé initial. SimpleX utilise des messages hors bande ou, dans certains cas, des connexions sécurisées et approuvées préexistantes pour l'échange de clé initial .", "simplex-network-overlay-card-1-li-3": "Le P2P ne résout pas <a href='https://fr.wikipedia.org/wiki/Attaque_de_l%27homme_du_milieu'>l'attaque MITM</a> et la plupart des implémentations existantes n'utilisent pas de messages hors bande pour l'échange de clé initial. SimpleX utilise des messages hors bande ou, dans certains cas, des connexions sécurisées et approuvées préexistantes pour l'échange de clé initial .",
"simplex-network-overlay-card-1-li-4": "Les réseaux P2P peuvent être bloquées par certains fournisseurs Internet (comme <a href='https://fr.wikipedia.org/wiki/BitTorrent'>BitTorrent</a>). SimpleX est indépendant du transport - il peut fonctionner sur des protocoles Web standard, par exemple WebSockets.", "simplex-network-overlay-card-1-li-4": "Les réseaux P2P peuvent être bloquées par certains fournisseurs Internet (comme <a href='https://fr.wikipedia.org/wiki/BitTorrent'>BitTorrent</a>). SimpleX est indépendant du transport - il peut fonctionner sur des protocoles Web standard, par exemple WebSockets.",
"simplex-network-overlay-card-1-li-5": "Tous les réseaux P2P connus sont susceptibles d'être vulnérables à une <a href='https://fr.wikipedia.org/wiki/Attaque_Sybil'>attaque Sybil</a>, car chaque nœud peut être découvert et le réseau fonctionne comme un tout. Les mesures connues pour réduire la probabilité d'une attaque Sybil nécessitent soit un composant centralisé, soit des <a href='https://fr.wikipedia.org/wiki/Preuve_de_travail'>preuves de travail</a> coûteuses. Le réseau SimpleX ne permet pas de découvrir les serveurs, il est fragmenté et fonctionne comme de multiples sous-réseaux isolées, ce qui rend impossible les attaques à l'échelle du réseau.", "simplex-network-overlay-card-1-li-5": "Tous les réseaux P2P connus sont susceptibles d'être vulnérables à une <a href='https://fr.wikipedia.org/wiki/Attaque_Sybil'>attaque Sybil</a>, car chaque nœud peut être découvert et le réseau fonctionne comme un tout. Les mesures connues pour réduire la probabilité d'une attaque Sybil nécessitent soit un composant centralisé, soit des <a href='https://fr.wikipedia.org/wiki/Preuve_de_travail'>preuves de travail</a> coûteuses. Le réseau SimpleX ne permet pas de découvrir les serveurs, il est fragmenté et fonctionne comme de multiples sous-réseaux isolées, ce qui rend impossible les attaques à l'échelle du réseau.",
"simplex-network-overlay-card-1-li-6": "Les réseaux P2P sont susceptibles d'être vulnérables aux <a href='https://www.usenix.org/conference/woot15/workshop-program/presentation/p2p-file-sharing-hell-exploiting-bittorrent'>attaques DRDoS</a>, lorsque les clients peuvent rediffuser et amplifier le trafic, entraînant un déni de service à l'échelle du réseau. Les clients SimpleX relaient uniquement le trafic à partir d'une connexion connue et ne peuvent pas être utilisés par un attaquant pour amplifier le trafic sur l'ensemble du réseau.", "simplex-network-overlay-card-1-li-6": "Les réseaux P2P sont susceptibles d'être vulnérables aux <a href='https://www.usenix.org/conference/woot15/workshop-program/presentation/p2p- file-sharing-hell-exploiting-bittorrent'>attaques DRDoS</a>, lorsque les clients peuvent rediffuser et amplifier le trafic, entraînant un déni de service à l'échelle du réseau. Les clients SimpleX relaient uniquement le trafic à partir d'une connexion connue et ne peuvent pas être utilisés par un attaquant pour amplifier le trafic sur l'ensemble du réseau.",
"privacy-matters-overlay-card-1-p-1": "De nombreuses grandes entreprises utilisent les informations sur les personnes avec lesquelles vous êtes connecté pour estimer vos revenus, vous vendre des produits dont vous n'avez pas vraiment besoin et déterminer les prix.", "privacy-matters-overlay-card-1-p-1": "De nombreuses grandes entreprises utilisent les informations sur les personnes avec lesquelles vous êtes connecté pour estimer vos revenus, vous vendre des produits dont vous n'avez pas vraiment besoin et déterminer les prix.",
"privacy-matters-overlay-card-1-p-2": "Les vendeurs en ligne savent que les personnes à faible revenu sont plus susceptibles d'effectuer des achats urgents. Ils peuvent donc pratiquer des prix plus élevés ou supprimer des remises.", "privacy-matters-overlay-card-1-p-2": "Les vendeurs en ligne savent que les personnes à faible revenu sont plus susceptibles d'effectuer des achats urgents. Ils peuvent donc pratiquer des prix plus élevés ou supprimer des remises.",
"privacy-matters-overlay-card-1-p-3": "Certaines sociétés financières et d'assurance utilisent des graphiques sociaux pour déterminer les taux d'intérêt et les primes. Cela fait souvent payer plus les personnes à faible revenu - c'est connu sous le nom de <a href ='https://fairbydesign.com/povertypremium/' target='_blank'>'prime à la pauvreté'</a>.", "privacy-matters-overlay-card-1-p-3": "Certaines sociétés financières et d'assurance utilisent des graphiques sociaux pour déterminer les taux d'intérêt et les primes. Cela fait souvent payer plus les personnes à faible revenu - c'est connu sous le nom de <a href ='https://fairbydesign.com/povertypremium/' target='_blank'>'prime à la pauvreté'</a>.",

View File

@ -93,7 +93,7 @@
"docs-dropdown-1": "SimpleXプラットフォーム", "docs-dropdown-1": "SimpleXプラットフォーム",
"hero-overlay-card-1-p-5": "クライアント デバイスのみがユーザー プロファイル、連絡先、およびグループを保存します。 メッセージは 2 レイヤーのエンドツーエンド暗号化を使用して送信されます。", "hero-overlay-card-1-p-5": "クライアント デバイスのみがユーザー プロファイル、連絡先、およびグループを保存します。 メッセージは 2 レイヤーのエンドツーエンド暗号化を使用して送信されます。",
"simplex-chat-for-the-terminal": "ターミナル用 SimpleX チャット", "simplex-chat-for-the-terminal": "ターミナル用 SimpleX チャット",
"simplex-network-overlay-card-1-li-3": "P2P は <a href='https://en.wikipedia.org/wiki/Man-in-the-middle_attack'>MITM 攻撃</a> 問題を解決せず、既存の実装のほとんどは最初の鍵交換に帯域外メッセージを使用していません 。 SimpleX は、最初のキー交換に帯域外メッセージを使用するか、場合によっては既存の安全で信頼できる接続を使用します。", "simplex-network-overlay-card-1-li-3": "P2P は <a href='https://en.wikipedia.org/wiki/Man-in-the-middle_ Attack'>MITM 攻撃</a> 問題を解決せず、既存の実装のほとんどは最初の鍵交換に帯域外メッセージを使用していません 。 SimpleX は、最初のキー交換に帯域外メッセージを使用するか、場合によっては既存の安全で信頼できる接続を使用します。",
"the-instructions--source-code": "ソース コードからダウンロードまたはコンパイルする方法を説明します。", "the-instructions--source-code": "ソース コードからダウンロードまたはコンパイルする方法を説明します。",
"simplex-network-section-desc": "Simplex Chat は、P2P とフェデレーション ネットワークの利点を組み合わせて最高のプライバシーを提供します。", "simplex-network-section-desc": "Simplex Chat は、P2P とフェデレーション ネットワークの利点を組み合わせて最高のプライバシーを提供します。",
"privacy-matters-section-subheader": "メタデータのプライバシーを保護する &mdash; <span class='text-active-blue'>話す相手</span> &mdash; 以下のことからあなたを守ります:", "privacy-matters-section-subheader": "メタデータのプライバシーを保護する &mdash; <span class='text-active-blue'>話す相手</span> &mdash; 以下のことからあなたを守ります:",
@ -150,7 +150,7 @@
"privacy-matters-2-overlay-1-title": "プライバシーはあなたに力を与えます", "privacy-matters-2-overlay-1-title": "プライバシーはあなたに力を与えます",
"simplex-unique-overlay-card-2-p-2": "オプションのユーザー アドレスを使用しても、スパムの連絡先リクエストの送信に使用される可能性がありますが、接続を失うことなく変更または完全に削除できます。", "simplex-unique-overlay-card-2-p-2": "オプションのユーザー アドレスを使用しても、スパムの連絡先リクエストの送信に使用される可能性がありますが、接続を失うことなく変更または完全に削除できます。",
"simplex-unique-4-overlay-1-title": "完全に分散化されています &mdash; ユーザーは SimpleX ネットワークを所有します", "simplex-unique-4-overlay-1-title": "完全に分散化されています &mdash; ユーザーは SimpleX ネットワークを所有します",
"simplex-network-overlay-card-1-li-5": "すべての既知の P2P ネットワークは、各ノードが検出可能であり、ネットワーク全体が動作するため、<a href='https://en.wikipedia.org/wiki/Sybil_attack'>Sybil 攻撃</a>に対して脆弱である可能性があります。 この問題を軽減する既知の対策には、一元化されたコンポーネントか、高価な<a href='https://en.wikipedia.org/wiki/Proof_of_work'>作業証明</a>が必要です。 SimpleX ネットワークにはサーバーの検出機能がなく、断片化されており、複数の分離されたサブネットワークとして動作するため、ネットワーク全体への攻撃は不可能です。", "simplex-network-overlay-card-1-li-5": "すべての既知の P2P ネットワークは、各ノードが検出可能であり、ネットワーク全体が動作するため、<a href='https://en.wikipedia.org/wiki/Sybil_question'>Sybil 攻撃</a>に対して脆弱である可能性があります。 この問題を軽減する既知の対策には、一元化されたコンポーネントか、高価な<a href='https://en.wikipedia.org/wiki/Proof_of_work'>作業証明</a>が必要です。 SimpleX ネットワークにはサーバーの検出機能がなく、断片化されており、複数の分離されたサブネットワークとして動作するため、ネットワーク全体への攻撃は不可能です。",
"simplex-private-2-title": "追加レイヤーの<br>サーバー暗号化", "simplex-private-2-title": "追加レイヤーの<br>サーバー暗号化",
"hero-overlay-card-1-p-4": "この設計により、ユーザーの情報の漏洩が防止されます&apos; アプリケーションレベルのメタデータ。 プライバシーをさらに向上させ、IP アドレスを保護するために、Tor 経由でメッセージング サーバーに接続できます。", "hero-overlay-card-1-p-4": "この設計により、ユーザーの情報の漏洩が防止されます&apos; アプリケーションレベルのメタデータ。 プライバシーをさらに向上させ、IP アドレスを保護するために、Tor 経由でメッセージング サーバーに接続できます。",
"f-droid-org-repo": "F-Droid.org リポジトリ", "f-droid-org-repo": "F-Droid.org リポジトリ",

View File

@ -67,10 +67,6 @@
"term": "Message padding", "term": "Message padding",
"definition": "Message padding" "definition": "Message padding"
}, },
{
"term": "Non-repudiation",
"definition": "Non-repudiation"
},
{ {
"term": "Onion routing", "term": "Onion routing",
"definition": "Onion routing" "definition": "Onion routing"
@ -107,10 +103,6 @@
"term": "Recovery from compromise", "term": "Recovery from compromise",
"definition": "Post-compromise security" "definition": "Post-compromise security"
}, },
{
"term": "Repudiation",
"definition": "Repudiation"
},
{ {
"term": "User identity", "term": "User identity",
"definition": "User identity" "definition": "User identity"

View File

@ -3,7 +3,7 @@
<p><a href="https://www.privacyguides.org/real-time-communication/#simplex-chat" target="_blank">Privacy Guides</a> recommendations.</p> <p><a href="https://www.privacyguides.org/real-time-communication/#simplex-chat" target="_blank">Privacy Guides</a> recommendations.</p>
<p><a href="https://www.kuketz-blog.de/simplex-eindruecke-vom-messenger-ohne-identifier/" target="_blank">Review by Mike Kuketz</a>.</p> <p><a href="https://www.kuketz-blog.de/simplex-eindruecke-vom-messenger-ohne-identifier/" target="_blank">Review by Mike Kuketz</a>.</p>
<p><a href="https://www.messenger-matrix.de" target="_blank">The messenger matrix</a>.</p> <p><a href="https://www.messenger-matrix.de" target="_blank">The messenger matrix</a>.</p>
<p class="mb-[12px]"><a href="https://supernovas.space/detailed_reviews.html#simplex" target="_blank">Supernova review</a> and <a href="https://supernovas.space/messengers.html" target="_blank">messenger ratings</a>.</p> <p class="mb-[12px]"><a href="https://supernova.tilde.team/detailed_reviews.html#simplex" target="_blank">Supernova review</a> and <a href="https://supernova.tilde.team/messengers.html" target="_blank">messenger ratings</a>.</p>
<p>v4.3 is released:</p> <p>v4.3 is released:</p>