Compare commits

..

61 Commits

Author SHA1 Message Date
JRoberts
5a2dd7b4bc 4.2.1 2022-11-12 16:01:27 +04:00
JRoberts
1a4d2b6de6 android: version 4.2.1 (68) 2022-11-12 15:59:27 +04:00
JRoberts
e4b46a45d3 android: restore Develop section divider 2022-11-12 15:45:04 +04:00
JRoberts
d61a7fb4d8 ios: version 4.2.1 (91) 2022-11-12 15:07:35 +04:00
JRoberts
bddb37593c mobile: 4.2.1 German translations (#1354) 2022-11-12 14:37:04 +04:00
JRoberts
8b794b2285 core: fix group link tests sporadically failing due to non deterministic events order (#1353) 2022-11-12 14:13:34 +04:00
Evgeny Poberezkin
8d0ec01a9b site: shorten anchor links 2022-11-12 09:49:07 +00:00
JRoberts
d85aa655cb mobile: fix translation 2022-11-12 11:54:28 +04:00
JRoberts
ba0cffb511 android: catch DecodeException (#1351)
* Catched drawable's DecodeException

(cherry picked from commit 90ed170f61)

* missing import

* texts

Co-authored-by: Avently <7953703+avently@users.noreply.github.com>
2022-11-12 11:44:20 +04:00
JRoberts
d029ce9817 ios: version 4.2.1 (90) 2022-11-11 21:48:52 +04:00
JRoberts
678f4f5e87 android: prevent exception on concurrent attempts to add members to group (#1348)
* android: Random crashes fix

(cherry picked from commit 0f7789c411)

* Better way of disabling members adding ato a group

(cherry picked from commit 96ca7f0d85)

* check outside

Co-authored-by: Avently <7953703+avently@users.noreply.github.com>
2022-11-11 21:08:02 +04:00
JRoberts
b780a41272 core: client that joins via group link to probe contacts, not host (#1343) 2022-11-11 18:34:32 +04:00
Evgeny Poberezkin
1caaca83cb blog: update group link 2022-11-11 14:26:51 +00:00
Stanislav Dmitrenko
c8b2bcb064 android: Fix of StackoverflowError (#1322)
* android: Fix of StackoverflowError

* Break

* Comment

* Update apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt

* Revert "Update apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt"

This reverts commit ea8015e01d.

Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
2022-11-11 13:33:45 +00:00
Stanislav Dmitrenko
adfe20b54c android: Support info (#1347)
* android: Support info

* Dirrectly to Play Store

* Different icon

* text

Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
2022-11-11 10:50:49 +00:00
JRoberts
b9d625da18 ios: support (#1346)
* ios: update settings

* translation

* redundant item

* fix stopped chat buttons

* corrections

Co-authored-by: JRoberts <8711996+jr-simplex@users.noreply.github.com>

* translations

Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
2022-11-11 08:30:10 +00:00
JRoberts
8631cf1471 android: remove local authentication error toasts, rename Retry -> Unlock (#1339) 2022-11-10 16:58:31 +04:00
Evgeny Poberezkin
0b6b8bd327 website: update video 2022-11-10 12:41:46 +00:00
JRoberts
e83ed30a49 ios: update library 2022-11-10 11:31:13 +04:00
Evgeny Poberezkin
29f919b3d6 blog: update post 2022-11-09 20:45:37 +00:00
JRoberts
2636f2ce1c mobile: increase default tcp timeouts (#1336) 2022-11-09 21:24:07 +04:00
JRoberts
f80f56de61 core: allow repeat connection via group link if group was deleted but contact with host is present (#1335) 2022-11-09 21:11:05 +04:00
JRoberts
941660625d website: phrasing, link (#1333) 2022-11-09 15:32:45 +04:00
Evgeny Poberezkin
ad1432e0ee core: make parsing independent of the order (#1332)
* core: make parsing independent of the order

* test

* fix

Co-authored-by: JRoberts <8711996+jr-simplex@users.noreply.github.com>
2022-11-09 14:48:24 +04:00
JRoberts
1cfbbd3115 core: update simplexmq (3.4.0) (#1328) 2022-11-09 14:13:39 +04:00
JRoberts
21ffe0ad49 core: repeated invite correctly updates role if changed (#1327) 2022-11-09 14:12:42 +04:00
JRoberts
54ad071655 website: fix typo 2022-11-09 10:25:09 +04:00
Evgeny Poberezkin
992c934fd1 add image 2022-11-08 20:42:00 +00:00
Evgeny Poberezkin
a6c6f1dbff website: update image 2022-11-08 20:29:00 +00:00
Evgeny Poberezkin
a652a14d58 site: fix date in blog index 2022-11-08 19:57:10 +00:00
Evgeny Poberezkin
9e77f05e58 website: fix images, update blog headline 2022-11-08 19:45:56 +00:00
Evgeny Poberezkin
355a3c429c site: fix share image in articles 2022-11-08 18:41:00 +00:00
Evgeny Poberezkin
a1ce3b9c69 blog: update headers 2022-11-08 17:54:42 +00:00
Evgeny Poberezkin
00af82cb19 add github button 2022-11-08 17:42:39 +00:00
Evgeny Poberezkin
68b6d9e966 site: update domain 2022-11-08 17:20:03 +00:00
Evgeny Poberezkin
75165cc70a blog: fix date 2022-11-08 17:01:40 +00:00
Evgeny Poberezkin
e5bf5092b1 readme: fix link 2022-11-08 17:00:07 +00:00
Evgeny Poberezkin
e5a4cca5e0 docs: update readme, blog 2022-11-08 16:59:14 +00:00
Evgeny Poberezkin
41f4f11155 website: fix popups and blog bullets 2022-11-08 16:43:04 +00:00
Evgeny Poberezkin
99bfd446d1 ci: remove website branch from web.yml 2022-11-08 15:36:53 +00:00
Evgeny Poberezkin
dd740e82cf a quick blog fix (#1324)
Co-authored-by: M Sarmad Qadeer <MSarmadQadeer@gmail.com>
2022-11-08 15:23:36 +00:00
Evgeny Poberezkin
5fabeff1fa docs: update readme (#1318)
* docs: update readme

* update roadmap

* typo

Co-authored-by: JRoberts <8711996+jr-simplex@users.noreply.github.com>

* update doc

* update readme

Co-authored-by: JRoberts <8711996+jr-simplex@users.noreply.github.com>
2022-11-08 13:20:23 +00:00
Evgeny Poberezkin
324730f8ae blog: readme, index (#1321)
* blog: readme, index

* add blog preview to the site
2022-11-08 13:19:32 +00:00
Evgeny Poberezkin
ed1faff500 blog: security audit, the new website, v4.2 release (#1315)
* blog: security audit, the new website, v4.2 release

* update text

* correction

* link to report

* more text (#1317)

* more text

* more text

* readme

* more text

* add logo

* image

* more images

* fix image link

* image size

* more images

* update images

* correction

Co-authored-by: JRoberts <8711996+jr-simplex@users.noreply.github.com>

* correction

Co-authored-by: JRoberts <8711996+jr-simplex@users.noreply.github.com>

* corrections

Co-authored-by: JRoberts <8711996+jr-simplex@users.noreply.github.com>

* correction

Co-authored-by: JRoberts <8711996+jr-simplex@users.noreply.github.com>

Co-authored-by: JRoberts <8711996+jr-simplex@users.noreply.github.com>

* remove note

Co-authored-by: JRoberts <8711996+jr-simplex@users.noreply.github.com>
2022-11-08 12:59:28 +00:00
Stanislav Dmitrenko
ce7d0ab8cf android: fix keyboard not closing between views, photo not saved on camera rotation, repeat authentication on opening chat via notification, other minor issues (#1314)
* android: User's problems fix

* Hiding keyboard, state preserving while rotating camera orientation

* Allows to select image from camera multiple times without crashing

* Images sending after orientation change

* Do not ask again about auth when switching between dialogs
2022-11-08 15:58:54 +04:00
Evgeny Poberezkin
75dccf95c4 website: typo 2022-11-08 11:45:21 +00:00
Evgeny Poberezkin
fa5a70cd19 new website (#1307)
* nav in process

* edited web.yml

* navbar issue fixed

* added theme switcher

* added privacy matters section

* added features section

* updated nav padding

* added network section

* improved sidebar dark mode colors

* added footer

* simplex private section added

* added some improvements

* nav issue fixed

* simplex unique section added

* a small fix

* added overlay & data to some sections

* added overlay to simplex unique section
added some improvements to other sections too

* added a small fix

* updated CNAME

* markdown files for why simplex is unique

* Revert "markdown files for why simplex is unique"

This reverts commit ef728218f7.

* added hero section

* added comparison and simplex explained section

* added blogs page

* added articles page

* a small fix in hero section

* added contact page

* updated contact

* created files for overlay content

* a light update

* hero animation

* working on hero

* added responsiveness for mobile

* a quick fix

* added responsiveness to tablet screen

* added responsiveness for desktop screen on hero section

* switch theme of hero

* nav color update

* set comparisons sections

* switch theme of comparisons section

* added responsiveness in simplex explained section

* add logic to simplex explained

* added theme switcher to simplex explained

* manage join simplex section

* update what makes simplex private

* a quick update

* add improvements

* a bit update

* add improvements

* texts for why privacy matters section

* update headers

* texts for "why unique" and "features" sections

* EOLs

* update swipers

* update & add transitions to simplex unique section

* updated overlays

* increase the size of cross on overlays

* add overlays to hero

* website: texts for "private" and "explained" sections (#5)

* website: texts for "private" section

* texts for simplex explained

* blog previews and images (#6)

* blog previews and images

* text for dark mode

* add link style

* add overlay to -> unlike p2p networks

* add picture with blue arrows to simplex explained

* update blog list layout

* remove extra css

* bigger navigation circles & center positions

* make bullets (dots) bigger

* make private scroll thicker

* update hero & footer mobile download btns

* fix dark mode animation files (#7)

* improved contrast for light animation in hero section (#8)

* remove old animation

* Made Hero Pixel Perfect to Desktop

* texts in hero section overlays (#10)

* texts in hero section overlays

* replace hero video

* eol

* update footer links (#11)

* update footer links

* eol

* texts, links, fix layout (#12)

* mailchimp form (#13)

* site meta tags (#14)

* site meta tags

* update blog og:url

* amend texts

* font

* update text

* contact page

* Making things Polished in Hero (#15)

* Made Video Responsive on Tablet

* Fixed the issues

* remove extra files for home & contact page

* update invitation

* refactoring

* fix nav for dark

* quick fix

* update blog list layout

* refactoring

* disable inactive nav circles

* contact page

* fix mobile

* detect platform & show btns according to it

* contact & invitation page setting

* complete contact/invitation page

* create variables for download btns

* fixes for hero - for tablet & mobile

* update hero layout

* update footer layout

* increase the size of logo in navbar

* updated nav & footer logos

* add links to join simplex section

* text for p2p networks section

* text on contact page about link

* add touchstart handler to close popup

* update APK links

* update CNAME

Co-authored-by: M Sarmad Qadeer <MSarmadQadeer@gmail.com>
Co-authored-by: Ojas Shukla <54703305+whizzbbig@users.noreply.github.com>
2022-11-08 11:04:02 +00:00
Evgeny Poberezkin
dd9e94eefd site: move APK url (#1320) 2022-11-08 10:28:40 +00:00
JRoberts
0f65a001c8 ios: fix current conversation not opening after authentication (#1319) 2022-11-08 12:39:41 +04:00
JRoberts
f3e59aa3c3 ios: dismiss re-opened contact connection view upon connection (#1316) 2022-11-07 21:05:59 +04:00
JRoberts
655041c657 ios: fix changed member role resetting in view (#1312) 2022-11-07 20:44:04 +04:00
Stanislav Dmitrenko
4ca118666a android: Connect via group link alert (#1313)
* android: Connect via group link alert

* Rename

* Replace first

* rename CRData into CReqClientData to match haskell type

* Alert

* Shorter

* Strings

Co-authored-by: JRoberts <8711996+jr-simplex@users.noreply.github.com>
2022-11-07 20:28:11 +04:00
Stanislav Dmitrenko
365f92e958 android: Alert about deleted contact and errors in chat item (#1289) 2022-11-07 19:42:00 +04:00
Stanislav Dmitrenko
ddecd847e5 android: update available group actions on role change; connect via external link when app was closed; other fixes (#1311)
* android: Fixes for tests

* console item bottom padding

Co-authored-by: JRoberts <8711996+jr-simplex@users.noreply.github.com>
2022-11-07 16:46:15 +04:00
JRoberts
18677cec63 mobile: repeat group invitations don't duplicate chat preview (#1310) 2022-11-07 12:08:37 +04:00
Evgeny Poberezkin
4e8dcab020 update readme 2022-11-06 20:48:58 +00:00
Evgeny Poberezkin
eb0f78bd80 blog: reserve permalink for 4.2 release 2022-11-06 15:27:59 +00:00
Evgeny Poberezkin
cf1bd0d467 mobile: version 4.2 (ios: 89, android: 67) 2022-11-06 15:10:15 +00:00
Evgeny Poberezkin
00f712dc59 ios: fix group role (#1308) 2022-11-06 14:20:15 +00:00
mlanp
0a27f8834d android / iOS: german translations for 4.2 (#1306)
* android/iOS: added and fixed german translations for v4.2

* ios localizations

Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
2022-11-06 14:09:44 +00:00
Stanislav Dmitrenko
f8678af261 android: Some small fixes (#1305) 2022-11-06 13:30:10 +00:00
535 changed files with 6819 additions and 1951 deletions

View File

@@ -1,22 +1,26 @@
# SimpleX Chat Terms & Privacy Policy
SimpleX Chat is the first chat platform that is 100% private by design - 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 do not have access to your connections graph.
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.
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).
## Privacy Policy
SimpleX Chat Ltd. ("SimpleX Chat") uses the best industry practices for security and end-to-end encryption to provide secure end-to-end encrypted messaging via private connections. SimpleX Chat is built on top of SimpleX messaging and application platform that uses a new message routing protocol that allows establishing private connection 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 security audit was performed 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.html).
### Information you provide
We do not store user profiles. The profile you create in the app is local to your device. 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.
Messages. SimpleX Chat cannot decrypt or otherwise access the content or size of your messages (each message is padded to a fixed size of 16kb). SimpleX Chat temporarily stores end-to-end encrypted messages on its servers for delivery to the devices that are temporarily offline. Your message history is stored only on your own devices.
Messages. SimpleX Chat cannot decrypt or otherwise access the content or even size of your messages (each message is padded to a fixed size of 16kb). SimpleX Chat temporarily stores end-to-end encrypted messages on its servers for delivery to the devices that are offline, these messages are permanently removed as soon as they are delivered. Your message history is stored only on your own devices.
Connections with other users. When you create a connection with another user, two messaging queues are created on our servers (we use separate queues for direct and response messages, that can be on two different servers), or on the servers that you configured in the app, in case it allows such configuration. At the time of updating this document only our terminal app allows configuring the servers, our mobile apps will allow such configuration in the near future. Our servers do not store information about which queues are linked to your profile on the device, and they do not have any information in common that allow us to establish that these queues are related to your device or your profile - the access to each queue is authorized by a set of unique encryption keys, different for each queue, and separate for sender and recipient of the messages that are transmitted through the queue.
Connections with other users. When you create a connection with another user, two messaging queues (you can think about them as about mailboxes) are created on our servers, or on 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). 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 a set of anonymous unique cryptographic keys, different for each queue, and separate for sender and recipient of the messages. The exception to that is when you choose to use instant push notifications in our iOS app, because the design of push notifications requires storing the device token on notification server, and the server can observe how many messaging queues your device uses, and approximate how many messages are sent to each queue. It does not allow though to determine the actual addresses of these queues, as a separate address is used to subscibe to the notifications (unless notification and messaging servers exchange information), and 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. It also does not allow to see message content or sizes, as the actual messages are not sent via the notification service, 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 see 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).
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.
User Support. If you contact SimpleX Chat any personal data you may share with us is kept only for the purposes of researching the issue and contacting you about your case. We recommend contacting support via chat, when it is possible.
User Support. If you contact SimpleX Chat any personal data you may share with us is kept only for the purposes of researching the issue and contacting you about your case. We recommend contacting support [via chat](https://simplex.chat/contact#/?v=1&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2FK1rslx-m5bpXVIdMZg9NLUZ_8JBm8xTt%23%2F%3Fv%3D1%26dh%3DMCowBQYDK2VuAyEALDeVe-sG8mRY22LsXlPgiwTNs9dbiLrNuA7f3ZMAJ2w%253D%26srv%3Dbylepyau3ty4czmn77q4fglvperknl4bi2eb2fdy2bh4jxtf32kf73yd.onion), when it is possible.
### Information we may share
@@ -31,6 +35,8 @@ The cases when SimpleX Chat may need to share the data we temporarily store on t
- To detect, prevent, or otherwise address fraud, security, or technical issues.
- To protect against harm to the rights, property, or safety of SimpleX Chat, our users, or the public as required or permitted by law.
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
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.
@@ -47,7 +53,7 @@ You accept to our Terms of Service ("Terms") by installing or using any of our a
**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 do 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 cyphertext 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.
**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 users - 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.
**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.
**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.
@@ -87,4 +93,4 @@ You accept to our Terms of Service ("Terms") by installing or using any of our a
**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.
Updated March 1, 2022
Updated November 8, 2022

View File

@@ -16,15 +16,15 @@
&nbsp;
[<img src="https://github.com/simplex-chat/.github/blob/master/profile/images/testflight.png" alt="iOS TestFlight" height="41">](https://testflight.apple.com/join/DWuT2LQu)
&nbsp;
[<img src="https://github.com/simplex-chat/.github/blob/master/profile/images/apk_icon.png" alt="APK" height="41">](https://github.com/simplex-chat/website/raw/master/simplex.apk)
[<img src="https://github.com/simplex-chat/.github/blob/master/profile/images/apk_icon.png" alt="APK" height="41">](https://github.com/simplex-chat/simplex-chat/releases/latest/download/simplex.apk)
- 🖲 Protects your messages and metadata - who you talk to and when.
- 🔐 Double ratchet end-to-end encryption, with additional encryption layer.
- 📱 Mobile apps for Android ([Google Play](https://play.google.com/store/apps/details?id=chat.simplex.app), [APK](https://github.com/simplex-chat/website/raw/master/simplex.apk)) and [iOS](https://apps.apple.com/us/app/simplex-chat/id1605771084).
- 📱 Mobile apps for Android ([Google Play](https://play.google.com/store/apps/details?id=chat.simplex.app), [APK](https://github.com/simplex-chat/simplex-chat/releases/latest/download/simplex.apk)) and [iOS](https://apps.apple.com/us/app/simplex-chat/id1605771084).
- 🚀 [TestFlight preview for iOS](https://testflight.apple.com/join/DWuT2LQu) with the new features 1-2 weeks earlier - **limited to 10,000 users**!
- 🖥 Available as a terminal (console) app / CLI on Linux, MacOS, Windows.
**NEW**: v4.0 is released - now local chat database is encrypted with passphrase! See [the release announcement](./blog/20220928-simplex-chat-v4-encrypted-database.md).
**NEW**: Security audit by [Trail of Bits](https://www.trailofbits.com/about), the [new website](https://simplex.chat) and v4.2 released! [See the announcement](./blog/20221108-simplex-chat-v4.2-security-audit-new-website.md)
## Contents
@@ -42,8 +42,9 @@
- [Privacy: technical details and limitations](#privacy-technical-details-and-limitations)
- [For developers](#for-developers)
- [Roadmap](#roadmap)
- [Help us pay for 3rd party security audit](#help-us-pay-for-3rd-party-security-audit)
- [Disclaimer, License](#disclaimer)
- [Contribute](#contribute)
- [Help us with donations](#help-us-with-donations)
- [Disclaimers, Security contact, License](#disclaimers)
## Why privacy matters
@@ -83,6 +84,8 @@ You can use SimpleX with your own servers and still communicate with people usin
Recent updates:
[Nov 08, 2022. Security audit by Trail of Bits, the new website and v4.2 released](./blog/20221108-simplex-chat-v4.2-security-audit-new-website.md)
[Sep 28, 2022. v4.0: encrypted local chat database and many other changes](./blog/20220928-simplex-chat-v4-encrypted-database.md)
[Sep 1, 2022. v3.2: incognito mode, support .onion server hostnames, setting contact names, changing color scheme, etc. Implementation audit is arranged for October!](./blog/20220901-simplex-chat-v3.2-incognito-mode.md)
@@ -149,7 +152,6 @@ We plan to add soon:
1. Message queue rotation. Currently the queues created between two users are used until the contact is deleted, providing a long-term pairwise identifiers of the conversation. We are planning to add queue rotation to make these identifiers termporary and rotate based on some schedule TBC (e.g., every X messages, or every X hours/days).
2. Local files encryption. Currently the images and files you send and receive are stored in the app unencrypted, you can delete them via `Settings / Database passphrase & export`.
3. Message "mixing" - adding latency to message delivery, to protect against traffic correlation by message time.
4. Independent implementation audit.
## For developers
@@ -175,20 +177,20 @@ If you are considering developing with SimpleX platform please get in touch for
- ✅ Manual chat history deletion.
- ✅ End-to-end encrypted WebRTC audio and video calls via the mobile apps.
- ✅ Privacy preserving instant notifications for iOS using Apple Push Notification service.
- ✅ Chat database export and import
- ✅ Chat database export and import.
- ✅ Chat groups in mobile apps.
- ✅ Connecting to messaging servers via Tor.
- ✅ Dual server addresses to access messaging servers as v3 hidden services.
- ✅ Chat server and TypeScript client SDK to develop chat interfaces, integrations and chat bots (ready for announcement).
- ✅ Incognito mode to share a new random name with each contact.
- ✅ Chat database encryption.
- 🏗 Automatic chat history deletion.
- Automatic chat history deletion.
- ✅ Links to join groups and improve groups stability.
- 🏗 SMP queue redundancy and rotation.
- 🏗 Links to join groups and improve groups stability.
- Feeds/broadcasts
- 🏗 Voice messages.
- Feeds/broadcasts.
- Disappearing messages, with mutual agreement.
- Voice messages
- Video messages
- Video messages.
- Web widgets for custom interactivity in the chats.
- Message delivery confirmation.
- Supporting the same profile on multiple devices.
@@ -198,19 +200,25 @@ If you are considering developing with SimpleX platform please get in touch for
- Channels server for large groups and broadcast channels.
- Media server to optimize sending large files to groups.
- Desktop client.
- Using the same profile on multiple devices.
## Help us pay for 3rd party security audit
## Contribute
I will get straight to the point: I ask you to support SimpleX Chat with donations.
We would love to have you join the development! You can contribute to SimpleX Chat with:
We are prioritizing users privacy and security - it would be impossible without your support we were lucky to have so far.
- developing features - please connect to us via chat so we can help you get started.
- writing a tutorial or recipes about hosting servers, chat bot automations, etc.
- translate UI to some language - we are currently setting up the UI to simplify it, please get in touch and let us know if you would be able to support and update the translations.
- translate website homepage - there is a lot of content we would like to share, it would help to bring the new users.
We are planning a 3rd party security audit for the app, and it would hugely help us if some part of this $20,000+ expense could be covered with donations.
## Help us with donations
Huge thank you to everybody who donated to SimpleX Chat!
We are prioritizing users privacy and security - it would be impossible without your support.
Our pledge to our users is that SimpleX protocols are and will remain open, and in public domain, - so anybody can build the future implementations of the clients and the servers. We are building SimpleX platform based on the same principles as email and web, but much more private and secure.
If you are already using SimpleX Chat, or plan to use it in the future when it has more features, please consider making a donation - it will help us to raise more funds. Donating any amount, even the price of the cup of coffee, would make a huge difference for us.
Your donations help us raise more funds any amount, even the price of the cup of coffee, would make a big difference for us.
It is possible to donate via:
@@ -218,6 +226,7 @@ It is possible to donate via:
- [OpenCollective](https://opencollective.com/simplex-chat) - it charges a commission, and also accepts donations in crypto-currencies.
- Monero wallet: 8568eeVjaJ1RQ65ZUn9PRQ8ENtqeX9VVhcCYYhnVLxhV4JtBqw42so2VEUDQZNkFfsH5sXCuV7FN8VhRQ21DkNibTZP57Qt
- Bitcoin wallet: 1bpefFkzuRoMY3ZuBbZNZxycbg7NYPYTG
- please let us know, via GitHub issue or chat, if you want to create a donation in some other cryptocurrency - we will add the address to the list.
Thank you,
@@ -225,11 +234,27 @@ Evgeny
SimpleX Chat founder
## Disclaimer
## Disclaimers
[SimpleX protocols and security model](https://github.com/simplex-chat/simplexmq/blob/master/protocol/overview-tjr.md) was reviewed and had many improvements in v1.0.0; we are currently arranging for the independent implementation audit.
[SimpleX protocols and security model](https://github.com/simplex-chat/simplexmq/blob/master/protocol/overview-tjr.md) was reviewed, and had many breaking changes and improvements in v1.0.0.
You are likely to discover some bugs - we would really appreciate if you use it and let us know anything that needs to be fixed or improved.
The security audit was performed 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.html).
SimpleX Chat is still a relatively early stage platform (the mobile apps were released in March 2022), so you may discover some bugs and missing features. We would really appreciate if you let us know anything that needs to be fixed or improved.
The default servers configured in the app are provided on the best effort basis. We are currently not guaranteeing any SLAs, although historically our servers had over 99.9% uptime each.
We have never provided or have been requested 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 be following due legal process.
We do not log IP addresses of the users and we do not perform any traffic correlation on our servers. If transport level security is critical you must use Tor or some other similar network to access messaging servers. We will be improving the client applications to reduce the opportunities for traffic correlation.
Please read more in [Terms & privacy policy](./PRIVACY.md).
## Security contact
To report a security vulnerability, please send us email to chat@simplex.chat. We will coordinate the fix and disclosure. Please do NOT report security vulnerabilities via GitHub issues.
Please treat any findings of possible traffic correlation attacks allowing to correlate two different conversations to the same user, other than covered in [the threat model](https://github.com/simplex-chat/simplexmq/blob/stable/protocol/overview-tjr.md#threat-model), as security vulnerabilities, and follow this disclosure process.
## License
@@ -243,4 +268,4 @@ You are likely to discover some bugs - we would really appreciate if you use it
&nbsp;
[<img src="https://github.com/simplex-chat/.github/blob/master/profile/images/testflight.png" alt="iOS TestFlight" height="41">](https://testflight.apple.com/join/DWuT2LQu)
&nbsp;
[<img src="https://github.com/simplex-chat/.github/blob/master/profile/images/apk_icon.png" alt="APK" height="41">](https://github.com/simplex-chat/website/raw/master/simplex.apk)
[<img src="https://github.com/simplex-chat/.github/blob/master/profile/images/apk_icon.png" alt="APK" height="41">](https://github.com/simplex-chat/simplex-chat/releases/latest/download/simplex.apk)

View File

@@ -11,8 +11,8 @@ android {
applicationId "chat.simplex.app"
minSdk 29
targetSdk 32
versionCode 66
versionName "4.2-beta.3"
versionCode 68
versionName "4.2.1"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
ndk {

View File

@@ -14,7 +14,7 @@ import androidx.compose.foundation.layout.*
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Surface
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Replay
import androidx.compose.material.icons.outlined.Lock
import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Alignment
@@ -33,8 +33,7 @@ import chat.simplex.app.views.chat.ChatView
import chat.simplex.app.views.chatlist.*
import chat.simplex.app.views.database.DatabaseErrorView
import chat.simplex.app.views.helpers.*
import chat.simplex.app.views.newchat.connectViaUri
import chat.simplex.app.views.newchat.withUriAction
import chat.simplex.app.views.newchat.*
import chat.simplex.app.views.onboarding.*
import kotlinx.coroutines.delay
@@ -65,6 +64,7 @@ class MainActivity: FragmentActivity() {
// Only needed to be processed on first creation of activity
if (savedInstanceState == null) {
processNotificationIntent(intent, m)
processIntent(intent, m)
processExternalIntent(intent, m)
}
setContent {
@@ -103,6 +103,16 @@ class MainActivity: FragmentActivity() {
}
}
override fun onPause() {
super.onPause()
/**
* When new activity is created after a click on notification, the old one receives onPause before
* recreation but receives onStop after recreation. So using both (onPause and onStop) to prevent
* unwanted multiple auth dialogs from [runAuthenticate]
* */
enteredBackground.value = elapsedRealtime()
}
override fun onStop() {
super.onStop()
enteredBackground.value = elapsedRealtime()
@@ -134,17 +144,10 @@ class MainActivity: FragmentActivity() {
this@MainActivity,
completed = { laResult ->
when (laResult) {
LAResult.Success -> {
LAResult.Success ->
userAuthorized.value = true
}
is LAResult.Error -> {
is LAResult.Error, LAResult.Failed ->
laFailed.value = true
laErrorToast(applicationContext, laResult.errString)
}
LAResult.Failed -> {
laFailed.value = true
laFailedToast(applicationContext)
}
LAResult.Unavailable -> {
userAuthorized.value = true
m.performLA.value = false
@@ -180,15 +183,9 @@ class MainActivity: FragmentActivity() {
prefPerformLA.set(true)
laTurnedOnAlert()
}
is LAResult.Error -> {
is LAResult.Error, LAResult.Failed -> {
m.performLA.value = false
prefPerformLA.set(false)
laErrorToast(applicationContext, laResult.errString)
}
LAResult.Failed -> {
m.performLA.value = false
prefPerformLA.set(false)
laFailedToast(applicationContext)
}
LAResult.Unavailable -> {
m.performLA.value = false
@@ -213,15 +210,9 @@ class MainActivity: FragmentActivity() {
m.performLA.value = false
prefPerformLA.set(false)
}
is LAResult.Error -> {
is LAResult.Error, LAResult.Failed -> {
m.performLA.value = true
prefPerformLA.set(true)
laErrorToast(applicationContext, laResult.errString)
}
LAResult.Failed -> {
m.performLA.value = true
prefPerformLA.set(true)
laFailedToast(applicationContext)
}
LAResult.Unavailable -> {
m.performLA.value = false
@@ -288,14 +279,14 @@ fun MainPage(
}
@Composable
fun retryAuthView() {
fun authView() {
Box(
Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
SimpleButton(
stringResource(R.string.auth_retry),
icon = Icons.Outlined.Replay,
stringResource(R.string.auth_unlock),
icon = Icons.Outlined.Lock,
click = {
laFailed.value = false
runAuthenticate()
@@ -316,7 +307,7 @@ fun MainPage(
onboarding == null || userCreated == null -> SplashView()
!chatsAccessAuthorized -> {
if (chatModel.controller.appPrefs.performLA.get() && laFailed.value) {
retryAuthView()
authView()
} else {
SplashView()
}
@@ -425,23 +416,23 @@ fun connectIfOpenedViaUri(uri: Uri, chatModel: ChatModel) {
// TODO open from chat list view
chatModel.appOpenUrl.value = uri
} else {
withUriAction(uri) { action ->
val title = when (action) {
"contact" -> generalGetString(R.string.connect_via_contact_link)
"invitation" -> generalGetString(R.string.connect_via_invitation_link)
else -> {
Log.e(TAG, "URI has unexpected action. Alert shown.")
action
}
withUriAction(uri) { linkType ->
val title = when (linkType) {
ConnectionLinkType.CONTACT -> generalGetString(R.string.connect_via_contact_link)
ConnectionLinkType.INVITATION -> generalGetString(R.string.connect_via_invitation_link)
ConnectionLinkType.GROUP -> generalGetString(R.string.connect_via_group_link)
}
AlertManager.shared.showAlertMsg(
title = title,
text = generalGetString(R.string.profile_will_be_sent_to_contact_sending_link),
text = if (linkType == ConnectionLinkType.GROUP)
generalGetString(R.string.you_will_join_group)
else
generalGetString(R.string.profile_will_be_sent_to_contact_sending_link),
confirmText = generalGetString(R.string.connect_via_link_verb),
onConfirm = {
withApi {
Log.d(TAG, "connectIfOpenedViaUri: connecting")
connectViaUri(chatModel, action, uri)
connectViaUri(chatModel, linkType, uri)
}
}
)

View File

@@ -307,6 +307,11 @@ class ChatModel(val controller: ChatController) {
}
fun upsertGroupMember(groupInfo: GroupInfo, member: GroupMember): Boolean {
// user member was updated
if (groupInfo.membership.groupMemberId == member.groupMemberId) {
updateGroup(groupInfo)
return false
}
// update current chat
return if (chatId.value == groupInfo.id) {
val memberIndex = groupMembers.indexOfFirst { it.id == member.id }
@@ -426,7 +431,7 @@ sealed class ChatInfo: SomeChat, NamedChat {
abstract val incognito: Boolean
@Serializable @SerialName("direct")
class Direct(val contact: Contact): ChatInfo() {
data class Direct(val contact: Contact): ChatInfo() {
override val chatType get() = ChatType.Direct
override val localDisplayName get() = contact.localDisplayName
override val id get() = contact.id
@@ -448,7 +453,7 @@ sealed class ChatInfo: SomeChat, NamedChat {
}
@Serializable @SerialName("group")
class Group(val groupInfo: GroupInfo): ChatInfo() {
data class Group(val groupInfo: GroupInfo): ChatInfo() {
override val chatType get() = ChatType.Group
override val localDisplayName get() = groupInfo.localDisplayName
override val id get() = groupInfo.id
@@ -700,7 +705,7 @@ class GroupProfile (
}
@Serializable
class GroupMember (
data class GroupMember (
val groupMemberId: Long,
val groupId: Long,
val memberId: String,

View File

@@ -244,6 +244,10 @@ open class ChatController(var ctrl: ChatCtrl?, val ntfManager: NtfManager, val a
chatModel.onboardingStage.value = OnboardingStage.OnboardingComplete
chatModel.controller.appPrefs.chatLastStart.set(Clock.System.now())
chatModel.chatRunning.value = true
chatModel.appOpenUrl.value?.let {
chatModel.appOpenUrl.value = null
connectIfOpenedViaUri(it, chatModel)
}
startReceiver()
Log.d(TAG, "startChat: started")
} else {
@@ -260,8 +264,20 @@ open class ChatController(var ctrl: ChatCtrl?, val ntfManager: NtfManager, val a
private fun startReceiver() {
Log.d(TAG, "ChatController startReceiver")
if (receiverStarted) return
thread(name="receiver") {
GlobalScope.launch { withContext(Dispatchers.IO) { recvMspLoop() } }
receiverStarted = true
CoroutineScope(Dispatchers.IO).launch {
while (true) {
/** Global [ctrl] can be null. It's needed for having the same [ChatModel] that already made in [ChatController] without the need
* to change it everywhere in code after changing a database.
* Since it can be changed in background thread, making this check to prevent NullPointerException */
val ctrl = ctrl
if (ctrl == null) {
receiverStarted = false
break
}
val msg = recvMsg(ctrl)
if (msg != null) processReceivedMsg(msg)
}
}
}
@@ -287,26 +303,18 @@ open class ChatController(var ctrl: ChatCtrl?, val ntfManager: NtfManager, val a
}
}
private suspend fun recvMsg(ctrl: ChatCtrl): CR? {
return withContext(Dispatchers.IO) {
val json = chatRecvMsgWait(ctrl, MESSAGE_TIMEOUT)
if (json == "") {
null
} else {
val r = APIResponse.decodeStr(json).resp
Log.d(TAG, "chatRecvMsg: ${r.responseType}")
if (r is CR.Response || r is CR.Invalid) Log.d(TAG, "chatRecvMsg json: $json")
r
}
private fun recvMsg(ctrl: ChatCtrl): CR? {
val json = chatRecvMsgWait(ctrl, MESSAGE_TIMEOUT)
return if (json == "") {
null
} else {
val r = APIResponse.decodeStr(json).resp
Log.d(TAG, "chatRecvMsg: ${r.responseType}")
if (r is CR.Response || r is CR.Invalid) Log.d(TAG, "chatRecvMsg json: $json")
r
}
}
private suspend fun recvMspLoop() {
val msg = recvMsg(ctrl ?: return)
if (msg != null) processReceivedMsg(msg)
recvMspLoop()
}
suspend fun apiGetActiveUser(): User? {
val r = sendCmd(CC.ShowActiveUser())
if (r is CR.ActiveUser) return r.user
@@ -1035,7 +1043,7 @@ open class ChatController(var ctrl: ChatCtrl?, val ntfManager: NtfManager, val a
}
}
is CR.ReceivedGroupInvitation -> {
chatModel.addChat(Chat(chatInfo = ChatInfo.Group(r.groupInfo), chatItems = listOf()))
chatModel.updateGroup(r.groupInfo) // update so that repeat group invitations are not duplicated
// TODO NtfManager.shared.notifyGroupInvitation
}
is CR.UserAcceptedGroupSent -> {
@@ -1343,15 +1351,9 @@ open class ChatController(var ctrl: ChatCtrl?, val ntfManager: NtfManager, val a
appPrefs.performLA.set(true)
laTurnedOnAlert()
}
is LAResult.Error -> {
is LAResult.Error, LAResult.Failed -> {
chatModel.performLA.value = false
appPrefs.performLA.set(false)
laErrorToast(appContext, laResult.errString)
}
LAResult.Failed -> {
chatModel.performLA.value = false
appPrefs.performLA.set(false)
laFailedToast(appContext)
}
LAResult.Unavailable -> {
chatModel.performLA.value = false
@@ -1699,8 +1701,8 @@ data class NetCfg(
val defaults: NetCfg =
NetCfg(
socksProxy = null,
tcpConnectTimeout = 7_500_000,
tcpTimeout = 5_000_000,
tcpConnectTimeout = 10_000_000,
tcpTimeout = 7_000_000,
tcpKeepAlive = KeepAliveOpts.defaults,
smpPingInterval = 600_000_000
)
@@ -1708,8 +1710,8 @@ data class NetCfg(
val proxyDefaults: NetCfg =
NetCfg(
socksProxy = ":9050",
tcpConnectTimeout = 15_000_000,
tcpTimeout = 10_000_000,
tcpConnectTimeout = 20_000_000,
tcpTimeout = 15_000_000,
tcpKeepAlive = KeepAliveOpts.defaults,
smpPingInterval = 600_000_000
)

View File

@@ -25,8 +25,7 @@ import androidx.compose.ui.unit.sp
import androidx.fragment.app.FragmentActivity
import chat.simplex.app.R
import chat.simplex.app.model.*
import chat.simplex.app.ui.theme.SimpleButton
import chat.simplex.app.ui.theme.SimpleXTheme
import chat.simplex.app.ui.theme.*
import chat.simplex.app.views.chat.*
import chat.simplex.app.views.helpers.*
import com.google.accompanist.insets.ProvideWindowInsets
@@ -164,7 +163,8 @@ fun TerminalLog(terminalItems: List<TerminalItem>) {
val reversedTerminalItems by remember { derivedStateOf { terminalItems.reversed() } }
LazyColumn(state = listState, reverseLayout = true) {
items(reversedTerminalItems) { item ->
Text("${item.date.toString().subSequence(11, 19)} ${item.label}",
Text(
"${item.date.toString().subSequence(11, 19)} ${item.label}",
style = TextStyle(fontFamily = FontFamily.Monospace, fontSize = 18.sp, color = MaterialTheme.colors.primary),
maxLines = 1,
overflow = TextOverflow.Ellipsis,
@@ -173,7 +173,7 @@ fun TerminalLog(terminalItems: List<TerminalItem>) {
.clickable {
ModalManager.shared.showModal {
SelectionContainer(modifier = Modifier.verticalScroll(rememberScrollState())) {
Text(item.details)
Text(item.details, modifier = Modifier.padding(horizontal = DEFAULT_PADDING).padding(bottom = DEFAULT_PADDING))
}
}
}.padding(horizontal = 8.dp, vertical = 4.dp)

View File

@@ -47,7 +47,6 @@ fun ChatInfoView(
customUserProfile: Profile?,
localAlias: String,
close: () -> Unit,
onChatUpdated: (Chat) -> Unit,
) {
BackHandler(onBack = close)
val chat = chatModel.chats.firstOrNull { it.id == chatModel.chatId.value }
@@ -61,7 +60,7 @@ fun ChatInfoView(
localAlias,
developerTools,
onLocalAliasChanged = {
setContactAlias(chat.chatInfo.apiId, it, chatModel, onChatUpdated)
setContactAlias(chat.chatInfo.apiId, it, chatModel)
},
deleteContact = { deleteContactDialog(chat.chatInfo, chatModel, close) },
clearChat = { clearChatDialog(chat.chatInfo, chatModel, close) },
@@ -350,10 +349,9 @@ fun DeleteContactButton(onClick: () -> Unit) {
)
}
private fun setContactAlias(contactApiId: Long, localAlias: String, chatModel: ChatModel, onChatUpdated: (Chat) -> Unit) = withApi {
private fun setContactAlias(contactApiId: Long, localAlias: String, chatModel: ChatModel) = withApi {
chatModel.controller.apiSetContactAlias(contactApiId, localAlias)?.let {
chatModel.updateContact(it)
onChatUpdated(chatModel.getChat(chatModel.chatId.value ?: return@withApi) ?: return@withApi)
}
}

View File

@@ -1,8 +1,10 @@
package chat.simplex.app.views.chat
import android.content.Context
import android.content.res.Configuration
import android.graphics.Bitmap
import android.net.Uri
import android.view.inputmethod.InputMethodManager
import androidx.activity.compose.BackHandler
import androidx.compose.foundation.*
import androidx.compose.foundation.gestures.*
@@ -65,22 +67,32 @@ fun ChatView(chatModel: ChatModel) {
LaunchedEffect(Unit) {
// snapshotFlow here is because it reacts much faster on changes in chatModel.chatId.value.
// With LaunchedEffect(chatModel.chatId.value) there is a noticeable delay before reconstruction of the view
snapshotFlow { chatModel.chatId.value }
.distinctUntilChanged()
.collect {
if (activeChat.value?.id != chatModel.chatId.value) {
activeChat.value = if (chatModel.chatId.value == null) {
null
} else {
launch {
snapshotFlow { chatModel.chatId.value }
.distinctUntilChanged()
.collect {
if (activeChat.value?.id != chatModel.chatId.value && chatModel.chatId.value != null) {
// Redisplay the whole hierarchy if the chat is different to make going from groups to direct chat working correctly
// Also for situation when chatId changes after clicking in notification, etc
chatModel.getChat(chatModel.chatId.value!!)
activeChat.value = chatModel.getChat(chatModel.chatId.value!!)
}
markUnreadChatAsRead(activeChat, chatModel)
}
markUnreadChatAsRead(activeChat, chatModel)
}
}
launch {
// .toList() is important for making observation working
snapshotFlow { chatModel.chats.toList() }
.distinctUntilChanged()
.collect { chats ->
chats.firstOrNull { chat -> chat.chatInfo.id == chatModel.chatId.value }.let {
// Only changed chatInfo is important thing. Other properties can be skipped for reducing recompositions
if (it?.chatInfo != activeChat.value?.chatInfo) {
activeChat.value = it
}}
}
}
}
val view = LocalView.current
if (activeChat.value == null || user == null) {
chatModel.chatId.value = null
} else {
@@ -114,16 +126,18 @@ fun ChatView(chatModel: ChatModel) {
searchText,
useLinkPreviews = useLinkPreviews,
chatModelIncognito = chatModel.incognito.value,
back = { chatModel.chatId.value = null },
back = {
hideKeyboard(view)
chatModel.chatId.value = null
},
info = {
hideKeyboard(view)
withApi {
val cInfo = chat.chatInfo
if (cInfo is ChatInfo.Direct) {
val contactInfo = chatModel.controller.apiContactInfo(cInfo.apiId)
ModalManager.shared.showModalCloseable(true) { close ->
ChatInfoView(chatModel, cInfo.contact, contactInfo?.first, contactInfo?.second, chat.chatInfo.localAlias, close) {
activeChat.value = it
}
ChatInfoView(chatModel, cInfo.contact, contactInfo?.first, contactInfo?.second, chat.chatInfo.localAlias, close)
}
} else if (cInfo is ChatInfo.Group) {
setGroupMembers(cInfo.groupInfo, chatModel)
@@ -134,6 +148,7 @@ fun ChatView(chatModel: ChatModel) {
}
},
showMemberInfo = { groupInfo: GroupInfo, member: GroupMember ->
hideKeyboard(view)
withApi {
val stats = chatModel.controller.apiGroupMemberInfo(groupInfo.groupId, member.groupMemberId)
ModalManager.shared.showModalCloseable(true) { close ->
@@ -177,6 +192,7 @@ fun ChatView(chatModel: ChatModel) {
}
},
acceptCall = { contact ->
hideKeyboard(view)
val invitation = chatModel.callInvitations.remove(contact.id)
if (invitation == null) {
AlertManager.shared.showAlertMsg("Call already ended!")
@@ -185,6 +201,7 @@ fun ChatView(chatModel: ChatModel) {
}
},
addMembers = { groupInfo ->
hideKeyboard(view)
withApi {
setGroupMembers(groupInfo, chatModel)
ModalManager.shared.showModalCloseable(true) { close ->

View File

@@ -2,11 +2,11 @@ package chat.simplex.app.views.chat
import ComposeFileView
import android.Manifest
import android.app.Activity
import android.content.*
import android.content.pm.PackageManager
import android.graphics.Bitmap
import android.graphics.ImageDecoder
import android.graphics.ImageDecoder.DecodeException
import android.graphics.drawable.AnimatedImageDrawable
import android.net.Uri
import android.provider.MediaStore
@@ -14,7 +14,6 @@ import android.util.Log
import android.widget.Toast
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContract
import androidx.annotation.CallSuper
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CircleShape
@@ -26,6 +25,7 @@ import androidx.compose.material.icons.filled.Edit
import androidx.compose.material.icons.outlined.Reply
import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.Saver
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
@@ -34,7 +34,6 @@ import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.core.content.ContextCompat
import androidx.core.content.FileProvider
import chat.simplex.app.*
import chat.simplex.app.R
import chat.simplex.app.model.*
@@ -44,7 +43,6 @@ import kotlinx.coroutines.delay
import kotlinx.coroutines.runBlocking
import kotlinx.serialization.Serializable
import kotlinx.serialization.decodeFromString
import java.io.File
@Serializable
sealed class ComposePreview {
@@ -69,7 +67,7 @@ data class ComposeState(
val inProgress: Boolean = false,
val useLinkPreviews: Boolean
) {
constructor(editingItem: ChatItem, useLinkPreviews: Boolean): this (
constructor(editingItem: ChatItem, useLinkPreviews: Boolean): this(
editingItem.content.text,
chatItemPreview(editingItem),
ComposeContextItem.EditingItem(editingItem),
@@ -137,53 +135,22 @@ fun ComposeView(
showChooseAttachment: () -> Unit
) {
val context = LocalContext.current
val linkUrl = remember { mutableStateOf<String?>(null) }
val prevLinkUrl = remember { mutableStateOf<String?>(null) }
val pendingLinkUrl = remember { mutableStateOf<String?>(null) }
val cancelledLinks = remember { mutableSetOf<String>() }
val linkUrl = rememberSaveable { mutableStateOf<String?>(null) }
val prevLinkUrl = rememberSaveable { mutableStateOf<String?>(null) }
val pendingLinkUrl = rememberSaveable { mutableStateOf<String?>(null) }
val cancelledLinks = rememberSaveable { mutableSetOf<String>() }
val useLinkPreviews = chatModel.controller.appPrefs.privacyLinkPreviews.get()
val smallFont = MaterialTheme.typography.body1.copy(color = MaterialTheme.colors.onBackground)
val textStyle = remember { mutableStateOf(smallFont) }
// attachments
val chosenContent = remember { mutableStateOf<List<UploadContent>>(emptyList()) }
val chosenFile = remember { mutableStateOf<Uri?>(null) }
val photoUri = remember { mutableStateOf<Uri?>(null) }
val photoTmpFile = remember { mutableStateOf<File?>(null) }
class ComposeTakePicturePreview: ActivityResultContract<Void?, Bitmap?>() {
@CallSuper
override fun createIntent(context: Context, input: Void?): Intent {
photoTmpFile.value = File.createTempFile("image", ".bmp", SimplexApp.context.filesDir)
photoUri.value = FileProvider.getUriForFile(context, "${BuildConfig.APPLICATION_ID}.provider", photoTmpFile.value!!)
return Intent(MediaStore.ACTION_IMAGE_CAPTURE)
.putExtra(MediaStore.EXTRA_OUTPUT, photoUri.value)
}
override fun getSynchronousResult(
context: Context,
input: Void?
): SynchronousResult<Bitmap?>? = null
override fun parseResult(resultCode: Int, intent: Intent?): Bitmap? {
val photoUriVal = photoUri.value
val photoTmpFileVal = photoTmpFile.value
return if (resultCode == Activity.RESULT_OK && photoUriVal != null && photoTmpFileVal != null) {
val source = ImageDecoder.createSource(SimplexApp.context.contentResolver, photoUriVal)
val bitmap = ImageDecoder.decodeBitmap(source)
photoTmpFileVal.delete()
bitmap
} else {
Log.e(TAG, "Getting image from camera cancelled or failed.")
photoTmpFile.value?.delete()
null
}
}
}
val cameraLauncher = rememberLauncherForActivityResult(contract = ComposeTakePicturePreview()) { bitmap: Bitmap? ->
if (bitmap != null) {
val chosenContent = rememberSaveable { mutableStateOf<List<UploadContent>>(emptyList()) }
val chosenFile = rememberSaveable { mutableStateOf<Uri?>(null) }
val cameraLauncher = rememberCameraLauncher { uri: Uri? ->
if (uri != null) {
val source = ImageDecoder.createSource(SimplexApp.context.contentResolver, uri)
val bitmap = ImageDecoder.decodeBitmap(source)
val imagePreview = resizeImageToStrSize(bitmap, maxDataSize = 14000)
chosenContent.value = listOf(UploadContent.SimpleImage(bitmap))
chosenContent.value = listOf(UploadContent.SimpleImage(uri))
composeState.value = composeState.value.copy(preview = ComposePreview.ImagePreview(listOf(imagePreview)))
}
}
@@ -199,8 +166,17 @@ fun ComposeView(
val imagesPreview = ArrayList<String>()
uris.forEach { uri ->
val source = ImageDecoder.createSource(context.contentResolver, uri)
val drawable = ImageDecoder.decodeDrawable(source)
var bitmap: Bitmap? = ImageDecoder.decodeBitmap(source)
val drawable = try {
ImageDecoder.decodeDrawable(source)
} catch (e: DecodeException) {
AlertManager.shared.showAlertMsg(
title = generalGetString(R.string.image_decoding_exception_title),
text = generalGetString(R.string.image_decoding_exception_desc)
)
Log.e(TAG, "Error while decoding drawable: ${e.stackTraceToString()}")
null
}
var bitmap: Bitmap? = if (drawable != null) ImageDecoder.decodeBitmap(source) else null
if (drawable is AnimatedImageDrawable) {
// It's a gif or webp
val fileSize = getFileSize(context, uri)
@@ -214,7 +190,7 @@ fun ComposeView(
)
}
} else {
if (bitmap != null) content.add(UploadContent.SimpleImage(bitmap))
content.add(UploadContent.SimpleImage(uri))
}
if (bitmap != null) {
imagesPreview.add(resizeImageToStrSize(bitmap, maxDataSize = 14000))
@@ -243,7 +219,7 @@ fun ComposeView(
}
}
}
val galleryLauncher = rememberLauncherForActivityResult(contract = PickFromGallery()) { processPickedImage(it, null) }
val galleryLauncher = rememberLauncherForActivityResult(contract = PickMultipleFromGallery()) { processPickedImage(it, null) }
val galleryLauncherFallback = rememberGetMultipleContentsLauncher { processPickedImage(it, null) }
val filesLauncher = rememberGetContentLauncher { processPickedFile(it, null) }
@@ -391,7 +367,7 @@ fun ComposeView(
is ComposePreview.ImagePreview -> {
chosenContent.value.forEachIndexed { index, it ->
val file = when (it) {
is UploadContent.SimpleImage -> saveImage(context, it.bitmap)
is UploadContent.SimpleImage -> saveImage(context, it.uri)
is UploadContent.AnimatedImage -> saveAnimImage(context, it.uri)
}
if (file != null) {
@@ -550,7 +526,16 @@ fun ComposeView(
}
}
class PickFromGallery: ActivityResultContract<Int, List<Uri>>() {
class PickFromGallery: ActivityResultContract<Int, Uri?>() {
override fun createIntent(context: Context, input: Int) =
Intent(Intent.ACTION_PICK, MediaStore.Images.Media.INTERNAL_CONTENT_URI).apply {
type = "image/*"
}
override fun parseResult(resultCode: Int, intent: Intent?): Uri? = intent?.data
}
class PickMultipleFromGallery: ActivityResultContract<Int, List<Uri>>() {
override fun createIntent(context: Context, input: Int) =
Intent(Intent.ACTION_PICK, MediaStore.Images.Media.INTERNAL_CONTENT_URI).apply {
putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true)

View File

@@ -14,7 +14,6 @@ import androidx.compose.material.icons.filled.CheckCircle
import androidx.compose.material.icons.filled.TheaterComedy
import androidx.compose.material.icons.outlined.*
import androidx.compose.runtime.*
import androidx.compose.runtime.snapshots.SnapshotStateList
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
@@ -35,14 +34,16 @@ import chat.simplex.app.views.usersettings.SettingsActionItem
fun AddGroupMembersView(groupInfo: GroupInfo, chatModel: ChatModel, close: () -> Unit) {
val selectedContacts = remember { mutableStateListOf<Long>() }
val selectedRole = remember { mutableStateOf(GroupMemberRole.Member) }
var allowModifyMembers by remember { mutableStateOf(true) }
BackHandler(onBack = close)
AddGroupMembersLayout(
groupInfo = groupInfo,
contactsToAdd = getContactsToAdd(chatModel),
selectedContacts = selectedContacts,
selectedRole = selectedRole,
allowModifyMembers = allowModifyMembers,
inviteMembers = {
allowModifyMembers = false
withApi {
for (contactId in selectedContacts) {
val member = chatModel.controller.apiAddMember(groupInfo.groupId, contactId, selectedRole.value)
@@ -79,8 +80,9 @@ fun getContactsToAdd(chatModel: ChatModel): List<Contact> {
fun AddGroupMembersLayout(
groupInfo: GroupInfo,
contactsToAdd: List<Contact>,
selectedContacts: SnapshotStateList<Long>,
selectedContacts: List<Long>,
selectedRole: MutableState<GroupMemberRole>,
allowModifyMembers: Boolean,
inviteMembers: () -> Unit,
clearSelection: () -> Unit,
addContact: (Long) -> Unit,
@@ -119,18 +121,18 @@ fun AddGroupMembersLayout(
} else {
SectionView {
SectionItemView {
RoleSelectionRow(groupInfo, selectedRole)
RoleSelectionRow(groupInfo, selectedRole, allowModifyMembers)
}
SectionDivider()
InviteMembersButton(inviteMembers, disabled = selectedContacts.isEmpty())
InviteMembersButton(inviteMembers, disabled = selectedContacts.isEmpty() || !allowModifyMembers)
}
SectionCustomFooter {
InviteSectionFooter(selectedContactsCount = selectedContacts.count(), clearSelection)
InviteSectionFooter(selectedContactsCount = selectedContacts.size, allowModifyMembers, clearSelection)
}
SectionSpacer()
SectionView {
ContactList(contacts = contactsToAdd, selectedContacts, groupInfo, addContact, removeContact)
ContactList(contacts = contactsToAdd, selectedContacts, groupInfo, allowModifyMembers, addContact, removeContact)
}
SectionSpacer()
}
@@ -138,7 +140,7 @@ fun AddGroupMembersLayout(
}
@Composable
private fun RoleSelectionRow(groupInfo: GroupInfo, selectedRole: MutableState<GroupMemberRole>) {
private fun RoleSelectionRow(groupInfo: GroupInfo, selectedRole: MutableState<GroupMemberRole>, enabled: Boolean) {
Row(
Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
@@ -150,7 +152,7 @@ private fun RoleSelectionRow(groupInfo: GroupInfo, selectedRole: MutableState<Gr
values,
selectedRole,
icon = null,
enabled = remember { mutableStateOf(true) },
enabled = rememberUpdatedState(enabled),
onSelected = { selectedRole.value = it }
)
}
@@ -169,7 +171,7 @@ fun InviteMembersButton(onClick: () -> Unit, disabled: Boolean) {
}
@Composable
fun InviteSectionFooter(selectedContactsCount: Int, clearSelection: () -> Unit) {
fun InviteSectionFooter(selectedContactsCount: Int, enabled: Boolean, clearSelection: () -> Unit) {
Row(
Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
@@ -182,11 +184,11 @@ fun InviteSectionFooter(selectedContactsCount: Int, clearSelection: () -> Unit)
fontSize = 12.sp
)
Box(
Modifier.clickable { clearSelection() }
Modifier.clickable { if (enabled) clearSelection() }
) {
Text(
stringResource(R.string.clear_contacts_selection_button),
color = MaterialTheme.colors.primary,
color = if (enabled) MaterialTheme.colors.primary else HighOrLowlight,
fontSize = 12.sp
)
}
@@ -203,8 +205,9 @@ fun InviteSectionFooter(selectedContactsCount: Int, clearSelection: () -> Unit)
@Composable
fun ContactList(
contacts: List<Contact>,
selectedContacts: SnapshotStateList<Long>,
selectedContacts: List<Long>,
groupInfo: GroupInfo,
enabled: Boolean,
addContact: (Long) -> Unit,
removeContact: (Long) -> Unit
) {
@@ -212,7 +215,8 @@ fun ContactList(
contacts.forEachIndexed { index, contact ->
ContactCheckRow(
contact, groupInfo, addContact, removeContact,
checked = selectedContacts.contains(contact.apiId)
checked = selectedContacts.contains(contact.apiId),
enabled = enabled,
)
if (index < contacts.lastIndex) {
SectionDivider()
@@ -227,7 +231,8 @@ fun ContactCheckRow(
groupInfo: GroupInfo,
addContact: (Long) -> Unit,
removeContact: (Long) -> Unit,
checked: Boolean
checked: Boolean,
enabled: Boolean,
) {
val prohibitedToInviteIncognito = !groupInfo.membership.memberIncognito && contact.contactConnIncognito
val icon: ImageVector
@@ -237,19 +242,23 @@ fun ContactCheckRow(
iconColor = HighOrLowlight
} else if (checked) {
icon = Icons.Filled.CheckCircle
iconColor = MaterialTheme.colors.primary
iconColor = if (enabled) MaterialTheme.colors.primary else HighOrLowlight
} else {
icon = Icons.Outlined.Circle
iconColor = HighOrLowlight
}
SectionItemView(click = {
if (prohibitedToInviteIncognito) {
showProhibitedToInviteIncognitoAlertDialog()
} else if (!checked)
addContact(contact.apiId)
else
removeContact(contact.apiId)
}) {
SectionItemView(
click = if (enabled) {
{
if (prohibitedToInviteIncognito) {
showProhibitedToInviteIncognitoAlertDialog()
} else if (!checked)
addContact(contact.apiId)
else
removeContact(contact.apiId)
}
} else null
) {
ProfileImage(size = 36.dp, contact.image)
Spacer(Modifier.width(DEFAULT_SPACE_AFTER_ICON))
Text(
@@ -282,6 +291,7 @@ fun PreviewAddGroupMembersLayout() {
contactsToAdd = listOf(Contact.sampleData, Contact.sampleData, Contact.sampleData),
selectedContacts = remember { mutableStateListOf() },
selectedRole = remember { mutableStateOf(GroupMemberRole.Admin) },
allowModifyMembers = true,
inviteMembers = {},
clearSelection = {},
addContact = {},

View File

@@ -2,11 +2,13 @@ package chat.simplex.app.views.chat.group
import android.content.res.Configuration
import android.graphics.Bitmap
import android.net.Uri
import androidx.compose.foundation.*
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.*
import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
@@ -53,8 +55,8 @@ fun GroupProfileLayout(
val bottomSheetModalState = rememberModalBottomSheetState(initialValue = ModalBottomSheetValue.Hidden)
val displayName = remember { mutableStateOf(groupProfile.displayName) }
val fullName = remember { mutableStateOf(groupProfile.fullName) }
val chosenImage = remember { mutableStateOf<Bitmap?>(null) }
val profileImage = remember { mutableStateOf(groupProfile.image) }
val chosenImage = rememberSaveable { mutableStateOf<Uri?>(null) }
val profileImage = rememberSaveable { mutableStateOf(groupProfile.image) }
val scope = rememberCoroutineScope()
val scrollState = rememberScrollState()
val focusRequester = remember { FocusRequester() }

View File

@@ -160,7 +160,9 @@ fun CIImageView(
placeholder = BitmapPainter(imageBitmap.asImageBitmap()), // show original image while it's still loading by coil
imageLoader = imageLoader
)
val view = LocalView.current
imageView(imagePainter, onClick = {
hideKeyboard(view)
if (getLoadedFilePath(context, file) != null) {
ModalManager.shared.showCustomModal(animated = false) { close ->
ImageFullScreenView(imageProvider, close)

View File

@@ -56,10 +56,21 @@ fun ChatItemView(
.fillMaxWidth(),
contentAlignment = alignment,
) {
val onClick = {
when (cItem.meta.itemStatus) {
is CIStatus.SndErrorAuth -> {
showMsgDeliveryErrorAlert(generalGetString(R.string.message_delivery_error_desc))
}
is CIStatus.SndError -> {
showMsgDeliveryErrorAlert(generalGetString(R.string.unknown_error) + ": ${cItem.meta.itemStatus.agentError.string}")
}
else -> {}
}
}
Column(
Modifier
.clip(RoundedCornerShape(18.dp))
.combinedClickable(onLongClick = { showMenu.value = true }, onClick = {})
.combinedClickable(onLongClick = { showMenu.value = true }, onClick = onClick)
) {
@Composable fun ContentItem() {
if (cItem.file == null && cItem.quotedItem == null && isShortEmoji(cItem.content.text)) {
@@ -210,6 +221,13 @@ fun deleteMessageAlertDialog(chatItem: ChatItem, deleteMessage: (Long, CIDeleteM
)
}
private fun showMsgDeliveryErrorAlert(description: String) {
AlertManager.shared.showAlertMsg(
title = generalGetString(R.string.message_delivery_error_title),
text = description,
)
}
@Preview
@Composable
fun PreviewChatItemView() {

View File

@@ -15,6 +15,7 @@ import androidx.compose.ui.input.pointer.*
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.res.stringResource
import chat.simplex.app.R
import chat.simplex.app.views.helpers.*

View File

@@ -3,25 +3,20 @@ package chat.simplex.app.views.helpers
import android.util.Log
import androidx.compose.material.*
import androidx.compose.runtime.*
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import chat.simplex.app.R
import chat.simplex.app.TAG
class AlertManager {
var alertView = mutableStateOf<(@Composable () -> Unit)?>(null)
var presentAlert = mutableStateOf<Boolean>(false)
var alertViews = mutableStateListOf<(@Composable () -> Unit)>()
fun showAlert(alert: @Composable () -> Unit) {
Log.d(TAG, "AlertManager.showAlert")
alertView.value = alert
presentAlert.value = true
alertViews.add(alert)
}
fun hideAlert() {
presentAlert.value = false
alertView.value = null
alertViews.removeLastOrNull()
}
fun showAlertDialogButtons(
@@ -101,7 +96,7 @@ class AlertManager {
@Composable
fun showInView() {
if (presentAlert.value) alertView.value?.invoke()
remember { alertViews }.lastOrNull()?.invoke()
}
companion object {

View File

@@ -33,6 +33,6 @@ enum class NewChatSheetState {
}
sealed class UploadContent {
data class SimpleImage(val bitmap: Bitmap): UploadContent()
data class SimpleImage(val uri: Uri): UploadContent()
data class AnimatedImage(val uri: Uri): UploadContent()
}

View File

@@ -2,8 +2,7 @@ package chat.simplex.app.views.helpers
import android.Manifest
import android.app.Activity
import android.content.Context
import android.content.Intent
import android.content.*
import android.content.pm.PackageManager
import android.graphics.*
import android.net.Uri
@@ -20,8 +19,9 @@ import androidx.compose.foundation.layout.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Collections
import androidx.compose.material.icons.outlined.PhotoCamera
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.Saver
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.onFocusChanged
import androidx.compose.ui.platform.LocalContext
@@ -31,7 +31,12 @@ import androidx.core.content.ContextCompat
import androidx.core.content.FileProvider
import chat.simplex.app.*
import chat.simplex.app.R
import chat.simplex.app.model.json
import chat.simplex.app.views.chat.ComposeState
import chat.simplex.app.views.chat.PickFromGallery
import chat.simplex.app.views.newchat.ActionButton
import kotlinx.serialization.builtins.*
import kotlinx.serialization.decodeFromString
import java.io.ByteArrayOutputStream
import java.io.File
import kotlin.math.min
@@ -106,15 +111,12 @@ fun base64ToBitmap(base64ImageString: String): Bitmap {
}
}
class CustomTakePicturePreview: ActivityResultContract<Void?, Bitmap?>() {
private var uri: Uri? = null
private var tmpFile: File? = null
lateinit var externalContext: Context
class CustomTakePicturePreview(var uri: Uri?, var tmpFile: File?): ActivityResultContract<Void?, Uri?>() {
@CallSuper
override fun createIntent(context: Context, input: Void?): Intent {
externalContext = context
tmpFile = File.createTempFile("image", ".bmp", context.filesDir)
// Since the class should return Uri, the file should be deleted somewhere else. And in order to be sure, delegate this to system
tmpFile?.deleteOnExit()
uri = FileProvider.getUriForFile(context, "${BuildConfig.APPLICATION_ID}.provider", tmpFile!!)
return Intent(MediaStore.ACTION_IMAGE_CAPTURE)
.putExtra(MediaStore.EXTRA_OUTPUT, uri)
@@ -123,20 +125,28 @@ class CustomTakePicturePreview: ActivityResultContract<Void?, Bitmap?>() {
override fun getSynchronousResult(
context: Context,
input: Void?
): SynchronousResult<Bitmap?>? = null
): SynchronousResult<Uri?>? = null
override fun parseResult(resultCode: Int, intent: Intent?): Bitmap? {
override fun parseResult(resultCode: Int, intent: Intent?): Uri? {
return if (resultCode == Activity.RESULT_OK && uri != null) {
val source = ImageDecoder.createSource(externalContext.contentResolver, uri!!)
val bitmap = ImageDecoder.decodeBitmap(source)
tmpFile?.delete()
bitmap
uri
} else {
Log.e(TAG, "Getting image from camera cancelled or failed.")
tmpFile?.delete()
null
}
}
companion object {
fun saver(): Saver<CustomTakePicturePreview, *> = Saver(
save = { json.encodeToString(ListSerializer(String.serializer().nullable), listOf(it.uri?.toString(), it.tmpFile?.toString())) },
restore = {
val data = json.decodeFromString(ListSerializer(String.serializer().nullable), it)
val uri = if (data[0] != null) Uri.parse(data[0]) else null
val tmpFile = if (data[1] != null) File(data[1]) else null
CustomTakePicturePreview(uri, tmpFile)
}
)
}
}
//class GetGalleryContent: ActivityResultContracts.GetContent() {
// override fun createIntent(context: Context, input: String): Intent {
@@ -148,8 +158,12 @@ class CustomTakePicturePreview: ActivityResultContract<Void?, Bitmap?>() {
//fun rememberGalleryLauncher(cb: (Uri?) -> Unit): ManagedActivityResultLauncher<String, Uri?> =
// rememberLauncherForActivityResult(contract = GetGalleryContent(), cb)
@Composable
fun rememberCameraLauncher(cb: (Bitmap?) -> Unit): ManagedActivityResultLauncher<Void?, Bitmap?> =
rememberLauncherForActivityResult(contract = CustomTakePicturePreview(), cb)
fun rememberCameraLauncher(cb: (Uri?) -> Unit): ManagedActivityResultLauncher<Void?, Uri?> {
val contract = rememberSaveable(stateSaver = CustomTakePicturePreview.saver()) {
mutableStateOf(CustomTakePicturePreview(null, null))
}
return rememberLauncherForActivityResult(contract = contract.value, cb)
}
@Composable
fun rememberPermissionLauncher(cb: (Boolean) -> Unit): ManagedActivityResultLauncher<String, Boolean> =
@@ -165,22 +179,26 @@ fun rememberGetMultipleContentsLauncher(cb: (List<Uri>) -> Unit): ManagedActivit
@Composable
fun GetImageBottomSheet(
imageBitmap: MutableState<Bitmap?>,
imageBitmap: MutableState<Uri?>,
onImageChange: (Bitmap) -> Unit,
hideBottomSheet: () -> Unit
) {
val context = LocalContext.current
val galleryLauncher = rememberGetContentLauncher { uri: Uri? ->
val processPickedImage = { uri: Uri? ->
if (uri != null) {
val source = ImageDecoder.createSource(context.contentResolver, uri)
val bitmap = ImageDecoder.decodeBitmap(source)
imageBitmap.value = bitmap
imageBitmap.value = uri
onImageChange(bitmap)
}
}
val cameraLauncher = rememberCameraLauncher { bitmap: Bitmap? ->
if (bitmap != null) {
imageBitmap.value = bitmap
val galleryLauncher = rememberLauncherForActivityResult(contract = PickFromGallery()) { processPickedImage(it) }
val galleryLauncherFallback = rememberGetContentLauncher { processPickedImage(it) }
val cameraLauncher = rememberCameraLauncher { uri: Uri? ->
if (uri != null) {
imageBitmap.value = uri
val source = ImageDecoder.createSource(SimplexApp.context.contentResolver, uri)
val bitmap = ImageDecoder.decodeBitmap(source)
onImageChange(bitmap)
}
}
@@ -219,7 +237,11 @@ fun GetImageBottomSheet(
}
}
ActionButton(null, stringResource(R.string.from_gallery_button), icon = Icons.Outlined.Collections) {
galleryLauncher.launch("image/*")
try {
galleryLauncher.launch(0)
} catch (e: ActivityNotFoundException) {
galleryLauncherFallback.launch("image/*")
}
hideBottomSheet()
}
}

View File

@@ -89,18 +89,6 @@ fun laTurnedOnAlert() = AlertManager.shared.showAlertMsg(
generalGetString(R.string.auth_you_will_be_required_to_authenticate_when_you_start_or_resume)
)
fun laErrorToast(context: Context, errString: CharSequence) = Toast.makeText(
context,
if (errString.isNotEmpty()) String.format(generalGetString(R.string.auth_error_w_desc), errString) else generalGetString(R.string.auth_error),
Toast.LENGTH_SHORT
).show()
fun laFailedToast(context: Context) = Toast.makeText(
context,
generalGetString(R.string.auth_failed),
Toast.LENGTH_SHORT
).show()
fun laUnavailableInstructionAlert() = AlertManager.shared.showAlertMsg(
generalGetString(R.string.auth_unavailable),
generalGetString(R.string.auth_device_authentication_is_not_enabled_you_can_turn_on_in_settings_once_enabled)

View File

@@ -40,6 +40,12 @@ fun SearchTextField(modifier: Modifier, placeholder: String, onValueChange: (Str
keyboard?.show()
}
DisposableEffect(Unit) {
onDispose {
if (searchText.text.isNotEmpty()) onValueChange("")
}
}
val enabled = true
val colors = TextFieldDefaults.textFieldColors(
backgroundColor = Color.Unspecified,

View File

@@ -14,7 +14,9 @@ import android.text.SpannedString
import android.text.style.*
import android.util.Base64
import android.util.Log
import android.view.View
import android.view.ViewTreeObserver
import android.view.inputmethod.InputMethodManager
import androidx.annotation.StringRes
import androidx.compose.runtime.*
import androidx.compose.ui.graphics.Color
@@ -69,6 +71,9 @@ fun getKeyboardState(): State<KeyboardState> {
return keyboardState
}
fun hideKeyboard(view: View) =
(SimplexApp.context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager).hideSoftInputFromWindow(view.windowToken, 0)
// Resource to annotated string from
// https://stackoverflow.com/questions/68549248/android-jetpack-compose-how-to-show-styled-text-from-string-resources
fun generalGetString(id: Int): String {
@@ -306,6 +311,12 @@ fun getFileSize(context: Context, uri: Uri): Long? {
}
}
fun saveImage(context: Context, uri: Uri): String? {
val source = ImageDecoder.createSource(SimplexApp.context.contentResolver, uri)
val bitmap = ImageDecoder.decodeBitmap(source)
return saveImage(context, bitmap)
}
fun saveImage(context: Context, image: Bitmap): String? {
return try {
val ext = if (image.hasAlpha()) "png" else "jpg"

View File

@@ -1,6 +1,7 @@
package chat.simplex.app.views.newchat
import android.graphics.Bitmap
import android.net.Uri
import androidx.compose.foundation.*
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
@@ -8,6 +9,7 @@ import androidx.compose.material.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.ArrowForwardIos
import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
@@ -60,8 +62,8 @@ fun AddGroupLayout(chatModelIncognito: Boolean, createGroup: (GroupProfile) -> U
val scope = rememberCoroutineScope()
val displayName = remember { mutableStateOf("") }
val fullName = remember { mutableStateOf("") }
val profileImage = remember { mutableStateOf<String?>(null) }
val chosenImage = remember { mutableStateOf<Bitmap?>(null) }
val chosenImage = rememberSaveable { mutableStateOf<Uri?>(null) }
val profileImage = rememberSaveable { mutableStateOf<String?>(null) }
val focusRequester = remember { FocusRequester() }
ProvideWindowInsets(windowInsetsAnimationsEnabled = true) {

View File

@@ -3,6 +3,7 @@ package chat.simplex.app.views.newchat
import android.content.ClipboardManager
import android.content.res.Configuration
import android.net.Uri
import android.util.Log
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
@@ -17,6 +18,7 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.core.content.ContextCompat.getSystemService
import chat.simplex.app.R
import chat.simplex.app.TAG
import chat.simplex.app.model.ChatModel
import chat.simplex.app.ui.theme.*
import chat.simplex.app.views.helpers.*
@@ -35,10 +37,21 @@ fun PasteToConnectView(chatModel: ChatModel, close: () -> Unit) {
connectViaLink = { connReqUri ->
try {
val uri = Uri.parse(connReqUri)
withUriAction(uri) { action ->
if (connectViaUri(chatModel, action, uri)) {
close()
withUriAction(uri) { linkType ->
val action = suspend {
Log.d(TAG, "connectViaUri: connecting")
if (connectViaUri(chatModel, linkType, uri)) {
close()
}
}
if (linkType == ConnectionLinkType.GROUP) {
AlertManager.shared.showAlertMsg(
title = generalGetString(R.string.connect_via_group_link),
text = generalGetString(R.string.you_will_join_group),
confirmText = generalGetString(R.string.connect_via_link_verb),
onConfirm = { withApi { action() } }
)
} else action()
}
} catch (e: RuntimeException) {
AlertManager.shared.showAlertMsg(

View File

@@ -3,6 +3,7 @@ package chat.simplex.app.views.newchat
import android.Manifest
import android.content.res.Configuration
import android.net.Uri
import android.util.Log
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
@@ -14,12 +15,17 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.core.net.toUri
import chat.simplex.app.R
import chat.simplex.app.TAG
import chat.simplex.app.model.ChatModel
import chat.simplex.app.model.json
import chat.simplex.app.ui.theme.DEFAULT_PADDING
import chat.simplex.app.ui.theme.SimpleXTheme
import chat.simplex.app.views.helpers.*
import com.google.accompanist.permissions.rememberPermissionState
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Composable
fun ScanToConnectView(chatModel: ChatModel, close: () -> Unit) {
@@ -33,10 +39,21 @@ fun ScanToConnectView(chatModel: ChatModel, close: () -> Unit) {
QRCodeScanner { connReqUri ->
try {
val uri = Uri.parse(connReqUri)
withUriAction(uri) { action ->
if (connectViaUri(chatModel, action, uri)) {
close()
withUriAction(uri) { linkType ->
val action = suspend {
Log.d(TAG, "connectViaUri: connecting")
if (connectViaUri(chatModel, linkType, uri)) {
close()
}
}
if (linkType == ConnectionLinkType.GROUP) {
AlertManager.shared.showAlertMsg(
title = generalGetString(R.string.connect_via_group_link),
text = generalGetString(R.string.you_will_join_group),
confirmText = generalGetString(R.string.connect_via_link_verb),
onConfirm = { withApi { action() } }
)
} else action()
}
} catch (e: RuntimeException) {
AlertManager.shared.showAlertMsg(
@@ -49,10 +66,34 @@ fun ScanToConnectView(chatModel: ChatModel, close: () -> Unit) {
)
}
fun withUriAction(uri: Uri, run: suspend (String) -> Unit) {
enum class ConnectionLinkType {
CONTACT, INVITATION, GROUP
}
@Serializable
sealed class CReqClientData {
@Serializable @SerialName("group") data class Group(val groupLinkId: String): CReqClientData()
}
fun withUriAction(uri: Uri, run: suspend (ConnectionLinkType) -> Unit) {
val action = uri.path?.drop(1)?.replace("/", "")
if (action == "contact" || action == "invitation") {
withApi { run(action) }
val data = uri.toString().replaceFirst("#/", "/").toUri().getQueryParameter("data")
val type = when {
data != null -> {
val parsed = runCatching {
json.decodeFromString(CReqClientData.serializer(), data)
}
when {
parsed.getOrNull() is CReqClientData.Group -> ConnectionLinkType.GROUP
else -> null
}
}
action == "contact" -> ConnectionLinkType.CONTACT
action == "invitation" -> ConnectionLinkType.INVITATION
else -> null
}
if (type != null) {
withApi { run(type) }
} else {
AlertManager.shared.showAlertMsg(
title = generalGetString(R.string.invalid_contact_link),
@@ -61,14 +102,17 @@ fun withUriAction(uri: Uri, run: suspend (String) -> Unit) {
}
}
suspend fun connectViaUri(chatModel: ChatModel, action: String, uri: Uri): Boolean {
suspend fun connectViaUri(chatModel: ChatModel, action: ConnectionLinkType, uri: Uri): Boolean {
val r = chatModel.controller.apiConnect(uri.toString())
if (r) {
AlertManager.shared.showAlertMsg(
title = generalGetString(R.string.connection_request_sent),
text =
if (action == "contact") generalGetString(R.string.you_will_be_connected_when_your_connection_request_is_accepted)
else generalGetString(R.string.you_will_be_connected_when_your_contacts_device_is_online)
when (action) {
ConnectionLinkType.CONTACT -> generalGetString(R.string.you_will_be_connected_when_your_connection_request_is_accepted)
ConnectionLinkType.INVITATION -> generalGetString(R.string.you_will_be_connected_when_your_contacts_device_is_online)
ConnectionLinkType.GROUP -> generalGetString(R.string.you_will_be_connected_when_group_host_device_is_online)
}
)
}
return r

View File

@@ -146,13 +146,25 @@ fun SettingsLayout(
}
SectionSpacer()
SectionView(stringResource(R.string.settings_section_title_support)) {
ContributeItem(uriHandler)
SectionDivider()
RateAppItem(uriHandler)
SectionDivider()
StarOnGithubItem(uriHandler)
}
SectionSpacer()
SectionView(stringResource(R.string.settings_section_title_develop)) {
ChatConsoleItem(showTerminal)
SectionDivider()
SettingsPreferenceItem(Icons.Outlined.Construction, stringResource(R.string.settings_developer_tools), developerTools)
SectionDivider()
InstallTerminalAppItem(uriHandler)
val devTools = remember { mutableStateOf(developerTools.get()) }
SettingsPreferenceItem(Icons.Outlined.Construction, stringResource(R.string.settings_developer_tools), developerTools, devTools)
SectionDivider()
if (devTools.value) {
ChatConsoleItem(showTerminal)
SectionDivider()
InstallTerminalAppItem(uriHandler)
SectionDivider()
}
// SettingsActionItem(Icons.Outlined.Science, stringResource(R.string.settings_experimental_features), showSettingsModal { ExperimentalFeaturesView(it, enableCalls) })
// SectionDivider()
AppVersionItem()
@@ -252,6 +264,46 @@ fun MaintainIncognitoState(chatModel: ChatModel) {
}
}
@Composable private fun ContributeItem(uriHandler: UriHandler) {
SectionItemView({ uriHandler.openUri("https://github.com/simplex-chat/simplex-chat#contribute") }) {
Icon(
Icons.Outlined.Keyboard,
contentDescription = "GitHub",
tint = HighOrLowlight,
)
Spacer(Modifier.padding(horizontal = 4.dp))
Text(generalGetString(R.string.contribute), color = MaterialTheme.colors.primary)
}
}
@Composable private fun RateAppItem(uriHandler: UriHandler) {
SectionItemView({
runCatching { uriHandler.openUri("market://details?id=chat.simplex.app") }
.onFailure { uriHandler.openUri("https://play.google.com/store/apps/details?id=chat.simplex.app") }
}
) {
Icon(
Icons.Outlined.StarOutline,
contentDescription = "Google Play",
tint = HighOrLowlight,
)
Spacer(Modifier.padding(horizontal = 4.dp))
Text(generalGetString(R.string.rate_the_app), color = MaterialTheme.colors.primary)
}
}
@Composable private fun StarOnGithubItem(uriHandler: UriHandler) {
SectionItemView({ uriHandler.openUri("https://github.com/simplex-chat/simplex-chat") }) {
Icon(
painter = painterResource(id = R.drawable.ic_github),
contentDescription = "GitHub",
tint = HighOrLowlight,
)
Spacer(Modifier.padding(horizontal = 4.dp))
Text(generalGetString(R.string.star_on_github), color = MaterialTheme.colors.primary)
}
}
@Composable private fun ChatConsoleItem(showTerminal: () -> Unit) {
SectionItemView(showTerminal) {
Icon(

View File

@@ -2,6 +2,7 @@ package chat.simplex.app.views.usersettings
import android.content.res.Configuration
import android.graphics.Bitmap
import android.net.Uri
import androidx.compose.foundation.*
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CircleShape
@@ -12,6 +13,7 @@ import androidx.compose.material.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.*
import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
@@ -34,7 +36,7 @@ import kotlinx.coroutines.launch
fun UserProfileView(chatModel: ChatModel, close: () -> Unit) {
val user = chatModel.currentUser.value
if (user != null) {
val editProfile = remember { mutableStateOf(false) }
val editProfile = rememberSaveable { mutableStateOf(false) }
var profile by remember { mutableStateOf(user.profile.toProfile()) }
UserProfileLayout(
editProfile = editProfile,
@@ -67,8 +69,8 @@ fun UserProfileLayout(
val bottomSheetModalState = rememberModalBottomSheetState(initialValue = ModalBottomSheetValue.Hidden)
val displayName = remember { mutableStateOf(profile.displayName) }
val fullName = remember { mutableStateOf(profile.fullName) }
val chosenImage = remember { mutableStateOf<Bitmap?>(null) }
val profileImage = remember { mutableStateOf(profile.image) }
val chosenImage = rememberSaveable { mutableStateOf<Uri?>(null) }
val profileImage = rememberSaveable { mutableStateOf(profile.image) }
val scope = rememberCoroutineScope()
val scrollState = rememberScrollState()
val keyboardState by getKeyboardState()

View File

@@ -6,7 +6,9 @@
<!-- Connect via Link - MainActivity.kt -->
<string name="connect_via_contact_link">Über den Kontakt-Link verbinden?</string>
<string name="connect_via_invitation_link">Über den Einladungs-Link verbinden?</string>
<string name="connect_via_group_link">Über den Gruppen-Link verbinden?</string>
<string name="profile_will_be_sent_to_contact_sending_link">Ihr Profil wird an den Kontakt gesendet von dem Sie diesen Link erhalten haben.</string>
<string name="you_will_join_group">Sie werden der Gruppe beitreten, auf die sich dieser Link bezieht und sich mit deren Gruppenmitgliedern verbinden.</string>
<string name="connect_via_link_verb">Verbinden</string>
<!-- Server info - ChatModel.kt -->
@@ -32,12 +34,12 @@
<string name="display_name_connecting">verbinde…</string>
<string name="description_you_shared_one_time_link">sie haben einen Einmal-Link geteilt</string>
<string name="description_you_shared_one_time_link_incognito">sie haben Inkognito einen Einmal-Link geteilt</string>
<string name="description_via_group_link">***via group link</string>
<string name="description_via_group_link_incognito">***incognito via group link</string>
<string name="description_via_contact_address_link">über einen Kontaktadressen Link</string>
<string name="description_via_contact_address_link_incognito">inkognito über einen Kontaktadressen Link</string>
<string name="description_via_group_link">über einen Gruppen-Link</string>
<string name="description_via_group_link_incognito">Inkognito über einen Gruppen-Link</string>
<string name="description_via_contact_address_link">über einen Kontaktadressen-Link</string>
<string name="description_via_contact_address_link_incognito">Inkognito über einen Kontaktadressen-Link</string>
<string name="description_via_one_time_link">über einen Einmal-Link</string>
<string name="description_via_one_time_link_incognito">inkognito über einen Einmal-Link</string>
<string name="description_via_one_time_link_incognito">Inkognito über einen Einmal-Link</string>
<!-- SimpleXAPI.kt -->
<string name="error_saving_smp_servers">Fehler beim Speichern der SMP-Server</string>
@@ -56,7 +58,7 @@
<string name="error_receiving_file">Fehler beim Empfangen der Datei</string>
<string name="error_creating_address">Fehler beim Erstellen der Adresse</string>
<string name="contact_already_exists">Kontakt ist bereits vorhanden</string>
<string name="you_are_already_connected_to_vName_via_this_link">Sie sind bereits über diesen Link mit <xliff:g id="contactName" example="Alice">%1$s!</xliff:g> verbunden.</string>
<string name="you_are_already_connected_to_vName_via_this_link">Sie sind bereits mit <xliff:g id="contactName" example="Alice">%1$s!</xliff:g> verbunden.</string>
<string name="invalid_connection_link">Ungültiger Verbindungslink</string>
<string name="please_check_correct_link_and_maybe_ask_for_a_new_one">Überprüfen Sie bitte, ob Sie den richtigen Link genutzt haben oder bitten Sie Ihren Kontakt darum, Ihnen nochmal einen Link zuzusenden.</string>
<string name="connection_error_auth">Verbindungsfehler (AUTH)</string>
@@ -67,7 +69,7 @@
<string name="error_deleting_group">Fehler beim Löschen der Gruppe</string>
<string name="error_deleting_contact_request">Fehler beim Löschen der Kontaktanfrage</string>
<string name="error_deleting_pending_contact_connection">Fehler beim Löschen der anstehenden Kontaktaufnahme</string>
<string name="error_changing_address">*** Error changing address</string>
<string name="error_changing_address">Fehler beim Wechseln der Adresse</string>
<!-- background service notice - SimpleXAPI.kt -->
<string name="icon_descr_instant_notifications">Sofortige Benachrichtigungen</string>
@@ -129,16 +131,16 @@
<string name="auth_enable_simplex_lock">SimpleX Sperre aktivieren</string>
<string name="auth_disable_simplex_lock">SimpleX Sperre deaktivieren</string>
<string name="auth_confirm_credential">Bestätigen Sie Ihre Zugangsdaten</string>
<string name="auth_error">Authentifizierungsfehler</string>
<string name="auth_error_w_desc">Authentifizierungsfehler: <xliff:g id="desc">%1$s</xliff:g></string>
<string name="auth_failed">Authentifizierung fehlgeschlagen</string>
<string name="auth_unavailable">Authentifizierung nicht verfügbar</string>
<string name="auth_device_authentication_is_not_enabled_you_can_turn_on_in_settings_once_enabled">Geräteauthentifizierung ist nicht aktiviert. Sie können die SimpleX Sperre über die Einstellungen aktivieren, sobald Sie die Geräteauthentifizierung aktiviert haben.</string>
<string name="auth_device_authentication_is_disabled_turning_off">Geräteauthentifizierung ist deaktiviert. SimpleX Sperre ist abgeschaltet.</string>
<string name="auth_retry">Wiederholen</string>
<string name="auth_stop_chat">Chat beenden</string>
<string name="auth_open_chat_console">Chat-Konsole öffnen</string>
<!-- Chat Alerts - ChatItemView.kt -->
<string name="message_delivery_error_title">Fehler bei der Nachrichtenzustellung</string>
<string name="message_delivery_error_desc">Dieser Kontakt hat sehr wahrscheinlich die Verbindung mit Ihnen gelöscht.</string>
<!-- Chat Actions - ChatItemView.kt (and general) -->
<string name="reply_verb">Antwort</string>
<string name="share_verb">Teilen</string>
@@ -147,7 +149,7 @@
<string name="edit_verb">Bearbeiten</string>
<string name="delete_verb">Löschen</string>
<string name="delete_message__question">Die Nachricht löschen?</string>
<string name="delete_message_cannot_be_undone_warning">Nachricht wird gelöscht - Dies kann nicht rückgängig gemacht werden!</string>
<string name="delete_message_cannot_be_undone_warning">Nachricht wird gelöscht - dies kann nicht rückgängig gemacht werden!</string>
<string name="for_me_only">Nur für mich</string>
<string name="for_everybody">Für alle</string>
@@ -183,6 +185,8 @@
<string name="icon_descr_cancel_file_preview">Dateivorschau abbrechen</string>
<string name="images_limit_title">Zu viele Bilder!</string>
<string name="images_limit_desc">Es können nur 10 Bilder auf einmal gesendet werden</string>
<string name="image_decoding_exception_title">Dekodierungsfehler</string>
<string name="image_decoding_exception_desc">Das Bild kann nicht dekodiert werden. Bitte versuchen Sie es mit einem anderen Bild oder wenden Sie sich an die Entwickler.</string>
<!-- Images - chat.simplex.app.views.chat.item.CIImageView.kt -->
<string name="image_descr">Bild</string>
@@ -190,7 +194,7 @@
<string name="icon_descr_asked_to_receive">Es wird um den Empfang eines Bildes gebeten</string>
<string name="icon_descr_image_snd_complete">Bild gesendet</string>
<string name="waiting_for_image">Warten auf ein Bild</string>
<string name="image_will_be_received_when_contact_is_online">Das Bild wird empfangen, wenn Ihr Kontakt online ist, bitte warten oder schauen Sie später nochmal nach!</string>
<string name="image_will_be_received_when_contact_is_online">Das Bild wird empfangen, sobald Ihr Kontakt online ist. Bitte warten oder schauen Sie später nochmal nach!</string>
<string name="image_saved">Bild wurde im Fotoalbum gespeichert</string>
<!-- Files - CIFileView.kt -->
@@ -199,7 +203,7 @@
<string name="contact_sent_large_file">Ihr Kontakt hat eine Datei gesendet, die größer ist als die derzeit unterstützte maximale Größe (<xliff:g id="maxFileSize">%1$s</xliff:g>).</string>
<string name="maximum_supported_file_size">Die derzeit maximal unterstützte Dateigröße beträgt <xliff:g id="maxFileSize">%1$s</xliff:g>.</string>
<string name="waiting_for_file">Warte auf Datei</string>
<string name="file_will_be_received_when_contact_is_online">Die Datei wird empfangen, wenn Ihr Kontakt online ist, bitte warten oder schauen Sie später nochmal nach!</string>
<string name="file_will_be_received_when_contact_is_online">Die Datei wird empfangen, sobald Ihr Kontakt online ist. Bitte warten oder schauen Sie später nochmal nach!</string>
<string name="file_saved">Datei gespeichert</string>
<string name="file_not_found">Datei nicht gefunden</string>
<string name="error_saving_file">Fehler beim Speichern der Datei</string>
@@ -216,8 +220,8 @@
<string name="icon_descr_server_status_disconnected">Getrennt</string>
<string name="icon_descr_server_status_error">Fehler</string>
<string name="icon_descr_server_status_pending">Ausstehend</string>
<string name="switch_receiving_address_question">*** Switch receiving address?</string>
<string name="switch_receiving_address_desc">*** This feature is experimental! It will only work if the other client has version 4.2 installed. You should see the message in the conversation once the address change is completed please check that you can still receive messages from this contact (or group member).</string>
<string name="switch_receiving_address_question">Empfängeradresse wechseln?</string>
<string name="switch_receiving_address_desc">Diese Funktion ist experimentell! Sie wird nur funktionieren, wenn der Kontakt ebenfalls die SimpleX-Version 4.2 installiert hat. Sobald der Adress-Wechsel abgeschlossen ist, sollten Sie die Nachricht im Chat-Verlauf sehen. Bitte prüfen Sie, ob Sie weiterhin Nachrichten von diesem Kontakt (oder Gruppenmitglied) empfangen können.</string>
<!-- Message Actions - SendMsgView.kt -->
<string name="icon_descr_send_message">Nachricht senden</string>
@@ -323,9 +327,9 @@
<string name="invalid_contact_link">Ungültiger Link!</string>
<string name="this_link_is_not_a_valid_connection_link">Dieser Link ist kein gültiger Verbindungslink!</string>
<string name="connection_request_sent">Verbindungsanfrage gesendet!</string>
<string name="you_will_be_connected_when_group_host_device_is_online">***You will be connected to group when the group host\'s device is online, please wait or check later!</string>
<string name="you_will_be_connected_when_your_connection_request_is_accepted">Sie werden verbunden, sobald Ihre Verbindungsanfrage akzeptiert wird, bitte warten oder schauen Sie später nochmal nach!</string>
<string name="you_will_be_connected_when_your_contacts_device_is_online">Sie werden verbunden, wenn das Gerät Ihres Kontakts online ist, bitte warten oder schauen Sie später nochmal nach!</string>
<string name="you_will_be_connected_when_group_host_device_is_online">Sie werden mit der Gruppe verbunden, sobald das Endgerät des Gruppen-Hosts online ist. Bitte warten oder schauen Sie später nochmal nach!</string>
<string name="you_will_be_connected_when_your_connection_request_is_accepted">Sie werden verbunden, sobald Ihre Verbindungsanfrage akzeptiert wird. Bitte warten oder schauen Sie später nochmal nach!</string>
<string name="you_will_be_connected_when_your_contacts_device_is_online">Sie werden verbunden, sobald das Endgerät Ihres Kontakts online ist. Bitte warten oder schauen Sie später nochmal nach!</string>
<string name="show_QR_code_for_your_contact_to_scan_from_the_app__multiline">Zeigen Sie Ihrem Kontakt den QR-Code aus der App zum Scannen.</string>
<string name="if_you_cannot_meet_in_person_show_QR_in_video_call_or_via_another_channel">Wenn Sie sich nicht persönlich treffen können, können Sie <b>den QR-Code während eines Videoanrufs anzeigen</b> oder einen Einladungslink über einen anderen Kanal mit Ihrem Kontakt teilen.</string >
<string name="your_chat_profile_will_be_sent_to_your_contact">Ihr Chat-Profil wird\nan Ihren Kontakt gesendet.</string>
@@ -354,12 +358,15 @@
<string name="how_to_use_simplex_chat">Wie man SimpleX nutzt</string>
<string name="markdown_help">Markdown Hilfe</string>
<string name="markdown_in_messages">Markdown in Nachrichten</string>
<string name="chat_with_the_founder">Verbinden Sie sich mit den Entwicklern</string>
<string name="chat_with_the_founder">Senden Sie Fragen und Ideen</string>
<string name="send_us_an_email">Senden Sie uns eine E-Mail</string>
<string name="chat_lock">SimpleX Sperre</string>
<string name="chat_console">Chat Konsole</string>
<string name="smp_servers">SMP-Server</string>
<string name="install_simplex_chat_for_terminal">Installieren Sie <xliff:g id="appNameFull">SimpleX Chat</xliff:g> als Terminalanwendung</string>
<string name="star_on_github">Stern auf GitHub</string>
<string name="contribute">Beitragen</string>
<string name="rate_the_app">Bewerte die App</string>
<string name="use_simplex_chat_servers__question">Verwenden Sie <xliff:g id="appNameFull">SimpleX Chat</xliff:g> Server?</string>
<string name="saved_SMP_servers_will_be_removed">Gespeicherte SMP-Server werden entfernt.</string>
<string name="your_SMP_servers">Ihre SMP-Server</string>
@@ -399,7 +406,7 @@
<string name="create_address">Adresse erstellen</string>
<string name="delete_address__question">Adresse löschen?</string>
<string name="all_your_contacts_will_remain_connected">Alle Ihre Kontakte bleiben verbunden.</string>
<string name="you_can_share_your_address_anybody_will_be_able_to_connect">Sie können Ihre Adresse als Link oder als QR-Code teilen Jeder kann sich darüber mit Ihnen verbinden. Sie werden Ihre mit dieser Adresse verbundenen Kontakte nicht verlieren, wenn Sie diese Adresse später löschen.</string>
<string name="you_can_share_your_address_anybody_will_be_able_to_connect">Sie können Ihre Adresse als Link oder als QR-Code teilen Jede Person kann sich darüber mit Ihnen verbinden. Sie werden Ihre mit dieser Adresse verbundenen Kontakte nicht verlieren, wenn Sie diese Adresse später löschen.</string>
<string name="share_link">Link teilen</string>
<string name="delete_address">Adresse löschen</string>
@@ -468,7 +475,7 @@
<string name="immune_to_spam_and_abuse">Immun gegen Spam und Missbrauch</string>
<string name="people_can_connect_only_via_links_you_share">Verbindungen mit Kontakten sind nur über Links möglich, die Sie oder Ihre Kontakte untereinander teilen.</string>
<string name="decentralized">Dezentral</string>
<string name="opensource_protocol_and_code_anybody_can_run_servers">Open-Source-Protokoll und -Code Jeder kann seine eigenen Server nutzen.</string>
<string name="opensource_protocol_and_code_anybody_can_run_servers">Open-Source-Protokoll und -Code Jede Person kann ihre eigenen Server aufsetzen und nutzen.</string>
<string name="create_your_profile">Erstellen Sie Ihr Profil</string>
<string name="make_private_connection">Stellen Sie eine private Verbindung her</string>
<string name="how_it_works">Wie es funktioniert</string>
@@ -553,13 +560,14 @@
<string name="privacy_and_security">Datenschutz &amp; Sicherheit</string>
<string name="your_privacy">Meine Privatsphäre</string>
<string name="auto_accept_images">Bilder automatisch akzeptieren</string>
<string name="transfer_images_faster">*** Transfer images faster (BETA)</string>
<string name="transfer_images_faster">Bilder schneller übertragen (BETA)</string>
<string name="send_link_previews">Link-Vorschau senden</string>
<!-- Settings sections -->
<string name="settings_section_title_you">MEINE DATEN</string>
<string name="settings_section_title_settings">EINSTELLUNGEN</string>
<string name="settings_section_title_help">HILFE</string>
<string name="settings_section_title_support">UNTERSTÜTZEN SIMPLEX CHAT</string>
<string name="settings_section_title_develop">ENTWICKLUNG</string>
<string name="settings_section_title_device">GERÄT</string>
<string name="settings_section_title_chats">CHATS</string>
@@ -618,7 +626,7 @@
<string name="chat_item_ttl_seconds">%s Sekunde(n)</string>
<string name="delete_messages_after">Löschen der Nachrichten</string>
<string name="enable_automatic_deletion_question">Automatisches Löschen von Nachrichten aktivieren?</string>
<string name="enable_automatic_deletion_message">Diese Aktion kann nicht rückgängig gemacht werden - Alle empfangenen und gesendeten Nachrichten, die über den ausgewählten Zeitraum hinaus gehen, werden gelöscht. Dies kann mehrere Minuten dauern.</string>
<string name="enable_automatic_deletion_message">Diese Aktion kann nicht rückgängig gemacht werden - Alle empfangenen und gesendeten Nachrichten, die über den ausgewählten Zeitraum hinaus gehen, werden gelöscht. Dieser Vorgang kann mehrere Minuten dauern.</string>
<string name="delete_messages">Nachrichten löschen</string>
<string name="error_changing_message_deletion">Fehler beim Ändern der Einstellung</string>
@@ -719,7 +727,7 @@
<!-- Group event chat items -->
<string name="rcv_group_event_member_added">hat <xliff:g id="member profile" example="alice (Alice)">%1$s</xliff:g> eingeladen.</string>
<string name="rcv_group_event_member_connected">beigetreten</string>
<string name="rcv_group_event_member_left">verlassen</string>
<string name="rcv_group_event_member_left">hat die Gruppe verlassen</string>
<string name="rcv_group_event_changed_member_role">änderte die Rolle von %s auf %s</string>
<string name="rcv_group_event_changed_your_role">änderte Ihre Rolle auf %s</string>
<string name="rcv_group_event_member_deleted">entfernte <xliff:g id="member profile" example="alice (Alice)">%1$s</xliff:g> aus der Gruppe.</string>
@@ -734,12 +742,12 @@
<string name="snd_group_event_group_profile_updated">Gruppenprofil aktualisiert</string>
<!-- Conn event chat items -->
<string name="rcv_conn_event_switch_queue_phase_completed">*** changed address for you</string>
<string name="rcv_conn_event_switch_queue_phase_changing">*** changing address…</string>
<string name="snd_conn_event_switch_queue_phase_completed_for_member">*** you changed address for %s</string>
<string name="snd_conn_event_switch_queue_phase_changing_for_member">*** changing address for %s</string>
<string name="snd_conn_event_switch_queue_phase_completed">*** you changed address</string>
<string name="snd_conn_event_switch_queue_phase_changing">*** changing address…</string>
<string name="rcv_conn_event_switch_queue_phase_completed">wechselte die Adresse für Sie</string>
<string name="rcv_conn_event_switch_queue_phase_changing">Wechsel der Adresse ...</string>
<string name="snd_conn_event_switch_queue_phase_completed_for_member">Sie haben die Adresse für %s gewechselt</string>
<string name="snd_conn_event_switch_queue_phase_changing_for_member">Wechsel der Adresse für %s ...</string>
<string name="snd_conn_event_switch_queue_phase_completed">Sie haben die Adresse gewechselt</string>
<string name="snd_conn_event_switch_queue_phase_changing">Wechsel der Adresse ...</string>
<!-- GroupMemberRole -->
<string name="group_member_role_member">Mitglied</string>
@@ -806,7 +814,7 @@
<string name="role_in_group">Rolle</string>
<string name="change_role">Rolle ändern</string>
<string name="change_verb">Ändern</string>
<string name="switch_verb">***Switch</string>
<string name="switch_verb">Wechseln</string>
<string name="change_member_role_question">Die Mitgliederrolle ändern?</string>
<string name="member_role_will_be_changed_with_notification">Die Mitgliederrolle wird auf \"%s\" geändert. Alle Mitglieder der Gruppe werden benachrichtigt.</string>
<string name="member_role_will_be_changed_with_invitation">Die Mitgliederrolle wird auf \"%s\" geändert. Das Mitglied wird eine neue Einladung erhalten.</string>
@@ -822,7 +830,7 @@
<string name="receiving_via">Empfangen über</string>
<string name="sending_via">Senden über</string>
<string name="network_status">Netzwerkstatus</string>
<string name="switch_receiving_address">***Switch receiving address (BETA)</string>
<string name="switch_receiving_address">Wechseln der Empfängeradresse (BETA)</string>
<!-- AddGroupView.kt -->
<string name="create_secret_group_title">Geheime Gruppe erstellen</string>

View File

@@ -6,7 +6,9 @@
<!-- Connect via Link - MainActivity.kt -->
<string name="connect_via_contact_link">Соединиться через ссылку-контакт?</string>
<string name="connect_via_invitation_link">Соединиться через ссылку-приглашение?</string>
<string name="connect_via_group_link">Соединиться через ссылку группы?</string>
<string name="profile_will_be_sent_to_contact_sending_link">Ваш профиль будет отправлен контакту, от которого вы получили эту ссылку.</string>
<string name="you_will_join_group">Вы вступите в группу, на которую ссылается эта ссылка.</string>
<string name="connect_via_link_verb">Соединиться</string>
<!-- Server info - ChatModel.kt -->
@@ -56,7 +58,7 @@
<string name="error_receiving_file">Ошибка при получении файла</string>
<string name="error_creating_address">Ошибка при создании адреса</string>
<string name="contact_already_exists">Существующий контакт</string>
<string name="you_are_already_connected_to_vName_via_this_link">Вы уже соединены с <xliff:g id="contactName" example="Alice">%1$s!</xliff:g> через эту ссылку.</string>
<string name="you_are_already_connected_to_vName_via_this_link">Вы уже соединены с контактом <xliff:g id="contactName" example="Alice">%1$s!</xliff:g>.</string>
<string name="invalid_connection_link">Ошибка в ссылке контакта</string>
<string name="please_check_correct_link_and_maybe_ask_for_a_new_one">Пожалуйста, проверьте, что вы использовали правильную ссылку, или попросите ваш контакт отправить вам новую.</string>
<string name="connection_error_auth">Ошибка соединения (AUTH)</string>
@@ -129,16 +131,16 @@
<string name="auth_enable_simplex_lock">Включить блокировку SimpleX</string>
<string name="auth_disable_simplex_lock">Отключить блокировку SimpleX</string>
<string name="auth_confirm_credential">Пройдите аутентификацию</string>
<string name="auth_error">Ошибка аутентификации</string>
<string name="auth_error_w_desc">Ошибка аутентификации: <xliff:g id="desc">%1$s</xliff:g></string>
<string name="auth_failed">Ошибка аутентификации</string>
<string name="auth_unavailable">Аутентификация недоступна</string>
<string name="auth_device_authentication_is_not_enabled_you_can_turn_on_in_settings_once_enabled">Аутентификация устройства не включена. Вы можете включить блокировку SimpleX в Настройках после включения аутентификации.</string>
<string name="auth_device_authentication_is_disabled_turning_off">Аутентификация устройства выключена. Отключение блокировки SimpleX Chat.</string>
<string name="auth_retry">Повторить</string>
<string name="auth_stop_chat">Остановить чат</string>
<string name="auth_open_chat_console">Открыть консоль</string>
<!-- Chat Alerts - ChatItemView.kt -->
<string name="message_delivery_error_title">Ошибка доставки сообщения</string>
<string name="message_delivery_error_desc">Скорее всего, этот контакт удалил соединение с вами.</string>
<!-- Chat Actions - ChatItemView.kt (and general) -->
<string name="reply_verb">Ответить</string>
<string name="share_verb">Поделиться</string>
@@ -183,6 +185,8 @@
<string name="icon_descr_cancel_file_preview">Удалить превью файла</string>
<string name="images_limit_title">Слишком много изображений!</string>
<string name="images_limit_desc">Только 10 изображений могут быть отправлены одномоментно</string>
<string name="image_decoding_exception_title">Ошибка декодирования</string>
<string name="image_decoding_exception_desc">Не получается декодировать изображение. Пожалуйста, попробуйте другое изображение или свяжитесь с разработчиками.</string>
<!-- Images - chat.simplex.app.views.chat.item.CIImageView.kt -->
<string name="image_descr">Изображение</string>
@@ -351,12 +355,15 @@
<string name="how_to_use_simplex_chat">Как использовать</string>
<string name="markdown_help">Форматирование сообщений</string>
<string name="markdown_in_messages">Форматирование сообщений</string>
<string name="chat_with_the_founder">Соединиться с разработчиками</string>
<string name="chat_with_the_founder">Отправьте вопросы и идеи</string>
<string name="send_us_an_email">Отправить email</string>
<string name="chat_lock">Блокировка SimpleX</string>
<string name="chat_console">Консоль</string>
<string name="smp_servers">SMP серверы</string>
<string name="install_simplex_chat_for_terminal"><xliff:g id="appNameFull">SimpleX Chat</xliff:g> для терминала</string>
<string name="star_on_github">Поставить звездочку в GitHub</string>
<string name="contribute">Внести свой вклад</string>
<string name="rate_the_app">Оценить приложение</string>
<string name="use_simplex_chat_servers__question">Использовать серверы предосталенные <xliff:g id="appNameFull">SimpleX Chat</xliff:g>?</string>
<string name="saved_SMP_servers_will_be_removed">Сохраненные SMP серверы будут удалены.</string>
<string name="your_SMP_servers">Ваши SMP серверы</string>
@@ -560,6 +567,7 @@
<string name="settings_section_title_you">ВЫ</string>
<string name="settings_section_title_settings">НАСТРОЙКИ</string>
<string name="settings_section_title_help">ПОМОЩЬ</string>
<string name="settings_section_title_support">ПОДДЕРЖАТЬ SIMPLEX CHAT</string>
<string name="settings_section_title_develop">ДЛЯ РАЗРАБОТЧИКОВ</string>
<string name="settings_section_title_device">УСТРОЙСТВО</string>
<string name="settings_section_title_chats">ЧАТЫ</string>

View File

@@ -6,7 +6,9 @@
<!-- Connect via Link - MainActivity.kt -->
<string name="connect_via_contact_link">Connect via contact link?</string>
<string name="connect_via_invitation_link">Connect via invitation link?</string>
<string name="connect_via_group_link">Connect via group link?</string>
<string name="profile_will_be_sent_to_contact_sending_link">Your profile will be sent to the contact that you received this link from.</string>
<string name="you_will_join_group">You will join a group this link refers to and connect to its group members.</string>
<string name="connect_via_link_verb">Connect</string>
<!-- Server info - ChatModel.kt -->
@@ -56,7 +58,7 @@
<string name="error_receiving_file">Error receiving file</string>
<string name="error_creating_address">Error creating address</string>
<string name="contact_already_exists">Contact already exists</string>
<string name="you_are_already_connected_to_vName_via_this_link">You are already connected to <xliff:g id="contactName" example="Alice">%1$s!</xliff:g> via this link.</string>
<string name="you_are_already_connected_to_vName_via_this_link">You are already connected to <xliff:g id="contactName" example="Alice">%1$s!</xliff:g>.</string>
<string name="invalid_connection_link">Invalid connection link</string>
<string name="please_check_correct_link_and_maybe_ask_for_a_new_one">Please check that you used the correct link or ask your contact to send you another one.</string>
<string name="connection_error_auth">Connection error (AUTH)</string>
@@ -129,16 +131,16 @@
<string name="auth_enable_simplex_lock">Enable SimpleX Lock</string>
<string name="auth_disable_simplex_lock">Disable SimpleX Lock</string>
<string name="auth_confirm_credential">Confirm your credential</string>
<string name="auth_error">Authentication error</string>
<string name="auth_error_w_desc">Authentication error: <xliff:g id="desc">%1$s</xliff:g></string>
<string name="auth_failed">Authentication failed</string>
<string name="auth_unavailable">Authentication unavailable</string>
<string name="auth_device_authentication_is_not_enabled_you_can_turn_on_in_settings_once_enabled">Device authentication is not enabled. You can turn on SimpleX Lock via Settings, once you enable device authentication.</string>
<string name="auth_device_authentication_is_disabled_turning_off">Device authentication is disabled. Turning off SimpleX Lock.</string>
<string name="auth_retry">Retry</string>
<string name="auth_stop_chat">Stop chat</string>
<string name="auth_open_chat_console">Open chat console</string>
<!-- Chat Alerts - ChatItemView.kt -->
<string name="message_delivery_error_title">Message delivery error</string>
<string name="message_delivery_error_desc">Most likely this contact has deleted the connection with you.</string>
<!-- Chat Actions - ChatItemView.kt (and general) -->
<string name="reply_verb">Reply</string>
<string name="share_verb">Share</string>
@@ -183,6 +185,8 @@
<string name="icon_descr_cancel_file_preview">Cancel file preview</string>
<string name="images_limit_title">Too many images!</string>
<string name="images_limit_desc">Only 10 images can be sent at the same time</string>
<string name="image_decoding_exception_title">Decoding error</string>
<string name="image_decoding_exception_desc">The image cannot be decoded. Please, try a different image or contact developers.</string>
<!-- Images - chat.simplex.app.views.chat.item.CIImageView.kt -->
<string name="image_descr">Image</string>
@@ -354,12 +358,15 @@
<string name="how_to_use_simplex_chat">How to use it</string>
<string name="markdown_help">Markdown help</string>
<string name="markdown_in_messages">Markdown in messages</string>
<string name="chat_with_the_founder">Connect to the developers</string>
<string name="chat_with_the_founder">Send questions and ideas</string>
<string name="send_us_an_email">Send us email</string>
<string name="chat_lock">SimpleX Lock</string>
<string name="chat_console">Chat console</string>
<string name="smp_servers">SMP servers</string>
<string name="install_simplex_chat_for_terminal">Install <xliff:g id="appNameFull">SimpleX Chat</xliff:g> for terminal</string>
<string name="star_on_github">Star on GitHub</string>
<string name="contribute">Contribute</string>
<string name="rate_the_app">Rate the app</string>
<string name="use_simplex_chat_servers__question">Use <xliff:g id="appNameFull">SimpleX Chat</xliff:g> servers?</string>
<string name="saved_SMP_servers_will_be_removed">Saved SMP servers will be removed.</string>
<string name="your_SMP_servers">Your SMP servers</string>
@@ -560,6 +567,7 @@
<string name="settings_section_title_you">YOU</string>
<string name="settings_section_title_settings">SETTINGS</string>
<string name="settings_section_title_help">HELP</string>
<string name="settings_section_title_support">SUPPORT SIMPLEX CHAT</string>
<string name="settings_section_title_develop">DEVELOP</string>
<string name="settings_section_title_device">DEVICE</string>
<string name="settings_section_title_chats">CHATS</string>

View File

@@ -102,9 +102,11 @@ class AppDelegate: NSObject, UIApplicationDelegate {
class SceneDelegate: NSObject, ObservableObject, UIWindowSceneDelegate {
var window: UIWindow?
var windowScene: UIWindowScene?
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
guard let windowScene = scene as? UIWindowScene else { return }
self.windowScene = windowScene
window = windowScene.keyWindow
window?.tintColor = UIColor(cgColor: getUIAccentColorDefault())
window?.overrideUserInterfaceStyle = getUserInterfaceStyleDefault()

View File

@@ -70,6 +70,7 @@ struct ContentView: View {
dismissAllSheets(animated: false) {
justAuthenticate()
}
chatModel.chatId = nil
}
}

View File

@@ -386,6 +386,11 @@ final class ChatModel: ObservableObject {
}
func upsertGroupMember(_ groupInfo: GroupInfo, _ member: GroupMember) -> Bool {
// user member was updated
if groupInfo.membership.groupMemberId == member.groupMemberId {
updateGroup(groupInfo)
return false
}
// update current chat
if chatId == groupInfo.id {
if let i = groupMembers.firstIndex(where: { $0.id == member.id }) {

View File

@@ -379,9 +379,13 @@ func apiConnect(connReq: String) async -> ConnReqType? {
case .sentConfirmation: return .invitation
case .sentInvitation: return .contact
case let .contactAlreadyExists(contact):
let m = ChatModel.shared
if let c = m.getContactChat(contact.contactId) {
await MainActor.run { m.chatId = c.id }
}
am.showAlertMsg(
title: "Contact already exists",
message: "You are already connected to \(contact.displayName) via this link."
message: "You are already connected to \(contact.displayName)."
)
return nil
case .chatCmdError(.error(.invalidConnReq)):
@@ -986,10 +990,7 @@ func processReceivedMsg(_ res: ChatResponse) async {
_ = m.upsertChatItem(cInfo, cItem)
}
case let .receivedGroupInvitation(groupInfo, _, _):
m.addChat(Chat(
chatInfo: .group(groupInfo: groupInfo),
chatItems: []
))
m.updateGroup(groupInfo) // update so that repeat group invitations are not duplicated
// NtfManager.shared.notifyContactRequest(contactRequest) // TODO notifyGroupInvitation?
case let .userAcceptedGroupSent(groupInfo, hostContact):
m.updateGroup(groupInfo)

View File

@@ -392,8 +392,11 @@ struct ChatView: View {
await MainActor.run { selectedMember = member }
}
}
.sheet(item: $selectedMember, onDismiss: { memberConnectionStats = nil }) { member in
GroupMemberInfoView(groupInfo: groupInfo, member: member, connectionStats: $memberConnectionStats)
.sheet(item: $selectedMember, onDismiss: {
selectedMember = nil
memberConnectionStats = nil
}) { _ in
GroupMemberInfoView(groupInfo: groupInfo, member: $selectedMember, connectionStats: $memberConnectionStats)
}
} else {
Rectangle().fill(.clear)

View File

@@ -71,8 +71,11 @@ struct GroupChatInfoView: View {
.sheet(isPresented: $showAddMembersSheet) {
AddGroupMembersView(chat: chat, groupInfo: groupInfo)
}
.sheet(item: $selectedMember, onDismiss: { connectionStats = nil }) { member in
GroupMemberInfoView(groupInfo: groupInfo, member: member, connectionStats: $connectionStats)
.sheet(item: $selectedMember, onDismiss: {
selectedMember = nil
connectionStats = nil
}) { _ in
GroupMemberInfoView(groupInfo: groupInfo, member: $selectedMember, connectionStats: $connectionStats)
}
.sheet(isPresented: $showGroupProfile) {
GroupProfileView(groupId: groupInfo.apiId, groupProfile: groupInfo.groupProfile)

View File

@@ -13,22 +13,22 @@ struct GroupMemberInfoView: View {
@EnvironmentObject var chatModel: ChatModel
@Environment(\.dismiss) var dismiss: DismissAction
var groupInfo: GroupInfo
@State var member: GroupMember
@Binding var member: GroupMember?
@Binding var connectionStats: ConnectionStats?
@State private var newRole: GroupMemberRole = .member
@State private var alert: GroupMemberInfoViewAlert?
@AppStorage(DEFAULT_DEVELOPER_TOOLS) private var developerTools = false
enum GroupMemberInfoViewAlert: Identifiable {
case removeMemberAlert
case changeMemberRoleAlert(role: GroupMemberRole)
case removeMemberAlert(mem: GroupMember)
case changeMemberRoleAlert(mem: GroupMember, role: GroupMemberRole)
case switchAddressAlert
case error(title: LocalizedStringKey, error: LocalizedStringKey)
var id: String {
switch self {
case .removeMemberAlert: return "removeMemberAlert"
case let .changeMemberRoleAlert(role): return "changeMemberRoleAlert \(role.rawValue)"
case let .changeMemberRoleAlert(_, role): return "changeMemberRoleAlert \(role.rawValue)"
case .switchAddressAlert: return "switchAddressAlert"
case let .error(title, _): return "error \(title)"
}
@@ -37,81 +37,77 @@ struct GroupMemberInfoView: View {
var body: some View {
NavigationView {
List {
groupMemberInfoHeader()
.listRowBackground(Color.clear)
if let member = member {
List {
groupMemberInfoHeader(member)
.listRowBackground(Color.clear)
if let contactId = member.memberContactId {
Section {
openDirectChatButton(contactId)
if let contactId = member.memberContactId {
Section {
openDirectChatButton(contactId)
}
}
}
Section("Member") {
infoRow("Group", groupInfo.displayName)
Section("Member") {
infoRow("Group", groupInfo.displayName)
HStack {
if let roles = member.canChangeRoleTo(groupInfo: groupInfo) {
Picker("Change role", selection: $newRole) {
ForEach(roles) { role in
Text(role.text)
.foregroundStyle(.secondary)
}
}
} else {
Text("Role")
Spacer()
Text(member.memberRole.text)
.foregroundStyle(.secondary)
infoRow("Role", member.memberRole.text)
}
}
.onAppear { newRole = member.memberRole }
.onChange(of: newRole) { _ in
if newRole != member.memberRole {
alert = .changeMemberRoleAlert(role: newRole)
// TODO invited by - need to get contact by contact id
if let conn = member.activeConn {
let connLevelDesc = conn.connLevel == 0 ? NSLocalizedString("direct", comment: "connection level description") : String.localizedStringWithFormat(NSLocalizedString("indirect (%d)", comment: "connection level description"), conn.connLevel)
infoRow("Connection", connLevelDesc)
}
}
// TODO invited by - need to get contact by contact id
if let conn = member.activeConn {
let connLevelDesc = conn.connLevel == 0 ? NSLocalizedString("direct", comment: "connection level description") : String.localizedStringWithFormat(NSLocalizedString("indirect (%d)", comment: "connection level description"), conn.connLevel)
infoRow("Connection", connLevelDesc)
Section("Servers") {
// TODO network connection status
if developerTools {
Button("Change receiving address (BETA)") {
alert = .switchAddressAlert
}
}
if let connStats = connectionStats {
smpServers("Receiving via", connStats.rcvServers)
smpServers("Sending via", connStats.sndServers)
}
}
if member.canBeRemoved(groupInfo: groupInfo) {
Section {
removeMemberButton(member)
}
}
}
Section("Servers") {
// TODO network connection status
if developerTools {
Button("Change receiving address (BETA)") {
alert = .switchAddressAlert
Section("For console") {
infoRow("Local name", member.localDisplayName)
infoRow("Database ID", "\(member.groupMemberId)")
}
}
if let connStats = connectionStats {
smpServers("Receiving via", connStats.rcvServers)
smpServers("Sending via", connStats.sndServers)
}
}
if member.canBeRemoved(groupInfo: groupInfo) {
Section {
removeMemberButton()
}
}
if developerTools {
Section("For console") {
infoRow("Local name", member.localDisplayName)
infoRow("Database ID", "\(member.groupMemberId)")
.navigationBarHidden(true)
.onAppear { newRole = member.memberRole }
.onChange(of: newRole) { _ in
if newRole != member.memberRole {
alert = .changeMemberRoleAlert(mem: member, role: newRole)
}
}
}
.navigationBarHidden(true)
}
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top)
.alert(item: $alert) { alertItem in
switch(alertItem) {
case .removeMemberAlert: return removeMemberAlert()
case .changeMemberRoleAlert: return changeMemberRoleAlert()
case let .removeMemberAlert(mem): return removeMemberAlert(mem)
case let .changeMemberRoleAlert(mem, _): return changeMemberRoleAlert(mem)
case .switchAddressAlert: return switchAddressAlert(switchMemberAddress)
case let .error(title, error): return Alert(title: Text(title), message: Text(error))
}
@@ -142,18 +138,18 @@ struct GroupMemberInfoView: View {
}
}
private func groupMemberInfoHeader() -> some View {
private func groupMemberInfoHeader(_ mem: GroupMember) -> some View {
VStack {
ProfileImage(imageStr: member.image, color: Color(uiColor: .tertiarySystemFill))
ProfileImage(imageStr: mem.image, color: Color(uiColor: .tertiarySystemFill))
.frame(width: 192, height: 192)
.padding(.top, 12)
.padding()
Text(member.displayName)
Text(mem.displayName)
.font(.largeTitle)
.lineLimit(1)
.padding(.bottom, 2)
if member.fullName != "" && member.fullName != member.displayName {
Text(member.fullName)
if mem.fullName != "" && mem.fullName != mem.displayName {
Text(mem.fullName)
.font(.title2)
.lineLimit(2)
}
@@ -161,25 +157,25 @@ struct GroupMemberInfoView: View {
.frame(maxWidth: .infinity, alignment: .center)
}
func removeMemberButton() -> some View {
func removeMemberButton(_ mem: GroupMember) -> some View {
Button(role: .destructive) {
alert = .removeMemberAlert
alert = .removeMemberAlert(mem: mem)
} label: {
Label("Remove member", systemImage: "trash")
.foregroundColor(Color.red)
}
}
private func removeMemberAlert() -> Alert {
private func removeMemberAlert(_ mem: GroupMember) -> Alert {
Alert(
title: Text("Remove member?"),
message: Text("Member will be removed from group - this cannot be undone!"),
primaryButton: .destructive(Text("Remove")) {
Task {
do {
let member = try await apiRemoveMember(groupInfo.groupId, member.groupMemberId)
let updatedMember = try await apiRemoveMember(groupInfo.groupId, mem.groupMemberId)
await MainActor.run {
_ = chatModel.upsertGroupMember(groupInfo, member)
_ = chatModel.upsertGroupMember(groupInfo, updatedMember)
dismiss()
}
} catch let error {
@@ -193,20 +189,21 @@ struct GroupMemberInfoView: View {
)
}
private func changeMemberRoleAlert() -> Alert {
private func changeMemberRoleAlert(_ mem: GroupMember) -> Alert {
Alert(
title: Text("Change member role?"),
message: member.memberCurrent ? Text("Member role will be changed to \"\(newRole.text)\". All group members will be notified.") : Text("Member role will be changed to \"\(newRole.text)\". The member will receive a new invitation."),
message: mem.memberCurrent ? Text("Member role will be changed to \"\(newRole.text)\". All group members will be notified.") : Text("Member role will be changed to \"\(newRole.text)\". The member will receive a new invitation."),
primaryButton: .default(Text("Change")) {
Task {
do {
let mem = try await apiMemberRole(groupInfo.groupId, member.groupMemberId, newRole)
let updatedMember = try await apiMemberRole(groupInfo.groupId, mem.groupMemberId, newRole)
await MainActor.run {
member = mem
_ = chatModel.upsertGroupMember(groupInfo, mem)
member = updatedMember
_ = chatModel.upsertGroupMember(groupInfo, updatedMember)
}
} catch let error {
newRole = member.memberRole
newRole = mem.memberRole
logger.error("apiMemberRole error: \(responseError(error))")
let a = getErrorAlert(error, "Error changing role")
alert = .error(title: a.title, error: a.message)
@@ -214,7 +211,7 @@ struct GroupMemberInfoView: View {
}
},
secondaryButton: .cancel {
newRole = member.memberRole
newRole = mem.memberRole
}
)
}
@@ -222,7 +219,9 @@ struct GroupMemberInfoView: View {
private func switchMemberAddress() {
Task {
do {
try await apiSwitchGroupMember(groupInfo.apiId, member.groupMemberId)
if let member = member {
try await apiSwitchGroupMember(groupInfo.apiId, member.groupMemberId)
}
} catch let error {
logger.error("switchMemberAddress apiSwitchGroupMember error: \(responseError(error))")
let a = getErrorAlert(error, "Error changing address")
@@ -238,7 +237,7 @@ struct GroupMemberInfoView_Previews: PreviewProvider {
static var previews: some View {
GroupMemberInfoView(
groupInfo: GroupInfo.sampleData,
member: GroupMember.sampleData,
member: Binding.constant(GroupMember.sampleData),
connectionStats: Binding.constant(nil)
)
}

View File

@@ -56,6 +56,7 @@ struct AddContactView: View {
.padding()
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top)
}
.onAppear { chatModel.connReqInv = connReqInvitation }
}
}

View File

@@ -78,25 +78,25 @@ func connectViaLink(_ connectionLink: String, _ dismiss: DismissAction? = nil) {
}
}
struct CRData: Decodable {
struct CReqClientData: Decodable {
var type: String
var groupLinkId: String?
}
func parseLinkQueryData(_ connectionLink: String) -> CRData? {
func parseLinkQueryData(_ connectionLink: String) -> CReqClientData? {
if let hashIndex = connectionLink.firstIndex(of: "#"),
let urlQuery = URL(string: String(connectionLink[connectionLink.index(after: hashIndex)...])),
let components = URLComponents(url: urlQuery, resolvingAgainstBaseURL: false),
let data = components.queryItems?.first(where: { $0.name == "data" })?.value,
let d = data.data(using: .utf8),
let crData = try? getJSONDecoder().decode(CRData.self, from: d) {
let crData = try? getJSONDecoder().decode(CReqClientData.self, from: d) {
return crData
} else {
return nil
}
}
func checkCRDataGroup(_ crData: CRData) -> Bool {
func checkCRDataGroup(_ crData: CReqClientData) -> Bool {
return crData.type == "group" && crData.groupLinkId != nil
}

View File

@@ -7,6 +7,7 @@
//
import SwiftUI
import StoreKit
import SimpleXChat
let simplexTeamURL = URL(string: "simplex:/contact#/?v=1&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2FK1rslx-m5bpXVIdMZg9NLUZ_8JBm8xTt%23MCowBQYDK2VuAyEALDeVe-sG8mRY22LsXlPgiwTNs9dbiLrNuA7f3ZMAJ2w%3D")!
@@ -70,6 +71,7 @@ func setGroupDefaults() {
struct SettingsView: View {
@Environment(\.colorScheme) var colorScheme
@EnvironmentObject var chatModel: ChatModel
@EnvironmentObject var sceneDelegate: SceneDelegate
@Binding var showSettings: Bool
@AppStorage(DEFAULT_DEVELOPER_TOOLS) private var developerTools = false
@State private var settingsSheet: SettingsSheet?
@@ -179,36 +181,55 @@ struct SettingsView: View {
settingsRow("textformat") { Text("Markdown in messages") }
}
settingsRow("number") {
Button {
Button("Send questions and ideas") {
showSettings = false
DispatchQueue.main.async {
UIApplication.shared.open(simplexTeamURL)
}
} label: {
Text("Chat with the developers")
}
}
.disabled(chatModel.chatRunning != true)
settingsRow("envelope") { Text("[Send us email](mailto:chat@simplex.chat)") }
}
Section("Develop") {
NavigationLink {
TerminalView()
} label: {
settingsRow("terminal") { Text("Chat console") }
}
settingsRow("chevron.left.forwardslash.chevron.right") {
Toggle("Developer tools", isOn: $developerTools)
Section("Support SimpleX Chat") {
settingsRow("keyboard") { Text("[Contribute](https://github.com/simplex-chat/simplex-chat#contribute)") }
settingsRow("star") {
Button("Rate the app") {
if let scene = sceneDelegate.windowScene {
SKStoreReviewController.requestReview(in: scene)
}
}
}
ZStack(alignment: .leading) {
Image(colorScheme == .dark ? "github_light" : "github")
.resizable()
.frame(width: 24, height: 24)
.opacity(0.5)
Text("Install [SimpleX Chat for terminal](https://github.com/simplex-chat/simplex-chat)")
Text("[Star on GitHub](https://github.com/simplex-chat/simplex-chat)")
.padding(.leading, indent)
}
}
Section("Develop") {
settingsRow("chevron.left.forwardslash.chevron.right") {
Toggle("Developer tools", isOn: $developerTools)
}
if developerTools {
NavigationLink {
TerminalView()
} label: {
settingsRow("terminal") { Text("Chat console") }
}
ZStack(alignment: .leading) {
Image(colorScheme == .dark ? "github_light" : "github")
.resizable()
.frame(width: 24, height: 24)
.opacity(0.5)
Text("Install [SimpleX Chat for terminal](https://github.com/simplex-chat/simplex-chat)")
.padding(.leading, indent)
}
}
// NavigationLink {
// ExperimentalFeaturesView()
// .navigationTitle("Experimental features")

View File

@@ -380,12 +380,12 @@
</trans-unit>
<trans-unit id="Change receiving address (BETA)" xml:space="preserve">
<source>Change receiving address (BETA)</source>
<target>***Change receiving address (BETA)</target>
<target>Wechseln der Empfängeradresse (BETA)</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Change receiving address?" xml:space="preserve">
<source>Change receiving address?</source>
<target>***Change receiving address?</target>
<target>Empfängeradresse wechseln?</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Change role" xml:space="preserve">
@@ -428,11 +428,6 @@
<target>Der Chat ist beendet</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Chat with the developers" xml:space="preserve">
<source>Chat with the developers</source>
<target>Chatten Sie mit den Entwicklern</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Chats" xml:space="preserve">
<source>Chats</source>
<target>Chats</target>
@@ -500,7 +495,7 @@
</trans-unit>
<trans-unit id="Connect via group link?" xml:space="preserve">
<source>Connect via group link?</source>
<target>***Connect via group link?</target>
<target>Über den Gruppen-Link verbinden?</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Connect via link" xml:space="preserve">
@@ -1038,7 +1033,7 @@
</trans-unit>
<trans-unit id="Error changing address" xml:space="preserve">
<source>Error changing address</source>
<target>***Error changing address</target>
<target>Fehler beim Wechseln der Adresse</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error changing role" xml:space="preserve">
@@ -1083,7 +1078,7 @@
</trans-unit>
<trans-unit id="Error deleting contact" xml:space="preserve">
<source>Error deleting contact</source>
<target>***Error deleting contact</target>
<target>Fehler beim Löschen des Kontakts</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error deleting database" xml:space="preserve">
@@ -1213,7 +1208,7 @@
</trans-unit>
<trans-unit id="File will be received when your contact is online, please wait or check later!" xml:space="preserve">
<source>File will be received when your contact is online, please wait or check later!</source>
<target>Die Datei wird empfangen, wenn Ihr Kontakt online ist, bitte warten oder schauen Sie später nochmal nach!</target>
<target>Die Datei wird empfangen, sobald Ihr Kontakt online ist. Bitte warten oder schauen Sie später nochmal nach!</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="File: %@" xml:space="preserve">
@@ -1363,7 +1358,7 @@
</trans-unit>
<trans-unit id="Image will be received when your contact is online, please wait or check later!" xml:space="preserve">
<source>Image will be received when your contact is online, please wait or check later!</source>
<target>Das Bild wird empfangen, sobald Ihr Kontakt online ist, bitte warten oder schauen Sie später nochmal nach!</target>
<target>Das Bild wird empfangen, sobald Ihr Kontakt online ist. Bitte warten oder schauen Sie später nochmal nach!</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Immune to spam and abuse" xml:space="preserve">
@@ -1790,7 +1785,7 @@ Wir werden Serverredundanzen hinzufügen, um verloren gegangene Nachrichten zu v
</trans-unit>
<trans-unit id="Open-source protocol and code anybody can run the servers." xml:space="preserve">
<source>Open-source protocol and code anybody can run the servers.</source>
<target>Open-Source-Protokoll und -Code Jeder kann seine eigenen Server aufsetzen und nutzen.</target>
<target>Open-Source-Protokoll und -Code Jede Person kann ihre eigenen Server aufsetzen und nutzen.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="PING interval" xml:space="preserve">
@@ -1845,7 +1840,7 @@ Wir werden Serverredundanzen hinzufügen, um verloren gegangene Nachrichten zu v
</trans-unit>
<trans-unit id="Please enter the previous password after restoring database backup. This action can not be undone." xml:space="preserve">
<source>Please enter the previous password after restoring database backup. This action can not be undone.</source>
<target>Bitte geben Sie das vorherige Passwort ein, nachdem Sie die Datenbanksicherung wiederhergestellt haben. Diese Aktion kann nicht rückgängig gemacht werden.</target>
<target>Bitte geben Sie das vorherige Passwort ein, nachdem Sie die Datenbanksicherung wiederhergestellt haben. Diese Aktion kann nicht rückgängig gemacht werden!</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Please restart the app and migrate the database to enable push notifications." xml:space="preserve">
@@ -1888,6 +1883,11 @@ Wir werden Serverredundanzen hinzufügen, um verloren gegangene Nachrichten zu v
<target>Push-Benachrichtigungen</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Rate the app" xml:space="preserve">
<source>Rate the app</source>
<target>Bewerte die App</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Read" xml:space="preserve">
<source>Read</source>
<target>Lesen</target>
@@ -2103,6 +2103,11 @@ Wir werden Serverredundanzen hinzufügen, um verloren gegangene Nachrichten zu v
<target>Benachrichtigungen senden:</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Send questions and ideas" xml:space="preserve">
<source>Send questions and ideas</source>
<target>Senden Sie Fragen und Ideen</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Sender cancelled file transfer." xml:space="preserve">
<source>Sender cancelled file transfer.</source>
<target>Der Absender hat die Dateiübertragung abgebrochen.</target>
@@ -2248,6 +2253,11 @@ Wir werden Serverredundanzen hinzufügen, um verloren gegangene Nachrichten zu v
<target>Chat beenden?</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Support SimpleX Chat" xml:space="preserve">
<source>Support SimpleX Chat</source>
<target>Unterstützen SimpleX Chat</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="System" xml:space="preserve">
<source>System</source>
<target>System</target>
@@ -2370,22 +2380,22 @@ Wir werden Serverredundanzen hinzufügen, um verloren gegangene Nachrichten zu v
</trans-unit>
<trans-unit id="This action cannot be undone - all received and sent files and media will be deleted. Low resolution pictures will remain." xml:space="preserve">
<source>This action cannot be undone - all received and sent files and media will be deleted. Low resolution pictures will remain.</source>
<target>Diese Aktion kann nicht rückgängig gemacht werden - Alle empfangenen und gesendeten Dateien und Medien werden gelöscht. Bilder mit niedriger Auflösung bleiben erhalten.</target>
<target>Diese Aktion kann nicht rückgängig gemacht werden! Alle empfangenen und gesendeten Dateien und Medien werden gelöscht. Bilder mit niedriger Auflösung bleiben erhalten.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="This action cannot be undone - the messages sent and received earlier than selected will be deleted. It may take several minutes." xml:space="preserve">
<source>This action cannot be undone - the messages sent and received earlier than selected will be deleted. It may take several minutes.</source>
<target>Diese Aktion kann nicht rückgängig gemacht werden - Alle empfangenen und gesendeten Nachrichten, die über den ausgewählten Zeitraum hinaus gehen, werden gelöscht. Dies kann mehrere Minuten dauern.</target>
<target>Diese Aktion kann nicht rückgängig gemacht werden! Alle empfangenen und gesendeten Nachrichten, die über den ausgewählten Zeitraum hinaus gehen, werden gelöscht. Dieser Vorgang kann mehrere Minuten dauern.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="This action cannot be undone - your profile, contacts, messages and files will be irreversibly lost." xml:space="preserve">
<source>This action cannot be undone - your profile, contacts, messages and files will be irreversibly lost.</source>
<target>Diese Aktion kann nicht rückgängig gemacht werden - Ihr Profil und Ihre Kontakte, Nachrichten und Dateien gehen unwiderruflich verloren.</target>
<target>Diese Aktion kann nicht rückgängig gemacht werden! Ihr Profil und Ihre Kontakte, Nachrichten und Dateien gehen unwiderruflich verloren.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="This feature is experimental! It will only work if the other client has version 4.2 installed. You should see the message in the conversation once the address change is completed please check that you can still receive messages from this contact (or group member)." xml:space="preserve">
<source>This feature is experimental! It will only work if the other client has version 4.2 installed. You should see the message in the conversation once the address change is completed please check that you can still receive messages from this contact (or group member).</source>
<target>***This feature is experimental! It will only work if the other client has version 4.2 installed. You should see the message in the conversation once the address change is completed please check that you can still receive messages from this contact (or group member).</target>
<target>Diese Funktion ist experimentell! Sie wird nur funktionieren, wenn der Kontakt ebenfalls die SimpleX-Version 4.2 installiert hat. Sobald der Adress-Wechsel abgeschlossen ist, sollten Sie die Nachricht im Chat-Verlauf sehen. Bitte prüfen Sie, ob Sie weiterhin Nachrichten von diesem Kontakt (oder Gruppenmitglied) empfangen können.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="This group no longer exists." xml:space="preserve">
@@ -2432,7 +2442,7 @@ Sie werden aufgefordert, die Authentifizierung abzuschließen, bevor diese Funkt
</trans-unit>
<trans-unit id="Transfer images faster (BETA)" xml:space="preserve">
<source>Transfer images faster (BETA)</source>
<target>***Transfer images faster (BETA)</target>
<target>Bilder schneller übertragen (BETA)</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Trying to connect to the server used to receive messages from this contact (error: %@)." xml:space="preserve">
@@ -2617,9 +2627,9 @@ Bitten Sie Ihren Kontakt darum einen weiteren Verbindungs-Link zu erzeugen, um s
<target>Sie haben die Verbindung akzeptiert</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="You are already connected to %@ via this link." xml:space="preserve">
<source>You are already connected to %@ via this link.</source>
<target>Sie sind bereits über diesen Link mit %@ verbunden.</target>
<trans-unit id="You are already connected to %@." xml:space="preserve">
<source>You are already connected to %@.</source>
<target>Sie sind bereits mit %@ verbunden.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="You are connected to the server used to receive messages from this contact." xml:space="preserve">
@@ -2654,7 +2664,7 @@ Bitten Sie Ihren Kontakt darum einen weiteren Verbindungs-Link zu erzeugen, um s
</trans-unit>
<trans-unit id="You can share your address as a link or as a QR code - anybody will be able to connect to you. You won't lose your contacts if you later delete it." xml:space="preserve">
<source>You can share your address as a link or as a QR code - anybody will be able to connect to you. You won't lose your contacts if you later delete it.</source>
<target>Sie können Ihre Adresse als Link oder als QR-Code teilen Jeder kann sich darüber mit Ihnen verbinden. Sie werden Ihre mit dieser Adresse verbundenen Kontakte nicht verlieren, wenn Sie diese Adresse später löschen.</target>
<target>Sie können Ihre Adresse als Link oder als QR-Code teilen Jede Person kann sich darüber mit Ihnen verbinden. Sie werden Ihre mit dieser Adresse verbundenen Kontakte nicht verlieren, wenn Sie diese Adresse später löschen.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="You can start chat via app Settings / Database or by restarting the app" xml:space="preserve">
@@ -2719,17 +2729,17 @@ Bitten Sie Ihren Kontakt darum einen weiteren Verbindungs-Link zu erzeugen, um s
</trans-unit>
<trans-unit id="You will be connected to group when the group host's device is online, please wait or check later!" xml:space="preserve">
<source>You will be connected to group when the group host's device is online, please wait or check later!</source>
<target>***You will be connected to group when the group host's device is online, please wait or check later!</target>
<target>Sie werden mit der Gruppe verbunden, sobald das Endgerät des Gruppen-Hosts online ist. Bitte warten oder schauen Sie später nochmal nach!</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="You will be connected when your connection request is accepted, please wait or check later!" xml:space="preserve">
<source>You will be connected when your connection request is accepted, please wait or check later!</source>
<target>Sie werden verbunden, sobald Ihre Verbindungsanfrage akzeptiert wird, bitte warten oder schauen Sie später nochmal nach!</target>
<target>Sie werden verbunden, sobald Ihre Verbindungsanfrage akzeptiert wird. Bitte warten oder schauen Sie später nochmal nach!</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="You will be connected when your contact's device is online, please wait or check later!" xml:space="preserve">
<source>You will be connected when your contact's device is online, please wait or check later!</source>
<target>Sie werden verbunden, wenn das Gerät Ihres Kontakts online ist, bitte warten oder schauen Sie später nochmal nach!</target>
<target>Sie werden verbunden, sobald das Endgerät Ihres Kontakts online ist. Bitte warten oder schauen Sie später nochmal nach!</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="You will be required to authenticate when you start or resume the app after 30 seconds in background." xml:space="preserve">
@@ -2739,7 +2749,7 @@ Bitten Sie Ihren Kontakt darum einen weiteren Verbindungs-Link zu erzeugen, um s
</trans-unit>
<trans-unit id="You will join a group this link refers to and connect to its group members." xml:space="preserve">
<source>You will join a group this link refers to and connect to its group members.</source>
<target>***You will join a group this link refers to and connect to its group members.</target>
<target>Sie werden der Gruppe beitreten, auf die sich dieser Link bezieht und sich mit deren Gruppenmitgliedern verbinden.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="You will stop receiving messages from this group. Chat history will be preserved." xml:space="preserve">
@@ -2866,11 +2876,21 @@ SimpleX-Server können Ihr Profil nicht einsehen.</target>
<target>Meine Einstellungen</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="[Contribute](https://github.com/simplex-chat/simplex-chat#contribute)" xml:space="preserve">
<source>[Contribute](https://github.com/simplex-chat/simplex-chat#contribute)</source>
<target>[Beitragen](https://github.com/simplex-chat/simplex-chat#contribute)</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="[Send us email](mailto:chat@simplex.chat)" xml:space="preserve">
<source>[Send us email](mailto:chat@simplex.chat)</source>
<target>[Senden Sie uns eine E-Mail](mailto:chat@simplex.chat)</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="[Star on GitHub](https://github.com/simplex-chat/simplex-chat)" xml:space="preserve">
<source>[Star on GitHub](https://github.com/simplex-chat/simplex-chat)</source>
<target>[Stern auf GitHub](https://github.com/simplex-chat/simplex-chat)</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="_italic_" xml:space="preserve">
<source>\_italic_</source>
<target>\_kursiv_</target>
@@ -2933,7 +2953,7 @@ SimpleX-Server können Ihr Profil nicht einsehen.</target>
</trans-unit>
<trans-unit id="changed address for you" xml:space="preserve">
<source>changed address for you</source>
<target>***changed address for you</target>
<target>wechselte die Adresse für Sie</target>
<note>chat item text</note>
</trans-unit>
<trans-unit id="changed role of %@ to %@" xml:space="preserve">
@@ -2948,12 +2968,12 @@ SimpleX-Server können Ihr Profil nicht einsehen.</target>
</trans-unit>
<trans-unit id="changing address for %@..." xml:space="preserve">
<source>changing address for %@...</source>
<target>***changing address for %@...</target>
<target>Wechseln der Adresse für %@ ...</target>
<note>chat item text</note>
</trans-unit>
<trans-unit id="changing address..." xml:space="preserve">
<source>changing address...</source>
<target>***changing address...</target>
<target>Wechseln der Adresse ...</target>
<note>chat item text</note>
</trans-unit>
<trans-unit id="colored" xml:space="preserve">
@@ -3098,12 +3118,12 @@ SimpleX-Server können Ihr Profil nicht einsehen.</target>
</trans-unit>
<trans-unit id="incognito via contact address link" xml:space="preserve">
<source>incognito via contact address link</source>
<target>Inkognito über einen Kontaktadressen Link</target>
<target>Inkognito über einen Kontaktadressen-Link</target>
<note>chat list item description</note>
</trans-unit>
<trans-unit id="incognito via group link" xml:space="preserve">
<source>incognito via group link</source>
<target>***incognito via group link</target>
<target>Inkognito über einen Gruppen-Link</target>
<note>chat list item description</note>
</trans-unit>
<trans-unit id="incognito via one-time link" xml:space="preserve">
@@ -3153,7 +3173,7 @@ SimpleX-Server können Ihr Profil nicht einsehen.</target>
</trans-unit>
<trans-unit id="left" xml:space="preserve">
<source>left</source>
<target>verlassen</target>
<target>hat die Gruppe verlassen</target>
<note>rcv group event chat item</note>
</trans-unit>
<trans-unit id="member" xml:space="preserve">
@@ -3163,7 +3183,7 @@ SimpleX-Server können Ihr Profil nicht einsehen.</target>
</trans-unit>
<trans-unit id="member connected" xml:space="preserve">
<source>connected</source>
<target>beigetreten</target>
<target>ist der Gruppe beigetreten</target>
<note>rcv group event chat item</note>
</trans-unit>
<trans-unit id="message received" xml:space="preserve">
@@ -3278,12 +3298,12 @@ SimpleX-Server können Ihr Profil nicht einsehen.</target>
</trans-unit>
<trans-unit id="via contact address link" xml:space="preserve">
<source>via contact address link</source>
<target>über einen Kontaktadressen Link</target>
<target>über einen Kontaktadressen-Link</target>
<note>chat list item description</note>
</trans-unit>
<trans-unit id="via group link" xml:space="preserve">
<source>via group link</source>
<target>***via group link</target>
<target>über einen Gruppen-Link</target>
<note>chat list item description</note>
</trans-unit>
<trans-unit id="via one-time link" xml:space="preserve">
@@ -3323,12 +3343,12 @@ SimpleX-Server können Ihr Profil nicht einsehen.</target>
</trans-unit>
<trans-unit id="you changed address" xml:space="preserve">
<source>you changed address</source>
<target>***you changed address</target>
<target>Sie haben die Adresse gewechselt</target>
<note>chat item text</note>
</trans-unit>
<trans-unit id="you changed address for %@" xml:space="preserve">
<source>you changed address for %@</source>
<target>***you changed address for %@</target>
<target>Sie haben die Adresse für %@ gewechselt</target>
<note>chat item text</note>
</trans-unit>
<trans-unit id="you changed role for yourself to %@" xml:space="preserve">

View File

@@ -428,11 +428,6 @@
<target>Chat is stopped</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Chat with the developers" xml:space="preserve">
<source>Chat with the developers</source>
<target>Chat with the developers</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Chats" xml:space="preserve">
<source>Chats</source>
<target>Chats</target>
@@ -1888,6 +1883,11 @@ We will be adding server redundancy to prevent lost messages.</target>
<target>Push notifications</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Rate the app" xml:space="preserve">
<source>Rate the app</source>
<target>Rate the app</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Read" xml:space="preserve">
<source>Read</source>
<target>Read</target>
@@ -2103,6 +2103,11 @@ We will be adding server redundancy to prevent lost messages.</target>
<target>Send notifications:</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Send questions and ideas" xml:space="preserve">
<source>Send questions and ideas</source>
<target>Send questions and ideas</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Sender cancelled file transfer." xml:space="preserve">
<source>Sender cancelled file transfer.</source>
<target>Sender cancelled file transfer.</target>
@@ -2248,6 +2253,11 @@ We will be adding server redundancy to prevent lost messages.</target>
<target>Stop chat?</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Support SimpleX Chat" xml:space="preserve">
<source>Support SimpleX Chat</source>
<target>Support SimpleX Chat</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="System" xml:space="preserve">
<source>System</source>
<target>System</target>
@@ -2617,9 +2627,9 @@ To connect, please ask your contact to create another connection link and check
<target>You accepted connection</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="You are already connected to %@ via this link." xml:space="preserve">
<source>You are already connected to %@ via this link.</source>
<target>You are already connected to %@ via this link.</target>
<trans-unit id="You are already connected to %@." xml:space="preserve">
<source>You are already connected to %@.</source>
<target>You are already connected to %@.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="You are connected to the server used to receive messages from this contact." xml:space="preserve">
@@ -2866,11 +2876,21 @@ SimpleX servers cannot see your profile.</target>
<target>Your settings</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="[Contribute](https://github.com/simplex-chat/simplex-chat#contribute)" xml:space="preserve">
<source>[Contribute](https://github.com/simplex-chat/simplex-chat#contribute)</source>
<target>[Contribute](https://github.com/simplex-chat/simplex-chat#contribute)</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="[Send us email](mailto:chat@simplex.chat)" xml:space="preserve">
<source>[Send us email](mailto:chat@simplex.chat)</source>
<target>[Send us email](mailto:chat@simplex.chat)</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="[Star on GitHub](https://github.com/simplex-chat/simplex-chat)" xml:space="preserve">
<source>[Star on GitHub](https://github.com/simplex-chat/simplex-chat)</source>
<target>[Star on GitHub](https://github.com/simplex-chat/simplex-chat)</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="_italic_" xml:space="preserve">
<source>\_italic_</source>
<target>\_italic_</target>

View File

@@ -428,11 +428,6 @@
<target>Чат остановлен</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Chat with the developers" xml:space="preserve">
<source>Chat with the developers</source>
<target>Соединиться с разработчиками</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Chats" xml:space="preserve">
<source>Chats</source>
<target>Чаты</target>
@@ -1888,6 +1883,11 @@ We will be adding server redundancy to prevent lost messages.</source>
<target>Доставка уведомлений</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Rate the app" xml:space="preserve">
<source>Rate the app</source>
<target>Оценить приложение</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Read" xml:space="preserve">
<source>Read</source>
<target>Прочитано</target>
@@ -2103,6 +2103,11 @@ We will be adding server redundancy to prevent lost messages.</source>
<target>Отправлять уведомления:</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Send questions and ideas" xml:space="preserve">
<source>Send questions and ideas</source>
<target>Отправьте вопросы и идеи</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Sender cancelled file transfer." xml:space="preserve">
<source>Sender cancelled file transfer.</source>
<target>Отправитель отменил передачу файла.</target>
@@ -2248,6 +2253,11 @@ We will be adding server redundancy to prevent lost messages.</source>
<target>Остановить чат?</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Support SimpleX Chat" xml:space="preserve">
<source>Support SimpleX Chat</source>
<target>Поддержать SimpleX Chat</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="System" xml:space="preserve">
<source>System</source>
<target>Системная</target>
@@ -2617,9 +2627,9 @@ To connect, please ask your contact to create another connection link and check
<target>Вы приняли приглашение соединиться</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="You are already connected to %@ via this link." xml:space="preserve">
<source>You are already connected to %@ via this link.</source>
<target>Вы уже соединены с %@ через эту ссылку.</target>
<trans-unit id="You are already connected to %@." xml:space="preserve">
<source>You are already connected to %@.</source>
<target>Вы уже соединены с контактом %@.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="You are connected to the server used to receive messages from this contact." xml:space="preserve">
@@ -2866,11 +2876,21 @@ SimpleX серверы не могут получить доступ к ваше
<target>Настройки</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="[Contribute](https://github.com/simplex-chat/simplex-chat#contribute)" xml:space="preserve">
<source>[Contribute](https://github.com/simplex-chat/simplex-chat#contribute)</source>
<target>[Внести свой вклад](https://github.com/simplex-chat/simplex-chat#contribute)</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="[Send us email](mailto:chat@simplex.chat)" xml:space="preserve">
<source>[Send us email](mailto:chat@simplex.chat)</source>
<target>[Отправить email](mailto:chat@simplex.chat)</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="[Star on GitHub](https://github.com/simplex-chat/simplex-chat)" xml:space="preserve">
<source>[Star on GitHub](https://github.com/simplex-chat/simplex-chat)</source>
<target>[Поставить звездочку в GitHub](https://github.com/simplex-chat/simplex-chat)</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="_italic_" xml:space="preserve">
<source>\_italic_</source>
<target>\_курсив_</target>

View File

@@ -120,11 +120,11 @@
5CFE0921282EEAF60002594B /* ZoomableScrollView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CFE0920282EEAF60002594B /* ZoomableScrollView.swift */; };
5CFE0922282EEAF60002594B /* ZoomableScrollView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CFE0920282EEAF60002594B /* ZoomableScrollView.swift */; };
640F50E327CF991C001E05C2 /* SMPServers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 640F50E227CF991C001E05C2 /* SMPServers.swift */; };
6432855F290BEE2B00FBE5C8 /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 6432855A290BEE2B00FBE5C8 /* libffi.a */; };
64328560290BEE2B00FBE5C8 /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 6432855B290BEE2B00FBE5C8 /* libgmp.a */; };
64328561290BEE2B00FBE5C8 /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 6432855C290BEE2B00FBE5C8 /* libgmpxx.a */; };
64328562290BEE2B00FBE5C8 /* libHSsimplex-chat-4.2.0-LeYwSLlznBGLTzzJTly7Iu-ghc8.10.7.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 6432855D290BEE2B00FBE5C8 /* libHSsimplex-chat-4.2.0-LeYwSLlznBGLTzzJTly7Iu-ghc8.10.7.a */; };
64328563290BEE2B00FBE5C8 /* libHSsimplex-chat-4.2.0-LeYwSLlznBGLTzzJTly7Iu.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 6432855E290BEE2B00FBE5C8 /* libHSsimplex-chat-4.2.0-LeYwSLlznBGLTzzJTly7Iu.a */; };
64328569291CDEF200FBE5C8 /* libHSsimplex-chat-4.2.0-LMeXtL0v5gA4lEDweXVrvD.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64328564291CDEF200FBE5C8 /* libHSsimplex-chat-4.2.0-LMeXtL0v5gA4lEDweXVrvD.a */; };
6432856A291CDEF200FBE5C8 /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64328565291CDEF200FBE5C8 /* libgmpxx.a */; };
6432856B291CDEF200FBE5C8 /* libHSsimplex-chat-4.2.0-LMeXtL0v5gA4lEDweXVrvD-ghc8.10.7.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64328566291CDEF200FBE5C8 /* libHSsimplex-chat-4.2.0-LMeXtL0v5gA4lEDweXVrvD-ghc8.10.7.a */; };
6432856C291CDEF200FBE5C8 /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64328567291CDEF200FBE5C8 /* libffi.a */; };
6432856D291CDEF200FBE5C8 /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64328568291CDEF200FBE5C8 /* libgmp.a */; };
6440CA00288857A10062C672 /* CIEventView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6440C9FF288857A10062C672 /* CIEventView.swift */; };
6440CA03288AECA70062C672 /* AddGroupMembersView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6440CA02288AECA70062C672 /* AddGroupMembersView.swift */; };
6442E0BA287F169300CEC0F9 /* AddGroupView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6442E0B9287F169300CEC0F9 /* AddGroupView.swift */; };
@@ -319,11 +319,11 @@
5CFA59CF286477B400863A68 /* ChatArchiveView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatArchiveView.swift; sourceTree = "<group>"; };
5CFE0920282EEAF60002594B /* ZoomableScrollView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = ZoomableScrollView.swift; path = Shared/Views/ZoomableScrollView.swift; sourceTree = SOURCE_ROOT; };
640F50E227CF991C001E05C2 /* SMPServers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SMPServers.swift; sourceTree = "<group>"; };
6432855A290BEE2B00FBE5C8 /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = "<group>"; };
6432855B290BEE2B00FBE5C8 /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = "<group>"; };
6432855C290BEE2B00FBE5C8 /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = "<group>"; };
6432855D290BEE2B00FBE5C8 /* libHSsimplex-chat-4.2.0-LeYwSLlznBGLTzzJTly7Iu-ghc8.10.7.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-4.2.0-LeYwSLlznBGLTzzJTly7Iu-ghc8.10.7.a"; sourceTree = "<group>"; };
6432855E290BEE2B00FBE5C8 /* libHSsimplex-chat-4.2.0-LeYwSLlznBGLTzzJTly7Iu.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-4.2.0-LeYwSLlznBGLTzzJTly7Iu.a"; sourceTree = "<group>"; };
64328564291CDEF200FBE5C8 /* libHSsimplex-chat-4.2.0-LMeXtL0v5gA4lEDweXVrvD.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-4.2.0-LMeXtL0v5gA4lEDweXVrvD.a"; sourceTree = "<group>"; };
64328565291CDEF200FBE5C8 /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = "<group>"; };
64328566291CDEF200FBE5C8 /* libHSsimplex-chat-4.2.0-LMeXtL0v5gA4lEDweXVrvD-ghc8.10.7.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-4.2.0-LMeXtL0v5gA4lEDweXVrvD-ghc8.10.7.a"; sourceTree = "<group>"; };
64328567291CDEF200FBE5C8 /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = "<group>"; };
64328568291CDEF200FBE5C8 /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = "<group>"; };
6440C9FF288857A10062C672 /* CIEventView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CIEventView.swift; sourceTree = "<group>"; };
6440CA02288AECA70062C672 /* AddGroupMembersView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddGroupMembersView.swift; sourceTree = "<group>"; };
6442E0B9287F169300CEC0F9 /* AddGroupView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddGroupView.swift; sourceTree = "<group>"; };
@@ -374,13 +374,13 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
6432856A291CDEF200FBE5C8 /* libgmpxx.a in Frameworks */,
6432856D291CDEF200FBE5C8 /* libgmp.a in Frameworks */,
6432856C291CDEF200FBE5C8 /* libffi.a in Frameworks */,
5CE2BA93284534B000EC33A6 /* libiconv.tbd in Frameworks */,
64328561290BEE2B00FBE5C8 /* libgmpxx.a in Frameworks */,
64328562290BEE2B00FBE5C8 /* libHSsimplex-chat-4.2.0-LeYwSLlznBGLTzzJTly7Iu-ghc8.10.7.a in Frameworks */,
6432855F290BEE2B00FBE5C8 /* libffi.a in Frameworks */,
64328560290BEE2B00FBE5C8 /* libgmp.a in Frameworks */,
5CE2BA94284534BB00EC33A6 /* libz.tbd in Frameworks */,
64328563290BEE2B00FBE5C8 /* libHSsimplex-chat-4.2.0-LeYwSLlznBGLTzzJTly7Iu.a in Frameworks */,
64328569291CDEF200FBE5C8 /* libHSsimplex-chat-4.2.0-LMeXtL0v5gA4lEDweXVrvD.a in Frameworks */,
6432856B291CDEF200FBE5C8 /* libHSsimplex-chat-4.2.0-LMeXtL0v5gA4lEDweXVrvD-ghc8.10.7.a in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -435,11 +435,11 @@
5C764E5C279C70B7000C6508 /* Libraries */ = {
isa = PBXGroup;
children = (
6432855A290BEE2B00FBE5C8 /* libffi.a */,
6432855B290BEE2B00FBE5C8 /* libgmp.a */,
6432855C290BEE2B00FBE5C8 /* libgmpxx.a */,
6432855D290BEE2B00FBE5C8 /* libHSsimplex-chat-4.2.0-LeYwSLlznBGLTzzJTly7Iu-ghc8.10.7.a */,
6432855E290BEE2B00FBE5C8 /* libHSsimplex-chat-4.2.0-LeYwSLlznBGLTzzJTly7Iu.a */,
64328567291CDEF200FBE5C8 /* libffi.a */,
64328568291CDEF200FBE5C8 /* libgmp.a */,
64328565291CDEF200FBE5C8 /* libgmpxx.a */,
64328566291CDEF200FBE5C8 /* libHSsimplex-chat-4.2.0-LMeXtL0v5gA4lEDweXVrvD-ghc8.10.7.a */,
64328564291CDEF200FBE5C8 /* libHSsimplex-chat-4.2.0-LMeXtL0v5gA4lEDweXVrvD.a */,
);
path = Libraries;
sourceTree = "<group>";
@@ -1211,7 +1211,7 @@
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 88;
CURRENT_PROJECT_VERSION = 91;
DEVELOPMENT_TEAM = 5NN7GUYB6T;
ENABLE_BITCODE = NO;
ENABLE_PREVIEWS = YES;
@@ -1232,7 +1232,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 4.2;
MARKETING_VERSION = 4.2.1;
PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.app;
PRODUCT_NAME = SimpleX;
SDKROOT = iphoneos;
@@ -1253,7 +1253,7 @@
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 88;
CURRENT_PROJECT_VERSION = 91;
DEVELOPMENT_TEAM = 5NN7GUYB6T;
ENABLE_BITCODE = NO;
ENABLE_PREVIEWS = YES;
@@ -1274,7 +1274,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 4.2;
MARKETING_VERSION = 4.2.1;
PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.app;
PRODUCT_NAME = SimpleX;
SDKROOT = iphoneos;
@@ -1332,7 +1332,7 @@
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = "SimpleX NSE/SimpleX NSE.entitlements";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 88;
CURRENT_PROJECT_VERSION = 91;
DEVELOPMENT_TEAM = 5NN7GUYB6T;
ENABLE_BITCODE = NO;
GENERATE_INFOPLIST_FILE = YES;
@@ -1345,7 +1345,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 4.2;
MARKETING_VERSION = 4.2.1;
PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.app.SimpleX-NSE";
PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = iphoneos;
@@ -1362,7 +1362,7 @@
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = "SimpleX NSE/SimpleX NSE.entitlements";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 88;
CURRENT_PROJECT_VERSION = 91;
DEVELOPMENT_TEAM = 5NN7GUYB6T;
ENABLE_BITCODE = NO;
GENERATE_INFOPLIST_FILE = YES;
@@ -1375,7 +1375,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 4.2;
MARKETING_VERSION = 4.2.1;
PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.app.SimpleX-NSE";
PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = iphoneos;

View File

@@ -44,6 +44,7 @@
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
enableUBSanitizer = "YES"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"

View File

@@ -530,7 +530,7 @@ public enum ChatResponse: Decodable, Error {
case let .contactsList(contacts): return String(describing: contacts)
case let .groupCreated(groupInfo): return String(describing: groupInfo)
case let .sentGroupInvitation(groupInfo, contact, member): return "groupInfo: \(groupInfo)\ncontact: \(contact)\nmember: \(member)"
case let .userAcceptedGroupSent(groupInfo): return String(describing: groupInfo)
case let .userAcceptedGroupSent(groupInfo, hostContact): return "groupInfo: \(groupInfo)\nhostContact: \(String(describing: hostContact))"
case let .userDeletedMember(groupInfo, member): return "groupInfo: \(groupInfo)\nmember: \(member)"
case let .leftMemberUser(groupInfo): return String(describing: groupInfo)
case let .groupMembers(group): return String(describing: group)
@@ -634,16 +634,16 @@ public struct NetCfg: Codable, Equatable {
public static let defaults: NetCfg = NetCfg(
socksProxy: nil,
tcpConnectTimeout: 7_500_000,
tcpTimeout: 5_000_000,
tcpConnectTimeout: 10_000_000,
tcpTimeout: 7_000_000,
tcpKeepAlive: KeepAliveOpts.defaults,
smpPingInterval: 600_000_000
)
public static let proxyDefaults: NetCfg = NetCfg(
socksProxy: nil,
tcpConnectTimeout: 15_000_000,
tcpTimeout: 10_000_000,
tcpConnectTimeout: 20_000_000,
tcpTimeout: 15_000_000,
tcpKeepAlive: KeepAliveOpts.defaults,
smpPingInterval: 600_000_000
)

View File

@@ -28,9 +28,15 @@
/* No comment provided by engineer. */
")" = ")";
/* No comment provided by engineer. */
"[Contribute](https://github.com/simplex-chat/simplex-chat#contribute)" = "[Beitragen](https://github.com/simplex-chat/simplex-chat#contribute)";
/* No comment provided by engineer. */
"[Send us email](mailto:chat@simplex.chat)" = "[Senden Sie uns eine E-Mail](mailto:chat@simplex.chat)";
/* No comment provided by engineer. */
"[Star on GitHub](https://github.com/simplex-chat/simplex-chat)" = "[Stern auf GitHub](https://github.com/simplex-chat/simplex-chat)";
/* No comment provided by engineer. */
"**Add new contact**: to create your one-time QR Code for your contact." = "**Fügen Sie einen neuen Kontakt hinzu**: Erzeugen Sie einen Einmal-QR-Code oder -Link für Ihren Kontakt.";
@@ -264,16 +270,16 @@
"Change member role?" = "Die Mitgliederrolle ändern?";
/* No comment provided by engineer. */
"Change receiving address (BETA)" = "***Change receiving address (BETA)";
"Change receiving address (BETA)" = "Wechseln der Empfängeradresse (BETA)";
/* No comment provided by engineer. */
"Change receiving address?" = "***Change receiving address?";
"Change receiving address?" = "Empfängeradresse wechseln?";
/* No comment provided by engineer. */
"Change role" = "Rolle ändern";
/* chat item text */
"changed address for you" = "***changed address for you";
"changed address for you" = "wechselte die Adresse für Sie";
/* rcv group event chat item */
"changed role of %@ to %@" = "änderte die Rolle von %1$@ auf %2$@";
@@ -282,10 +288,10 @@
"changed your role to %@" = "änderte Ihre Rolle auf %@";
/* chat item text */
"changing address for %@..." = "***changing address for %@...";
"changing address for %@..." = "Wechseln der Adresse für %@ ...";
/* chat item text */
"changing address..." = "***changing address...";
"changing address..." = "Wechseln der Adresse ...";
/* No comment provided by engineer. */
"Chat archive" = "Datenbank Archiv";
@@ -308,9 +314,6 @@
/* No comment provided by engineer. */
"Chat is stopped" = "Der Chat ist beendet";
/* No comment provided by engineer. */
"Chat with the developers" = "Chatten Sie mit den Entwicklern";
/* No comment provided by engineer. */
"Chats" = "Chats";
@@ -360,7 +363,7 @@
"Connect via contact link?" = "Über den Kontakt-Link verbinden?";
/* No comment provided by engineer. */
"Connect via group link?" = "***Connect via group link?";
"Connect via group link?" = "Über den Gruppen-Link verbinden?";
/* No comment provided by engineer. */
"Connect via link" = "Über einen Link verbinden";
@@ -741,7 +744,7 @@
"Error adding member(s)" = "Fehler beim Hinzufügen von Mitgliedern";
/* No comment provided by engineer. */
"Error changing address" = "***Error changing address";
"Error changing address" = "Fehler beim Wechseln der Adresse";
/* No comment provided by engineer. */
"Error changing role" = "Fehler beim Ändern der Rolle";
@@ -768,7 +771,7 @@
"Error deleting connection" = "Fehler beim Löschen der Verbindung";
/* No comment provided by engineer. */
"Error deleting contact" = "***Error deleting contact";
"Error deleting contact" = "Fehler beim Löschen des Kontakts";
/* No comment provided by engineer. */
"Error deleting database" = "Fehler beim Löschen der Datenbank";
@@ -846,7 +849,7 @@
"Exporting database archive..." = "Export des Datenbankarchivs...";
/* No comment provided by engineer. */
"File will be received when your contact is online, please wait or check later!" = "Die Datei wird empfangen, wenn Ihr Kontakt online ist, bitte warten oder schauen Sie später nochmal nach!";
"File will be received when your contact is online, please wait or check later!" = "Die Datei wird empfangen, sobald Ihr Kontakt online ist. Bitte warten oder schauen Sie später nochmal nach!";
/* No comment provided by engineer. */
"File: %@" = "Datei: %@";
@@ -942,7 +945,7 @@
"Ignore" = "Ignorieren";
/* No comment provided by engineer. */
"Image will be received when your contact is online, please wait or check later!" = "Das Bild wird empfangen, sobald Ihr Kontakt online ist, bitte warten oder schauen Sie später nochmal nach!";
"Image will be received when your contact is online, please wait or check later!" = "Das Bild wird empfangen, sobald Ihr Kontakt online ist. Bitte warten oder schauen Sie später nochmal nach!";
/* No comment provided by engineer. */
"Immune to spam and abuse" = "Immun gegen Spam und Missbrauch";
@@ -969,10 +972,10 @@
"Incognito mode protects the privacy of your main profile name and image — for each new contact a new random profile is created." = "Der Inkognito-Modus schützt die Privatsphäre Ihres Hauptprofilnamens und -bildes für jeden neuen Kontakt wird ein neues Zufallsprofil erstellt.";
/* chat list item description */
"incognito via contact address link" = "Inkognito über einen Kontaktadressen Link";
"incognito via contact address link" = "Inkognito über einen Kontaktadressen-Link";
/* chat list item description */
"incognito via group link" = "***incognito via group link";
"incognito via group link" = "Inkognito über einen Gruppen-Link";
/* chat list item description */
"incognito via one-time link" = "Inkognito über einen Einmal-Link";
@@ -1074,7 +1077,7 @@
"Leave group?" = "Die Gruppe verlassen?";
/* rcv group event chat item */
"left" = "verlassen";
"left" = "hat die Gruppe verlassen";
/* No comment provided by engineer. */
"Light" = "Hell";
@@ -1110,7 +1113,7 @@
"Member" = "Mitglied";
/* rcv group event chat item */
"member connected" = "beigetreten";
"member connected" = "ist der Gruppe beigetreten";
/* No comment provided by engineer. */
"Member role will be changed to \"%@\". All group members will be notified." = "Die Mitgliederrolle wird auf \"%@\" geändert. Alle Mitglieder der Gruppe werden benachrichtigt.";
@@ -1254,7 +1257,7 @@
"Open Settings" = "Geräte-Einstellungen öffnen";
/* No comment provided by engineer. */
"Open-source protocol and code anybody can run the servers." = "Open-Source-Protokoll und -Code Jeder kann seine eigenen Server aufsetzen und nutzen.";
"Open-source protocol and code anybody can run the servers." = "Open-Source-Protokoll und -Code Jede Person kann ihre eigenen Server aufsetzen und nutzen.";
/* No comment provided by engineer. */
"or chat with the developers" = "oder chatten Sie mit den Entwicklern";
@@ -1296,7 +1299,7 @@
"Please enter correct current passphrase." = "Bitte geben Sie das korrekte, aktuelle Passwort ein.";
/* No comment provided by engineer. */
"Please enter the previous password after restoring database backup. This action can not be undone." = "Bitte geben Sie das vorherige Passwort ein, nachdem Sie die Datenbanksicherung wiederhergestellt haben. Diese Aktion kann nicht rückgängig gemacht werden.";
"Please enter the previous password after restoring database backup. This action can not be undone." = "Bitte geben Sie das vorherige Passwort ein, nachdem Sie die Datenbanksicherung wiederhergestellt haben. Diese Aktion kann nicht rückgängig gemacht werden!";
/* No comment provided by engineer. */
"Please restart the app and migrate the database to enable push notifications." = "Bitte führen Sie einen Neustart der App durch und migrieren Sie die Datenbank, um Benachrichtigungen zu aktivieren.";
@@ -1322,6 +1325,9 @@
/* No comment provided by engineer. */
"Push notifications" = "Push-Benachrichtigungen";
/* No comment provided by engineer. */
"Rate the app" = "Bewerte die App";
/* No comment provided by engineer. */
"Read" = "Lesen";
@@ -1469,6 +1475,9 @@
/* No comment provided by engineer. */
"Send notifications:" = "Benachrichtigungen senden:";
/* No comment provided by engineer. */
"Send questions and ideas" = "Senden Sie Fragen und Ideen";
/* No comment provided by engineer. */
"Sender cancelled file transfer." = "Der Absender hat die Dateiübertragung abgebrochen.";
@@ -1568,6 +1577,9 @@
/* No comment provided by engineer. */
"strike" = "durchstreichen";
/* No comment provided by engineer. */
"Support SimpleX Chat" = "Unterstützen SimpleX Chat";
/* No comment provided by engineer. */
"System" = "System";
@@ -1641,19 +1653,19 @@
"Theme" = "Design";
/* No comment provided by engineer. */
"This action cannot be undone - all received and sent files and media will be deleted. Low resolution pictures will remain." = "Diese Aktion kann nicht rückgängig gemacht werden - Alle empfangenen und gesendeten Dateien und Medien werden gelöscht. Bilder mit niedriger Auflösung bleiben erhalten.";
"This action cannot be undone - all received and sent files and media will be deleted. Low resolution pictures will remain." = "Diese Aktion kann nicht rückgängig gemacht werden! Alle empfangenen und gesendeten Dateien und Medien werden gelöscht. Bilder mit niedriger Auflösung bleiben erhalten.";
/* No comment provided by engineer. */
"This action cannot be undone - the messages sent and received earlier than selected will be deleted. It may take several minutes." = "Diese Aktion kann nicht rückgängig gemacht werden - Alle empfangenen und gesendeten Nachrichten, die über den ausgewählten Zeitraum hinaus gehen, werden gelöscht. Dies kann mehrere Minuten dauern.";
"This action cannot be undone - the messages sent and received earlier than selected will be deleted. It may take several minutes." = "Diese Aktion kann nicht rückgängig gemacht werden! Alle empfangenen und gesendeten Nachrichten, die über den ausgewählten Zeitraum hinaus gehen, werden gelöscht. Dieser Vorgang kann mehrere Minuten dauern.";
/* No comment provided by engineer. */
"This action cannot be undone - your profile, contacts, messages and files will be irreversibly lost." = "Diese Aktion kann nicht rückgängig gemacht werden - Ihr Profil und Ihre Kontakte, Nachrichten und Dateien gehen unwiderruflich verloren.";
"This action cannot be undone - your profile, contacts, messages and files will be irreversibly lost." = "Diese Aktion kann nicht rückgängig gemacht werden! Ihr Profil und Ihre Kontakte, Nachrichten und Dateien gehen unwiderruflich verloren.";
/* notification title */
"this contact" = "Dieser Kontakt";
/* No comment provided by engineer. */
"This feature is experimental! It will only work if the other client has version 4.2 installed. You should see the message in the conversation once the address change is completed please check that you can still receive messages from this contact (or group member)." = "***This feature is experimental! It will only work if the other client has version 4.2 installed. You should see the message in the conversation once the address change is completed please check that you can still receive messages from this contact (or group member).";
"This feature is experimental! It will only work if the other client has version 4.2 installed. You should see the message in the conversation once the address change is completed please check that you can still receive messages from this contact (or group member)." = "Diese Funktion ist experimentell! Sie wird nur funktionieren, wenn der Kontakt ebenfalls die SimpleX-Version 4.2 installiert hat. Sobald der Adress-Wechsel abgeschlossen ist, sollten Sie die Nachricht im Chat-Verlauf sehen. Bitte prüfen Sie, ob Sie weiterhin Nachrichten von diesem Kontakt (oder Gruppenmitglied) empfangen können.";
/* No comment provided by engineer. */
"This group no longer exists." = "Diese Gruppe existiert nicht mehr.";
@@ -1680,7 +1692,7 @@
"To support instant push notifications the chat database has to be migrated." = "Um sofortige Push-Benachrichtigungen zu unterstützen, muss die Chat-Datenbank migriert werden.";
/* No comment provided by engineer. */
"Transfer images faster (BETA)" = "***Transfer images faster (BETA)";
"Transfer images faster (BETA)" = "Bilder schneller übertragen (BETA)";
/* No comment provided by engineer. */
"Trying to connect to the server used to receive messages from this contact (error: %@)." = "Beim Versuch die Verbindung mit dem Server aufzunehmen, der für den Empfang von Nachrichten mit diesem Kontakt genutzt wird, ist ein Fehler aufgetreten (Fehler: %@).";
@@ -1764,10 +1776,10 @@
"v%@ (%@)" = "v%@ (%@)";
/* chat list item description */
"via contact address link" = "über einen Kontaktadressen Link";
"via contact address link" = "über einen Kontaktadressen-Link";
/* chat list item description */
"via group link" = "***via group link";
"via group link" = "über einen Gruppen-Link";
/* chat list item description */
"via one-time link" = "über einen Einmal-Link";
@@ -1824,7 +1836,7 @@
"You accepted connection" = "Sie haben die Verbindung akzeptiert";
/* No comment provided by engineer. */
"You are already connected to %@ via this link." = "Sie sind bereits über diesen Link mit %@ verbunden.";
"You are already connected to %@." = "Sie sind bereits mit %@ verbunden.";
/* No comment provided by engineer. */
"You are connected to the server used to receive messages from this contact." = "Sie sind mit dem Server verbunden, der für den Empfang von Nachrichten mit diesem Kontakt genutzt wird.";
@@ -1848,7 +1860,7 @@
"You can share a link or a QR code - anybody will be able to join the group. You won't lose members of the group if you later delete it." = "Sie können diesen Link oder QR-Code teilen - Damit kann jede Person der Gruppe beitreten. Wenn Sie den Link später löschen, werden Sie keine Gruppenmitglieder verlieren, die der Gruppe darüber beigetreten sind.";
/* No comment provided by engineer. */
"You can share your address as a link or as a QR code - anybody will be able to connect to you. You won't lose your contacts if you later delete it." = "Sie können Ihre Adresse als Link oder als QR-Code teilen Jeder kann sich darüber mit Ihnen verbinden. Sie werden Ihre mit dieser Adresse verbundenen Kontakte nicht verlieren, wenn Sie diese Adresse später löschen.";
"You can share your address as a link or as a QR code - anybody will be able to connect to you. You won't lose your contacts if you later delete it." = "Sie können Ihre Adresse als Link oder als QR-Code teilen Jede Person kann sich darüber mit Ihnen verbinden. Sie werden Ihre mit dieser Adresse verbundenen Kontakte nicht verlieren, wenn Sie diese Adresse später löschen.";
/* No comment provided by engineer. */
"You can start chat via app Settings / Database or by restarting the app" = "Sie können den Chat über die App-Einstellungen / Datenbank oder durch Neustart der App starten.";
@@ -1857,10 +1869,10 @@
"You can use markdown to format messages:" = "Um Nachrichteninhalte zu formatieren, können Sie Markdowns verwenden:";
/* chat item text */
"you changed address" = "***you changed address";
"you changed address" = "Sie haben die Adresse gewechselt";
/* chat item text */
"you changed address for %@" = "***you changed address for %@";
"you changed address for %@" = "Sie haben die Adresse für %@ gewechselt";
/* snd group event chat item */
"you changed role for yourself to %@" = "Sie haben Ihre eigene Rolle auf %@ geändert";
@@ -1911,19 +1923,19 @@
"you shared one-time link incognito" = "Sie haben Inkognito einen Einmal-Link geteilt";
/* No comment provided by engineer. */
"You will be connected to group when the group host's device is online, please wait or check later!" = "***You will be connected to group when the group host's device is online, please wait or check later!";
"You will be connected to group when the group host's device is online, please wait or check later!" = "Sie werden mit der Gruppe verbunden, sobald das Endgerät des Gruppen-Hosts online ist. Bitte warten oder schauen Sie später nochmal nach!";
/* No comment provided by engineer. */
"You will be connected when your connection request is accepted, please wait or check later!" = "Sie werden verbunden, sobald Ihre Verbindungsanfrage akzeptiert wird, bitte warten oder schauen Sie später nochmal nach!";
"You will be connected when your connection request is accepted, please wait or check later!" = "Sie werden verbunden, sobald Ihre Verbindungsanfrage akzeptiert wird. Bitte warten oder schauen Sie später nochmal nach!";
/* No comment provided by engineer. */
"You will be connected when your contact's device is online, please wait or check later!" = "Sie werden verbunden, wenn das Gerät Ihres Kontakts online ist, bitte warten oder schauen Sie später nochmal nach!";
"You will be connected when your contact's device is online, please wait or check later!" = "Sie werden verbunden, sobald das Endgerät Ihres Kontakts online ist. Bitte warten oder schauen Sie später nochmal nach!";
/* No comment provided by engineer. */
"You will be required to authenticate when you start or resume the app after 30 seconds in background." = "Sie müssen sich authentifizieren, wenn Sie die im Hintergrund befindliche App nach 30 Sekunden starten oder fortsetzen.";
/* No comment provided by engineer. */
"You will join a group this link refers to and connect to its group members." = "***You will join a group this link refers to and connect to its group members.";
"You will join a group this link refers to and connect to its group members." = "Sie werden der Gruppe beitreten, auf die sich dieser Link bezieht und sich mit deren Gruppenmitgliedern verbinden.";
/* No comment provided by engineer. */
"You will stop receiving messages from this group. Chat history will be preserved." = "Sie werden von dieser Gruppe keine Nachrichten mehr erhalten. Der Chatverlauf wird beibehalten.";

View File

@@ -28,9 +28,15 @@
/* No comment provided by engineer. */
")" = ")";
/* No comment provided by engineer. */
"[Contribute](https://github.com/simplex-chat/simplex-chat#contribute)" = "[Внести свой вклад](https://github.com/simplex-chat/simplex-chat#contribute)";
/* No comment provided by engineer. */
"[Send us email](mailto:chat@simplex.chat)" = "[Отправить email](mailto:chat@simplex.chat)";
/* No comment provided by engineer. */
"[Star on GitHub](https://github.com/simplex-chat/simplex-chat)" = "[Поставить звездочку в GitHub](https://github.com/simplex-chat/simplex-chat)";
/* No comment provided by engineer. */
"**Add new contact**: to create your one-time QR Code for your contact." = "**Добавить новый контакт**: чтобы создать одноразовый QR код или ссылку для вашего контакта.";
@@ -308,9 +314,6 @@
/* No comment provided by engineer. */
"Chat is stopped" = "Чат остановлен";
/* No comment provided by engineer. */
"Chat with the developers" = "Соединиться с разработчиками";
/* No comment provided by engineer. */
"Chats" = "Чаты";
@@ -1322,6 +1325,9 @@
/* No comment provided by engineer. */
"Push notifications" = "Доставка уведомлений";
/* No comment provided by engineer. */
"Rate the app" = "Оценить приложение";
/* No comment provided by engineer. */
"Read" = "Прочитано";
@@ -1469,6 +1475,9 @@
/* No comment provided by engineer. */
"Send notifications:" = "Отправлять уведомления:";
/* No comment provided by engineer. */
"Send questions and ideas" = "Отправьте вопросы и идеи";
/* No comment provided by engineer. */
"Sender cancelled file transfer." = "Отправитель отменил передачу файла.";
@@ -1568,6 +1577,9 @@
/* No comment provided by engineer. */
"strike" = "зачеркнуть";
/* No comment provided by engineer. */
"Support SimpleX Chat" = "Поддержать SimpleX Chat";
/* No comment provided by engineer. */
"System" = "Системная";
@@ -1824,7 +1836,7 @@
"You accepted connection" = "Вы приняли приглашение соединиться";
/* No comment provided by engineer. */
"You are already connected to %@ via this link." = "Вы уже соединены с %@ через эту ссылку.";
"You are already connected to %@." = "Вы уже соединены с контактом %@.";
/* No comment provided by engineer. */
"You are connected to the server used to receive messages from this contact." = "Установлено соединение с сервером, через который вы получаете сообщения от этого контакта.";

View File

@@ -2,6 +2,7 @@
layout: layouts/article.html
title: "Simplex Chat"
date: 2020-10-22
preview: The prototype of SimpleX Messaging Server implementing SMP protocol.
permalink: "/blog/20201022-simplex-chat.html"
---

View File

@@ -2,6 +2,7 @@
layout: layouts/article.html
title: "Announcing SimpleX Chat Prototype!"
date: 2021-05-12
preview: Prototype chat app for the terminal (console).
permalink: "/blog/20210512-simplex-chat-terminal-ui.html"
---

View File

@@ -2,6 +2,7 @@
layout: layouts/article.html
title: "SimpleX announces SimpleX Chat v0.4"
date: 2021-09-14
preview: Terminal app now supports groups and file transfers.
permalink: "/blog/20210914-simplex-chat-v0.4-released.html"
---

View File

@@ -2,6 +2,7 @@
layout: layouts/article.html
title: "SimpleX announces SimpleX Chat v0.5"
date: 2021-12-08
preview: Support for long-term user addresses in terminal app.
permalink: "/blog/20211208-simplex-chat-v0.5-released.html"
---

View File

@@ -2,6 +2,7 @@
layout: layouts/article.html
title: "SimpleX announces SimpleX Chat v1"
date: 2022-01-12
preview: Major protocol changes address all design mistakes identified during concept review by an independent expert.
permalink: "/blog/20220112-simplex-chat-v1-released.html"
---

View File

@@ -2,6 +2,7 @@
layout: layouts/article.html
title: "SimpleX announces SimpleX Chat public beta for iOS"
date: 2022-02-14
preview: Our first prototype of mobile UI for iOS is available!
permalink: "/blog/20220214-simplex-chat-ios-public-beta.html"
---

View File

@@ -2,6 +2,7 @@
layout: layouts/article.html
title: "SimpleX announces SimpleX Chat mobile apps for iOS and Android"
date: 2022-03-08
preview: Brand new mobile apps with battle-tested Haskell core.
permalink: "/blog/20220308-simplex-chat-mobile-apps.html"
---
@@ -11,7 +12,7 @@ permalink: "/blog/20220308-simplex-chat-mobile-apps.html"
## SimpleX Chat is the first chat platform that is 100% private by design - it has no access to your connections graph
We have now released iPhone and Android apps to [Apple AppStore](https://apps.apple.com/us/app/simplex-chat/id1605771084) and [Google Play Store](https://play.google.com/store/apps/details?id=chat.simplex.app), [APK for Android](https://github.com/simplex-chat/website/raw/master/simplex.apk) is also available for direct download.
We have now released iPhone and Android apps to [Apple AppStore](https://apps.apple.com/us/app/simplex-chat/id1605771084) and [Google Play Store](https://play.google.com/store/apps/details?id=chat.simplex.app), [APK for Android](https://github.com/simplex-chat/simplex-chat/releases/latest/download/simplex.apk) is also available for direct download.
**Please note**: the current version is only supported on iPhone 8+ and on Android 10+ - we are planning to add support for iPad and older devices very soon, and we will announce it on our [Reddit](https://www.reddit.com/r/SimpleXChat/) and [Twitter](https://twitter.com/SimpleXChat) channels - please subscribe to follow our updates there.

View File

@@ -2,6 +2,7 @@
layout: layouts/article.html
title: "Instant notifications for SimpleX Chat mobile apps"
date: 2022-04-04
preview: Design of private instant notifications on Android and for push notifications for iOS.
permalink: "/blog/20220404-simplex-chat-instant-notifications.html"
---
@@ -63,7 +64,7 @@ How does it work? When the app is first started on an Android device, it starts
This service continues running when the app is switched off, and it is restarted when the device is restarted even if you don't open the app - so the message notifications arrive instantly every time. To maximize battery life, it can be turned off by switching off "Private notifications". You will still receive notifications while the app is running or in the background.
So, for Android we can now deliver instant message notifications without compromising users' privacy in any way. The app version 1.5 that includes private instant notifications is now available on [Play Store](https://play.google.com/store/apps/details?id=chat.simplex.app), in our [F-Droid repo](https://app.simplex.chat/) and via direct [APK](https://github.com/simplex-chat/website/raw/master/simplex.apk) downloads!
So, for Android we can now deliver instant message notifications without compromising users' privacy in any way. The app version 1.5 that includes private instant notifications is now available on [Play Store](https://play.google.com/store/apps/details?id=chat.simplex.app), in our [F-Droid repo](https://app.simplex.chat/) and via direct [APK](https://github.com/simplex-chat/simplex-chat/releases/latest/download/simplex.apk) downloads!
Please let us what needs to be improved - it's only the first version of instant notifications for Android!

View File

@@ -2,6 +2,8 @@
layout: layouts/article.html
title: "SimpleX Chat v2.0 - sending images and files in mobile apps"
date: 2022-05-11
image: images/20220511-images-files.png
preview: Read how SimpleX delivers messages without having user profile identifiers of any kind.
permalink: "/blog/20220511-simplex-chat-v2-images-files.html"
---

View File

@@ -2,6 +2,7 @@
layout: layouts/article.html
title: "SimpleX Chat v2.1 - better conversation privacy"
date: 2022-05-24
preview: Clear conversations without deleting contacts
permalink: "/blog/20220524-simplex-chat-better-privacy.html"
---

View File

@@ -1,11 +1,14 @@
---
layout: layouts/article.html
title: "SimpleX Chat v2.2 - the first messaging platform without user identities - 100% private by design!"
title: "SimpleX Chat v2.2 - the new privacy and security features"
date: 2022-06-04
image: images/20220604-privacy-settings.png
imageBottom: true
previewBody: blog_previews/20220604.html
permalink: "/blog/20220604-simplex-chat-new-privacy-security-settings.html"
---
# SimpleX Chat v2.2 - the first messaging platform without user identities - 100% private by design!
# SimpleX Chat v2.2 - the new privacy and security features
**Published:** June 4, 2022

View File

@@ -1,11 +1,13 @@
---
layout: layouts/article.html
title: "SimpleX announces SimpleX Chat v3"
title: "SimpleX announces SimpleX Chat v3 &mdash; with encrypted calls and iOS push notifications"
date: 2022-07-11
image: images/20220711-call.png
previewBody: blog_previews/20220711.html
permalink: "/blog/20220711-simplex-chat-v3-released-ios-notifications-audio-video-calls-database-export-import-protocol-improvements.html"
---
# SimpleX announces SimpleX Chat v3
# SimpleX announces SimpleX Chat v3 - with encrypted calls and iOS push notifications
**Published:** Jul 11, 2022

View File

@@ -1,11 +1,14 @@
---
layout: layouts/article.html
title: "SimpleX Chat v3.1-beta is released"
title: "SimpleX Chat v3.1-beta is released &mdash; improved battery/traffic usage"
date: 2022-07-23
image: images/20220723-group-invite.png
imageBottom: true
previewBody: blog_previews/20220723.html
permalink: "/blog/20220723-simplex-chat-v3.1-tor-groups-efficiency.html"
---
# SimpleX Chat v3.1-beta is released
# SimpleX Chat v3.1-beta is released - improved battery/traffic usage
**Published:** Jul 23, 2022
@@ -38,7 +41,7 @@ curl -o- https://raw.githubusercontent.com/simplex-chat/simplex-chat/stable/inst
Groups have been supported by SimpleX Chat core for a very long time, but there was no user interface in the mobile apps to use them - users had to use chat console to create groups, add members, and accept invitations.
This release allows accepting the invitations to join groups via mobile apps UI, making it much easier to create groups - only one user (a group owner) needs to use chat console, while all other groups members just need to tap a button in the UI to join or leave the group. Full group UI is coming in v3.1 in 1-2 weeks, but you can already start using groups today by installing beta-versions of mobile apps via [TestFlight](https://testflight.apple.com/join/DWuT2LQu), [Google PlayStore Beta](https://play.google.com/apps/testing/chat.simplex.app) and [APK download](https://github.com/simplex-chat/simplex-chat/releases/download/v3.1.0-beta.0/simplex.apk).
This release allows accepting the invitations to join groups via mobile apps UI, making it much easier to create groups - only one user (a group owner) needs to use chat console, while all other groups members just need to tap a button in the UI to join or leave the group. Full group UI is coming in v3.1 in 1-2 weeks, but you can already start using groups today by installing beta-versions of mobile apps via [TestFlight](https://testflight.apple.com/join/DWuT2LQu), [Google PlayStore Beta](https://play.google.com/apps/testing/chat.simplex.app) and [APK download](https://github.com/simplex-chat/simplex-chat/releases/latest/download/simplex.apk).
To manage groups via terminal app or via chat console in the mobile apps you have to use these commands:

View File

@@ -1,11 +1,14 @@
---
layout: layouts/article.html
title: "SimpleX Chat v3.1 is released"
title: "SimpleX Chat v3.1 is released &mdash; with secret groups and server access via Tor"
date: 2022-08-08
image: images/20220808-tor1.png
imageBottom: true
previewBody: blog_previews/20220808.html
permalink: "/blog/20220808-simplex-chat-v3.1-chat-groups.html"
---
# SimpleX Chat v3.1 is released
# SimpleX Chat v3.1 is released - with secret groups and server access via Tor
**Published:** Aug 8, 2022

View File

@@ -1,11 +1,14 @@
---
layout: layouts/article.html
title: "SimpleX Chat v3.2 is released"
title: "SimpleX Chat v3.2 is released &mdash; meet Incognito mode, unique to Simplex Chat"
date: 2022-09-01
image: images/20220901-incognito1.png
imageBottom: true
previewBody: blog_previews/20220901.html
permalink: "/blog/20220901-simplex-chat-v3.2-incognito-mode.html"
---
# SimpleX Chat v3.2 is released
# SimpleX Chat v3.2 is released - meet Incognito mode, unique to Simplex Chat
**Published:** Sep 1, 2022

View File

@@ -2,6 +2,9 @@
layout: layouts/article.html
title: "SimpleX Chat v4.0 with encrypted database is released"
date: 2022-09-28
image: images/20220928-passphrase.png
imageBottom: true
previewBody: blog_previews/20220928.html
permalink: "/blog/20220928-simplex-chat-v4-encrypted-database.html"
---

View File

@@ -0,0 +1,188 @@
---
layout: layouts/article.html
title: "Security assessment by Trail of Bits, the new website and v4.2 released"
date: 2022-11-08
image: images/20221108-trail-of-bits.jpg
previewBody: blog_previews/20221108.html
permalink: "/blog/20221108-simplex-chat-v4.2-security-audit-new-website.html"
---
# Security assessment by Trail of Bits, the new website and v4.2 released
**Published:** Nov 8, 2022
## Security assessment by Trail of Bits
<img src="./images/20221108-trail-of-bits.jpg" width=240>
When we first launched the app in March the response on Reddit was: _"Have you been audited or should we just ignore you?"_.
We have a growing number of enthusiasts using SimpleX Chat who can accept the security risks of unaudited system, but the users who depend on their security were patiently waiting until some independent experts validate our claims.
[Trail of Bits](https://www.trailofbits.com/about), a US based security and technology consultancy whose clients include big tech companies, governmental agencies and major blockchain projects, had 2 engineers reviewing SimpleX Chat, specifically [simplexmq library](https://github.com/simplex-chat/simplexmq) that is responsible for all cryptography and networking of SimpleX platform.
2 medium and 2 low severity issues were identified, all of which require a high difficulty attack to exploit the attacker would need to have a privileged access to the system, may need to know complex technical details, or must discover other weaknesses to exploit them. 3 of these issues are already fixed in v4.2.
Overall we have SimpleX Chat in a decent shape, with most reviewed areas other than identified issues being marked as "satisfactory", and authentication and access controls as "strong".
The issues are explained below, and the full security review is available via [Trail of Bits publications](https://github.com/trailofbits/publications#technology-product-reviews).
We are hugely thankful to Trails Of Bits and their engineers for the work they did, helping us identify these issues and strengthen the security of SimpleX Chat.
### Medium severity issues
#### X3DH key exchange for double ratchet protocol
We made a mistake implementing X3DH key exchange - the key derivation function was not applied to the result of concatenation of three DH operations. The attack to exploit this mistake has high complexity, as it would require compromising one of private keys generated by the clients, and also it would only affect forward secrecy until break-in recovery happens (after both sides sent some messages).
Please note that SimpleX does not perform X3DH with long-term identity keys, as the SimpleX protocol does not rely on long-term keys to identify client devices. Therefore, the impact of compromising a key will be less severe, as it will affect only the secrets of the connection where the key was compromised.
This issue is fixed in version 4.2 in [this PR](https://github.com/simplex-chat/simplexmq/pull/548/files), and if both clients are updated the key exchange will not have this vulnerability. Also, previously created connections should be secure as long as both sides sent the messages, but if you believe that your private key(s) could have been compromised (for example, if you used SimpleX Chat since before we added database encryption), we recommend that you create the new connections with your contacts, at least with the security-critical ones. Simply rotating the connection queue (manual queue rotation is added in version 4.2) will not be sufficient, as this rotation does not re-initialize the ratchets - this is something we will be adding in the future.
#### Keys are stored in unpinned memory and not cleared after their lifetime
The problem here is that the memory with cryptographic keys can be swapped to the storage and potentially accessed by an attacker who has root-level access to the device (or the level of access required to access swap file of the application). So, if you are running SimpleX Chat on desktop you could improve its security by running it in an isolated container.
On mobile operating systems it is less severe as each application already runs in its own container, and applications do not share access to their swap areas (e.g., on Android swap is a [compressed area in RAM](https://developer.android.com/topic/performance/memory-management) not accessible to other applications).
To exploit this issue an attacker needs to have a privileged system access to the device. Also, we believe [Haskell generational garbage collection](https://www.microsoft.com/en-us/research/wp-content/uploads/1993/01/gen-gc-for-haskell.pdf) makes the lifetime of unused memory lower than in other languages.
We will be addressing this issue in the near future, possibly by using library [secure-memory](https://hackage.haskell.org/package/secure-memory-0.0.0.2) created by Kirill Elagin, an engineer at Serokell, or some other similar approach.
### Low severity issues
#### The functions that do string padding and unpadding can throw exceptions
Both these issues are fixed in 4.2 in [this PR](https://github.com/simplex-chat/simplexmq/pull/547/files), with the additional unit tests, and we also validated that even before the fix the strings that would cause such exception were never passed to this function we could not find the possibility of the attack that would succeed because of this issue.
### What's next
There are areas of SimpleX Chat that were out of scope of this review, specifically:
- the chat protocol implementation and mobile UIs, as they includes no cryptography of networking (with the exception of Android app storing encrypted database passphrase and key exchange/encryption for WebRTC calls).
- push notifications server that is used by iOS clients.
We will be arranging to review these areas separately.
## The new website
Our [previous website](https://old-website.simplex.chat) was created 2 years ago to present SimpleX idea, there was no SimpleX Chat at the time - we only had a prototype implementation of SimpleX Messaging Protocol server then.
A lot of people told us that our website didn't explain well enough who SimpleX Chat is for, what problems it solves, and how it is different from the alternatives. So, while we love to be focused on the chat application, we decided to make the new one.
We hope that our [new website](https://simplex.chat) better answers these questions. If you think something should be added/removed/changed - please let us know. Thank you!
## SimpleX Chat v4.2 released!
New in this release:
- fixed 3 issues from the security audit!
- group links - group admins can create the links for new members to join
- auto-accept contact requests + configure whether to accept incognito and welcome message
- small things: change group member role, mark chat as unread, send stickers and GIFs from Android keyboards.
Beta features (enable Developer tools to try them):
- manually switch contact or member to another address / server (it has to be supported by both clients to work)
- receive files faster (enable it in Privacy & Security settings)
### Group links
<img src="./images/20221108-group1.png" width="288"> &nbsp;&nbsp; <img src="./images/20221108-group2.png" width="288">
It's been requested by many users - to be able to join a group via link. Because SimpleX Chat groups are fully decentralised, and there is no server-side state, joining via these links requires the participation of the link creator who has to be online to accept the group joining request.
The way it works under the hood is similar to how contact addresses work:
1. Group admin or owner creates a long term address that is technically the same as a user address, but it is associated with a specific group.
2. The user that joins the group can identify that this link belongs to some group by an additional piece of data in the link - `{"type": "group", "groupLinkId": "some random string"}`. The ID in this link does not represent a group identity, every time any user creates a new link for the same group, this ID will be different. This ID is used by the joining client to identify the group and automatically accept the invitation when it is received.
3. When admin receives a connection request, they automatically accept it and send invitation link to join the group.
4. The joining user compares the ID in the invitation with the ID in the link, and if they match automatically accepts the invitation.
After that it works as when joining via the manual invitation - the joining user will be establishing the connection with all existing members to be able to send messages to the group.
The link can be created via the group page, as shown on the picture.
We have several groups you can join to ask any questions or just to test the app:
- [#SimpleX-Group](https://simplex.chat/contact#/?v=1-2&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FWHV0YU1sYlU7NqiEHkHDB6gxO1ofTync%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAWbebOqVYuBXaiqHcXYjEHCpYi6VzDlu6CVaijDTmsQU%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion&data=%7B%22type%22%3A%22group%22%2C%22groupLinkId%22%3A%22mL-7Divb94GGmGmRBef5Dg%3D%3D%22%7D): a general group with more than a 100 members where you can ask any questions.
- Several groups by countries/languages: [\#SimpleX-DE](https://simplex.chat/contact#/?v=1-2&smp=smp%3A%2F%2Fhpq7_4gGJiilmz5Rf-CswuU5kZGkm_zOIooSw6yALRg%3D%40smp5.simplex.im%2FmIorjTDPG24jdLKXwutS6o9hdQQRZwfQ%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEA9N0BZaECrAw3we3S1Wq4QO7NERBuPt9447immrB50wo%253D%26srv%3Djjbyvoemxysm7qxap7m5d5m35jzv5qq6gnlv7s4rsn7tdwwmuqciwpid.onion&data=%7B%22type%22%3A%22group%22%2C%22groupLinkId%22%3A%22S8aISlOgkTMytSox9gAM2Q%3D%3D%22%7D) (German), [\#SimpleX-US](https://simplex.chat/contact#/?v=1-2&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FlTWmQplLEaoJyHnEL1-B3f2PtDsikcTs%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEA-hMBlsQjNxK2vaVhqW_UyAVtuoYqgYTigK4B9dJ9CGc%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion&data=%7B%22type%22%3A%22group%22%2C%22groupLinkId%22%3A%22G0UtRHIn0TmPoo08h_cbTA%3D%3D%22%7D) (US/English), [\#SimpleX-France](https://simplex.chat/contact#/?v=1-2&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2F11r6XyjwVMj0WDIUMbmNDXO996M_EN_1%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAXDmc2Lrj9WQOjEcWa0DeQHF3HcYOp9b68s8M_BJ7gEk%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion&data=%7B%22type%22%3A%22group%22%2C%22groupLinkId%22%3A%22EZCeSYpeIBkaQwCcpcF00w%3D%3D%22%7D), [\#SimpleX-RU](https://simplex.chat/contact#/?v=1-2&smp=smp%3A%2F%2Fhpq7_4gGJiilmz5Rf-CswuU5kZGkm_zOIooSw6yALRg%3D%40smp5.simplex.im%2FZSYM278L5WoZiApx3925EAjSXcsAVNVu%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEA7RJ2wfT8zdfOLyE5OtWLEAPowj-q6F2HB0ExbATw8Gk%253D%26srv%3Djjbyvoemxysm7qxap7m5d5m35jzv5qq6gnlv7s4rsn7tdwwmuqciwpid.onion&data=%7B%22type%22%3A%22group%22%2C%22groupLinkId%22%3A%22fsVoklNGptt7n-droqJYUQ%3D%3D%22%7D) (Russian), [#SimpleX-NL](https://simplex.chat/contact#/?v=1-2&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2FmP0LbswSbfxoVkkxiWE2NYnBCgZ9Snvj%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAVwZuSsw4Mf52EaBNdNI3RebsLm0jg65ZIkcmH9E5uy8%253D%26srv%3Dbylepyau3ty4czmn77q4fglvperknl4bi2eb2fdy2bh4jxtf32kf73yd.onion&data=%7B%22type%22%3A%22group%22%2C%22groupLinkId%22%3A%22M9xIULUNZx51Wsa5Kdb0Sg%3D%3D%22%7D) (Netherlands/Dutch), [#SimpleX-IT](https://simplex.chat/contact#/?v=1-2&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2FaZ_wjh6QAYHB-LjyGtp8bllkzoq880u-%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEA-_Wulzc3j16i7t77XJ5wgwxeW8_Ea8GxetMo7K4MgjI%253D%26srv%3Dbylepyau3ty4czmn77q4fglvperknl4bi2eb2fdy2bh4jxtf32kf73yd.onion&data=%7B%22type%22%3A%22group%22%2C%22groupLinkId%22%3A%22QWmXdrFzIeMd2OoEPMFkBQ%3D%3D%22%7D) (Italian).
You can join these groups either by opening these links in the app or by opening them in desktop browser and scanning QR code.
Let me know if you'd like to add some other countries to the list. Join via the apps to share what's going on and ask any questions!
### Auto-accept contact requests
<img src="./images/20221108-address1.png" width="288"> &nbsp;&nbsp; <img src="./images/20221108-address2.png" width="288">
When somebody connects to you via your long-term address you have to manually accept a connection request (it shows in blue color in the list of chats). The feature that we added in this release allows to configure the app to accept contact requests automatically, and also choose whether this contact should receive your main profile or a random incognito profile (independent of the current app setting), and add an optional auto-reply message.
This feature is useful if you publish your address on your webpage or social profile, and do not want to screen people who want to connect to you. You may want to send a standard welcome message, for example, if it is an online store, and you need to share any information with everybody who contacts you.
Our @simplex account that you connect to when you choose "Connect to developers" in the app used this feature for a long time, and now it is available to mobile app users.
### Some small things
1. Changing group member role is a very basic feature, but it was only added in this release.
2. You can now mark a conversation as unread, for example if you accidentally marked all messages as read and you want to review it later.
3. Send stickers and GIFs from Android keyboards, and, finally, the bug with backspace button is resolved as well.
### Change your delivery address (BETA)
<img src="./images/20221108-switch-address.png" width="288">
To manually switch any of your contacts (or a group member to a new server address) enable Developer tools and choose "Change receiving address" on the contact page. As long as they run a new version of the app and online, the switch should only take a few seconds.
That is a major improvement of metadata privacy of SimpleX protocols, because previously, while we didn't have user identifiers, the pairwise identifiers of messaging queues used to deliver messages were used for as long as the contact existed. Now these identifiers are temporary, and in a near future we will be adding automatic rotation of these delivery addresses.
It is also useful when you want to migrate message delivery to another server, for example, if you used SimpleX Chat default servers and now want to self-host your own. Or, maybe, you need to change the address of your server. Previously it would require creating new contacts and losing conversation histories, and now all you have to do is to change server configuration in the app, and when the change of the address is triggered (currently, only manually, and in the near future - automatically), your contacts will be migrated to a new server, without you doing anything - it only requires each party sending 2 messages to negotiate the reconnection, and it would also rotate the encryption keys used for the outer layer of E2E encryption.
### Receive images and small files faster (BETA)
<img src="./images/20221108-faster-images.png" width="288">
From version 4.2 all files smaller than ~92kb (equal to 6 message blocks) will be sent in the same connection where you have the chat, and files smaller than ~231kb (the limit for image size) can also be optionally received via the same connection the latter requires enabling "Transfer images faster" in Privacy & security settings (it will be available after you enable Developer tools). There are two reasons why it is not on by default yet: 1) we wanted to ensure it is stable; 2) there is a small effect on metadata privacy of having a burst of traffic in the same connection where you are having the main conversation.
This functionality was created for the future voice messages, as they need to be sent without acceptance, so that the recipients can listen to them even when the sender is offline.
## SimpleX platform
Some links to answer the most common questions:
[How can SimpleX deliver messages without user identifiers](./20220511-simplex-chat-v2-images-files.md#the-first-messaging-platform-without-user-identifiers).
[What are the risks to have identifiers assigned to the users](./20220711-simplex-chat-v3-released-ios-notifications-audio-video-calls-database-export-import-protocol-improvements.md#why-having-users-identifiers-is-bad-for-the-users).
[Technical details and limitations](./20220723-simplex-chat-v3.1-tor-groups-efficiency.md#privacy-technical-details-and-limitations).
[How SimpleX is different from Session, Matrix, Signal, etc.](https://github.com/simplex-chat/simplex-chat/blob/stable/README.md#frequently-asked-questions).
Please also see the information on our [new website](https://simplex.chat) - it also answers all these questions.
## Help us with donations
Huge thank you to everybody who donated to SimpleX Chat!
We are prioritizing users privacy and security - it would be impossible without your support.
Our pledge to our users is that SimpleX protocols are and will remain open, and in public domain, - so anybody can build the future implementations of the clients and the servers. We are building SimpleX platform based on the same principles as email and web, but much more private and secure.
Your donations help us raise more funds any amount, even the price of the cup of coffee, makes a big difference for us.
It is possible to donate via:
- [GitHub](https://github.com/sponsors/simplex-chat) - it is commission-free for us.
- [OpenCollective](https://opencollective.com/simplex-chat) - it charges a commission, and also accepts donations in many crypto-currencies.
- Monero wallet: 8568eeVjaJ1RQ65ZUn9PRQ8ENtqeX9VVhcCYYhnVLxhV4JtBqw42so2VEUDQZNkFfsH5sXCuV7FN8VhRQ21DkNibTZP57Qt
- Bitcoin wallet: 1bpefFkzuRoMY3ZuBbZNZxycbg7NYPYTG
- please let us know, via GitHub issue or chat, if you want to make a donation in some other cryptocurrency - we will add the address to the list.
Thank you,
Evgeny
SimpleX Chat founder

View File

@@ -1,5 +1,21 @@
# Blog
Nov 8, 2022 [Security audit by Trail of Bits, the new website and v4.2 released](./20221108-simplex-chat-v4.2-security-audit-new-website.md)
_"Have you been audited or should we just ignore you?"_
SimpleX Chat has now been audited by [Trail of Bits](https://www.trailofbits.com/about), 4 issues were identified, and 3 of them are fixed in 4.2
The new website is live: https://simplex.chat
v4.2 is released:
- group links - group admins can create the links for new members to join
- auto-accept contact requests + configure whether to accept incognito and welcome message
- small things: change group member role, mark chat as unread, send stickers and GIFs from Android keyboards.
- manually switch contact or member to another address / server (BETA)
- receive files faster (BETA)
Sep 28, 2022 [v4: local database encryption](./20220928-simplex-chat-v4-encrypted-database.md)
- encrypted local chat database - if you already use the app, you can encrypt the database in the app settings

Binary file not shown.

After

Width:  |  Height:  |  Size: 209 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 390 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 308 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 215 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 103 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 193 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 225 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 194 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

View File

@@ -7,7 +7,7 @@ constraints: zip +disable-bzip2 +disable-zstd
source-repository-package
type: git
location: https://github.com/simplex-chat/simplexmq.git
tag: 029fc6e781d78249d0b3a17edff7416ef22afed1
tag: d2b88a1baa390ec64b6535e32ce69f26f53f4d7a
source-repository-package
type: git

Binary file not shown.

View File

@@ -1,5 +1,5 @@
name: simplex-chat
version: 4.2.0
version: 4.2.1
#synopsis:
#description:
homepage: https://github.com/simplex-chat/simplex-chat#readme
@@ -34,7 +34,7 @@ dependencies:
- process == 1.6.*
- random >= 1.1 && < 1.3
- simple-logger == 0.1.*
- simplexmq >= 3.3
- simplexmq >= 3.4
- socks == 0.6.*
- sqlcipher-simple == 0.4.*
- stm == 2.5.*

View File

@@ -1,5 +1,5 @@
{
"https://github.com/simplex-chat/simplexmq.git"."029fc6e781d78249d0b3a17edff7416ef22afed1" = "054bjknwr75n7rpi4q41x966mnf4903rvy402vwyp1y9njzbd20q";
"https://github.com/simplex-chat/simplexmq.git"."d2b88a1baa390ec64b6535e32ce69f26f53f4d7a" = "1ijmhi9srkyq43aflsgx38hfir3q3q5d9xlq13g1sdh43i4wmyvk";
"https://github.com/simplex-chat/direct-sqlcipher.git"."34309410eb2069b029b8fc1872deb1e0db123294" = "0kwkmhyfsn2lixdlgl15smgr1h5gjk7fky6abzh8rng2h5ymnffd";
"https://github.com/simplex-chat/sqlcipher-simple.git"."5e154a2aeccc33ead6c243ec07195ab673137221" = "1d1gc5wax4vqg0801ajsmx1sbwvd9y7p7b8mmskvqsmpbwgbh0m0";
"https://github.com/simplex-chat/aeson.git"."3eb66f9a68f103b5f1489382aad89f5712a64db7" = "0kilkx59fl6c3qy3kjczqvm8c3f4n3p0bdk9biyflf51ljnzp4yp";

View File

@@ -5,7 +5,7 @@ cabal-version: 1.12
-- see: https://github.com/sol/hpack
name: simplex-chat
version: 4.2.0
version: 4.2.1
category: Web, System, Services, Cryptography
homepage: https://github.com/simplex-chat/simplex-chat#readme
author: simplex.chat
@@ -101,7 +101,7 @@ library
, process ==1.6.*
, random >=1.1 && <1.3
, simple-logger ==0.1.*
, simplexmq >=3.0
, simplexmq >=3.4
, socks ==0.6.*
, sqlcipher-simple ==0.4.*
, stm ==2.5.*
@@ -144,7 +144,7 @@ executable simplex-bot
, random >=1.1 && <1.3
, simple-logger ==0.1.*
, simplex-chat
, simplexmq >=3.0
, simplexmq >=3.4
, socks ==0.6.*
, sqlcipher-simple ==0.4.*
, stm ==2.5.*
@@ -187,7 +187,7 @@ executable simplex-bot-advanced
, random >=1.1 && <1.3
, simple-logger ==0.1.*
, simplex-chat
, simplexmq >=3.0
, simplexmq >=3.4
, socks ==0.6.*
, sqlcipher-simple ==0.4.*
, stm ==2.5.*
@@ -231,7 +231,7 @@ executable simplex-chat
, random >=1.1 && <1.3
, simple-logger ==0.1.*
, simplex-chat
, simplexmq >=3.0
, simplexmq >=3.4
, socks ==0.6.*
, sqlcipher-simple ==0.4.*
, stm ==2.5.*
@@ -284,7 +284,7 @@ test-suite simplex-chat-test
, random >=1.1 && <1.3
, simple-logger ==0.1.*
, simplex-chat
, simplexmq >=3.0
, simplexmq >=3.4
, socks ==0.6.*
, sqlcipher-simple ==0.4.*
, stm ==2.5.*

View File

@@ -27,7 +27,6 @@ import Data.Bifunctor (bimap, first)
import qualified Data.ByteString.Base64 as B64
import Data.ByteString.Char8 (ByteString)
import qualified Data.ByteString.Char8 as B
import Data.Char (isSpace)
import Data.Either (fromRight)
import Data.Fixed (div')
import Data.Functor (($>))
@@ -64,7 +63,7 @@ import Simplex.Messaging.Client (defaultNetworkConfig)
import qualified Simplex.Messaging.Crypto as C
import Simplex.Messaging.Encoding
import Simplex.Messaging.Encoding.String
import Simplex.Messaging.Parsers (base64P, parseAll)
import Simplex.Messaging.Parsers (base64P)
import Simplex.Messaging.Protocol (ErrorType (..), MsgBody, MsgFlags (..), NtfServer)
import qualified Simplex.Messaging.Protocol as SMP
import qualified Simplex.Messaging.TMap as TM
@@ -219,7 +218,7 @@ execChatCommand s = case parseChatCommand s of
Right cmd -> either CRChatCmdError id <$> runExceptT (processChatCommand cmd)
parseChatCommand :: ByteString -> Either String ChatCommand
parseChatCommand = parseAll chatCommandP . B.dropWhileEnd isSpace
parseChatCommand = A.parseOnly chatCommandP
toView :: ChatMonad m => ChatResponse -> m ()
toView event = do
@@ -766,13 +765,13 @@ processChatCommand = \case
conn <- withStore' $ \db -> createDirectConnection db userId connId cReq ConnJoined $ incognitoProfile $> profileToSend
toView $ CRNewContactConnection conn
pure CRSentConfirmation
Connect (Just (ACR SCMContact cReq)) -> withUser $ \User {userId, profile} ->
Connect (Just (ACR SCMContact cReq)) -> withUser $ \user ->
-- [incognito] generate profile to send
connectViaContact userId cReq $ fromLocalProfile profile
connectViaContact user cReq
Connect Nothing -> throwChatError CEInvalidConnReq
ConnectSimplex -> withUser $ \User {userId, profile} ->
ConnectSimplex -> withUser $ \user ->
-- [incognito] generate profile to send
connectViaContact userId adminContactReq $ fromLocalProfile profile
connectViaContact user adminContactReq
DeleteContact cName -> withUser $ \user -> do
contactId <- withStore $ \db -> getContactIdByName db user cName
processChatCommand $ APIDeleteChat (ChatRef CTDirect contactId)
@@ -856,10 +855,11 @@ processChatCommand = \case
member <- withStore $ \db -> createNewContactMember db gVar user groupId contact memRole agentConnId cReq
sendInvitation member cReq
pure $ CRSentGroupInvitation gInfo contact member
Just member@GroupMember {groupMemberId, memberStatus}
| memberStatus == GSMemInvited ->
Just member@GroupMember {groupMemberId, memberStatus, memberRole = mRole}
| memberStatus == GSMemInvited -> do
unless (mRole == memRole) $ withStore' $ \db -> updateGroupMemberRole db user member memRole
withStore' (\db -> getMemberInvitation db user groupMemberId) >>= \case
Just cReq -> sendInvitation member cReq $> CRSentGroupInvitation gInfo contact member
Just cReq -> sendInvitation member {memberRole = memRole} cReq $> CRSentGroupInvitation gInfo contact member {memberRole = memRole}
Nothing -> throwChatError $ CEGroupCantResendInvitation gInfo cName
| otherwise -> throwChatError $ CEGroupDuplicateMember cName
APIJoinGroup groupId -> withUser $ \user@User {userId} -> do
@@ -1108,8 +1108,8 @@ processChatCommand = \case
CTDirect -> withStore $ \db -> getDirectChatItemIdByText db userId cId SMDSnd (safeDecodeUtf8 msg)
CTGroup -> withStore $ \db -> getGroupChatItemIdByText db user cId (Just localDisplayName) (safeDecodeUtf8 msg)
_ -> throwChatError $ CECommandError "not supported"
connectViaContact :: UserId -> ConnectionRequestUri 'CMContact -> Profile -> m ChatResponse
connectViaContact userId cReq@(CRContactUri ConnReqUriData {crClientData}) profile = withChatLock "connectViaContact" $ do
connectViaContact :: User -> ConnectionRequestUri 'CMContact -> m ChatResponse
connectViaContact user@User {userId} cReq@(CRContactUri ConnReqUriData {crClientData}) = withChatLock "connectViaContact" $ do
let cReqHash = ConnReqUriHash . C.sha256Hash $ strEncode cReq
withStore' (\db -> getConnReqContactXContactId db userId cReqHash) >>= \case
(Just contact, _) -> pure $ CRContactAlreadyExists contact
@@ -1123,7 +1123,7 @@ processChatCommand = \case
-- alternatively we can re-send the main profile even if incognito mode is enabled
incognito <- readTVarIO =<< asks incognitoMode
incognitoProfile <- if incognito then Just <$> liftIO generateRandomProfile else pure Nothing
let profileToSend = fromMaybe profile incognitoProfile
let profileToSend = userProfileToSend user incognitoProfile Nothing
connId <- withAgent $ \a -> joinConnection a True cReq $ directMessage (XContact profileToSend $ Just xContactId)
let groupLinkId = crClientData >>= decodeJSON >>= \(CRDataGroup gli) -> Just gli
conn <- withStore' $ \db -> createConnReqConnection db userId connId cReqHash xContactId incognitoProfile groupLinkId
@@ -1639,7 +1639,7 @@ processAgentMessage (Just user@User {userId}) corrId agentConnId agentMessage =
_ -> Nothing
processDirectMessage :: ACommand 'Agent -> Connection -> Maybe Contact -> m ()
processDirectMessage agentMsg conn@Connection {connId, viaUserContactLink, customUserProfileId} = \case
processDirectMessage agentMsg conn@Connection {connId, viaUserContactLink, groupLinkId, customUserProfileId} = \case
Nothing -> case agentMsg of
CONF confId _ connInfo -> do
-- [incognito] send saved profile
@@ -1734,6 +1734,7 @@ processAgentMessage (Just user@User {userId}) corrId agentConnId agentMessage =
toView $ CRContactConnected ct (fmap fromLocalProfile incognitoProfile)
setActive $ ActiveC c
showToast (c <> "> ") "connected"
forM_ groupLinkId $ \_ -> probeMatchingContacts ct $ contactConnIncognito ct
forM_ viaUserContactLink $ \userContactLinkId ->
withStore' (\db -> getUserContactLinkById db userId userContactLinkId) >>= \case
Just (UserContactLink {autoAccept = Just AutoAccept {autoReply = mc_}}, groupId_) -> do
@@ -1745,7 +1746,6 @@ processAgentMessage (Just user@User {userId}) corrId agentConnId agentMessage =
gVar <- asks idsDrg
groupConnIds <- createAgentConnectionAsync user CFCreateConnGrpInv True SCMInvitation
withStore $ \db -> createNewContactMemberAsync db gVar user groupId ct GRMember groupConnIds
probeMatchingContacts ct $ contactConnIncognito ct
_ -> pure ()
Just (gInfo@GroupInfo {membership}, m@GroupMember {activeConn}) ->
when (maybe False ((== ConnReady) . connStatus) activeConn) $ do
@@ -2420,13 +2420,12 @@ processAgentMessage (Just user@User {userId}) corrId agentConnId agentMessage =
toView . CRNewChatItem $ AChatItem SCTGroup SMDRcv (GroupChat gInfo) ci
processGroupInvitation :: Contact -> GroupInvitation -> RcvMessage -> MsgMeta -> m ()
processGroupInvitation ct@Contact {localDisplayName = c, activeConn = Connection {connId, customUserProfileId}} inv@GroupInvitation {fromMember = (MemberIdRole fromMemId fromRole), invitedMember = (MemberIdRole memId memRole), connRequest, groupLinkId} msg msgMeta = do
processGroupInvitation ct@Contact {localDisplayName = c, activeConn = Connection {customUserProfileId, groupLinkId = groupLinkId'}} inv@GroupInvitation {fromMember = (MemberIdRole fromMemId fromRole), invitedMember = (MemberIdRole memId memRole), connRequest, groupLinkId} msg msgMeta = do
checkIntegrityCreateItem (CDDirectRcv ct) msgMeta
when (fromRole < GRMember || fromRole < memRole) $ throwChatError (CEGroupContactRole c)
when (fromMemId == memId) $ throwChatError CEGroupDuplicateMemberId
-- [incognito] if direct connection with host is incognito, create membership using the same incognito profile
(gInfo@GroupInfo {groupId, localDisplayName, groupProfile, membership = membership@GroupMember {groupMemberId, memberId}}, hostId) <- withStore $ \db -> createGroupInvitation db user ct inv customUserProfileId
groupLinkId' <- withStore' $ \db -> getConnectionGroupLinkId db user connId
if sameGroupLinkId groupLinkId groupLinkId'
then do
connIds <- joinAgentConnectionAsync user True connRequest . directMessage $ XGrpAcpt memberId
@@ -2482,15 +2481,20 @@ processAgentMessage (Just user@User {userId}) corrId agentConnId agentMessage =
forM_ r . uncurry $ probeMatch c1
probeMatch :: Contact -> Contact -> Probe -> m ()
probeMatch c1@Contact {profile = p1} c2@Contact {profile = p2} probe =
when (fromLocalProfile p1 == fromLocalProfile p2) $ do
void . sendDirectContactMessage c1 $ XInfoProbeOk probe
mergeContacts c1 c2
probeMatch c1@Contact {contactId = cId1, profile = p1} c2@Contact {contactId = cId2, profile = p2} probe =
if profilesMatch (fromLocalProfile p1) (fromLocalProfile p2) && cId1 /= cId2
then do
void . sendDirectContactMessage c1 $ XInfoProbeOk probe
mergeContacts c1 c2
else messageWarning "probeMatch ignored: profiles don't match or same contact id"
xInfoProbeOk :: Contact -> Probe -> m ()
xInfoProbeOk c1 probe = do
xInfoProbeOk c1@Contact {contactId = cId1} probe = do
r <- withStore' $ \db -> matchSentProbe db userId c1 probe
forM_ r $ \c2 -> mergeContacts c1 c2
forM_ r $ \c2@Contact {contactId = cId2} ->
if cId1 /= cId2
then mergeContacts c1 c2
else messageWarning "xInfoProbeOk ignored: same contact id"
-- to party accepting call
xCallInv :: Contact -> CallId -> CallInvitation -> RcvMessage -> MsgMeta -> m ()
@@ -3137,7 +3141,7 @@ withStore action = do
chatCommandP :: Parser ChatCommand
chatCommandP =
A.choice
choice
[ "/mute " *> ((`ShowMessages` False) <$> chatNameP'),
"/unmute " *> ((`ShowMessages` True) <$> chatNameP'),
("/user " <|> "/u ") *> (CreateActiveUser <$> userProfile),
@@ -3279,6 +3283,7 @@ chatCommandP =
"/debug locks" $> DebugLocks
]
where
choice = A.choice . map (\p -> p <* A.takeWhile (== ' ') <* A.endOfInput)
imagePrefix = (<>) <$> "data:" <*> ("image/png;base64," <|> "image/jpg;base64,")
imageP = safeDecodeUtf8 <$> ((<>) <$> imagePrefix <*> (B64.encode <$> base64P))
chatTypeP = A.char '@' $> CTDirect <|> A.char '#' $> CTGroup <|> A.char ':' $> CTContactConnection

View File

@@ -59,7 +59,6 @@ module Simplex.Chat.Store
deleteGroupLink,
getGroupLink,
getGroupLinkId,
getConnectionGroupLinkId,
createOrUpdateContactRequest,
getContactRequest,
getContactRequestIdByName,
@@ -452,7 +451,7 @@ getConnReqContactXContactId db userId cReqHash = do
-- Contact
ct.contact_id, ct.contact_profile_id, ct.local_display_name, ct.via_group, cp.display_name, cp.full_name, cp.image, cp.local_alias, ct.contact_used, ct.enable_ntfs, cp.preferences, ct.user_preferences, ct.created_at, ct.updated_at,
-- Connection
c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.custom_user_profile_id, c.conn_status, c.conn_type, c.local_alias,
c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id, c.conn_status, c.conn_type, c.local_alias,
c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at
FROM contacts ct
JOIN contact_profiles cp ON ct.contact_profile_id = cp.contact_profile_id
@@ -528,7 +527,7 @@ createConnection_ db userId connType entityId acId viaContact viaUserContactLink
:. (ent ConnContact, ent ConnMember, ent ConnSndFile, ent ConnRcvFile, ent ConnUserContact, currentTs, currentTs)
)
connId <- insertedRowId db
pure Connection {connId, agentConnId = AgentConnId acId, connType, entityId, viaContact, viaUserContactLink, viaGroupLink, customUserProfileId, connLevel, connStatus = ConnNew, localAlias = "", createdAt = currentTs}
pure Connection {connId, agentConnId = AgentConnId acId, connType, entityId, viaContact, viaUserContactLink, viaGroupLink, groupLinkId = Nothing, customUserProfileId, connLevel, connStatus = ConnNew, localAlias = "", createdAt = currentTs}
where
ent ct = if connType == ct then entityId else Nothing
@@ -770,7 +769,7 @@ getUserAddressConnections db User {userId} = do
<$> DB.query
db
[sql|
SELECT c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.custom_user_profile_id,
SELECT c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id,
c.conn_status, c.conn_type, c.local_alias, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at
FROM connections c
JOIN user_contact_links uc ON c.user_contact_link_id = uc.user_contact_link_id
@@ -784,7 +783,7 @@ getUserContactLinks db User {userId} =
<$> DB.query
db
[sql|
SELECT c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.custom_user_profile_id,
SELECT c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id,
c.conn_status, c.conn_type, c.local_alias, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at,
uc.user_contact_link_id, uc.conn_req_contact, uc.group_id
FROM connections c
@@ -917,7 +916,7 @@ getGroupLinkConnection db User {userId} groupInfo@GroupInfo {groupId} =
DB.query
db
[sql|
SELECT c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.custom_user_profile_id,
SELECT c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id,
c.conn_status, c.conn_type, c.local_alias, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at
FROM connections c
JOIN user_contact_links uc ON c.user_contact_link_id = uc.user_contact_link_id
@@ -975,11 +974,6 @@ getGroupLinkId db User {userId} GroupInfo {groupId} =
fmap join . maybeFirstRow fromOnly $
DB.query db "SELECT group_link_id FROM user_contact_links WHERE user_id = ? AND group_id = ? LIMIT 1" (userId, groupId)
getConnectionGroupLinkId :: DB.Connection -> User -> Int64 -> IO (Maybe GroupLinkId)
getConnectionGroupLinkId db User {userId} connId =
fmap join . maybeFirstRow fromOnly $
DB.query db "SELECT group_link_id FROM connections WHERE user_id = ? AND connection_id = ? LIMIT 1" (userId, connId)
createOrUpdateContactRequest :: DB.Connection -> UserId -> Int64 -> InvitationId -> Profile -> Maybe XContactId -> ExceptT StoreError IO ContactOrRequest
createOrUpdateContactRequest db userId userContactLinkId invId Profile {displayName, fullName, image, preferences} xContactId_ =
liftIO (maybeM getContact' xContactId_) >>= \case
@@ -1025,7 +1019,7 @@ createOrUpdateContactRequest db userId userContactLinkId invId Profile {displayN
-- Contact
ct.contact_id, ct.contact_profile_id, ct.local_display_name, ct.via_group, cp.display_name, cp.full_name, cp.image, cp.local_alias, ct.contact_used, ct.enable_ntfs, cp.preferences, ct.user_preferences, ct.created_at, ct.updated_at,
-- Connection
c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.custom_user_profile_id, c.conn_status, c.conn_type, c.local_alias,
c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id, c.conn_status, c.conn_type, c.local_alias,
c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at
FROM contacts ct
JOIN contact_profiles cp ON ct.contact_profile_id = cp.contact_profile_id
@@ -1219,7 +1213,7 @@ getContactConnections db userId Contact {contactId} =
DB.query
db
[sql|
SELECT c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.custom_user_profile_id,
SELECT c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id,
c.conn_status, c.conn_type, c.local_alias, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at
FROM connections c
JOIN contacts ct ON ct.contact_id = c.contact_id
@@ -1231,14 +1225,14 @@ getContactConnections db userId Contact {contactId} =
type EntityIdsRow = (Maybe Int64, Maybe Int64, Maybe Int64, Maybe Int64, Maybe Int64)
type ConnectionRow = (Int64, ConnId, Int, Maybe Int64, Maybe Int64, Bool, Maybe Int64, ConnStatus, ConnType, LocalAlias) :. EntityIdsRow :. Only UTCTime
type ConnectionRow = (Int64, ConnId, Int, Maybe Int64, Maybe Int64, Bool, Maybe GroupLinkId, Maybe Int64, ConnStatus, ConnType, LocalAlias) :. EntityIdsRow :. Only UTCTime
type MaybeConnectionRow = (Maybe Int64, Maybe ConnId, Maybe Int, Maybe Int64, Maybe Int64, Maybe Bool, Maybe Int64, Maybe ConnStatus, Maybe ConnType, Maybe LocalAlias) :. EntityIdsRow :. Only (Maybe UTCTime)
type MaybeConnectionRow = (Maybe Int64, Maybe ConnId, Maybe Int, Maybe Int64, Maybe Int64, Maybe Bool, Maybe GroupLinkId, Maybe Int64, Maybe ConnStatus, Maybe ConnType, Maybe LocalAlias) :. EntityIdsRow :. Only (Maybe UTCTime)
toConnection :: ConnectionRow -> Connection
toConnection ((connId, acId, connLevel, viaContact, viaUserContactLink, viaGroupLink, customUserProfileId, connStatus, connType, localAlias) :. (contactId, groupMemberId, sndFileId, rcvFileId, userContactLinkId) :. Only createdAt) =
toConnection ((connId, acId, connLevel, viaContact, viaUserContactLink, viaGroupLink, groupLinkId, customUserProfileId, connStatus, connType, localAlias) :. (contactId, groupMemberId, sndFileId, rcvFileId, userContactLinkId) :. Only createdAt) =
let entityId = entityId_ connType
in Connection {connId, agentConnId = AgentConnId acId, connLevel, viaContact, viaUserContactLink, viaGroupLink, customUserProfileId, connStatus, connType, localAlias, entityId, createdAt}
in Connection {connId, agentConnId = AgentConnId acId, connLevel, viaContact, viaUserContactLink, viaGroupLink, groupLinkId, customUserProfileId, connStatus, connType, localAlias, entityId, createdAt}
where
entityId_ :: ConnType -> Maybe Int64
entityId_ ConnContact = contactId
@@ -1248,8 +1242,8 @@ toConnection ((connId, acId, connLevel, viaContact, viaUserContactLink, viaGroup
entityId_ ConnUserContact = userContactLinkId
toMaybeConnection :: MaybeConnectionRow -> Maybe Connection
toMaybeConnection ((Just connId, Just agentConnId, Just connLevel, viaContact, viaUserContactLink, Just viaGroupLink, customUserProfileId, Just connStatus, Just connType, Just localAlias) :. (contactId, groupMemberId, sndFileId, rcvFileId, userContactLinkId) :. Only (Just createdAt)) =
Just $ toConnection ((connId, agentConnId, connLevel, viaContact, viaUserContactLink, viaGroupLink, customUserProfileId, connStatus, connType, localAlias) :. (contactId, groupMemberId, sndFileId, rcvFileId, userContactLinkId) :. Only createdAt)
toMaybeConnection ((Just connId, Just agentConnId, Just connLevel, viaContact, viaUserContactLink, Just viaGroupLink, groupLinkId, customUserProfileId, Just connStatus, Just connType, Just localAlias) :. (contactId, groupMemberId, sndFileId, rcvFileId, userContactLinkId) :. Only (Just createdAt)) =
Just $ toConnection ((connId, agentConnId, connLevel, viaContact, viaUserContactLink, viaGroupLink, groupLinkId, customUserProfileId, connStatus, connType, localAlias) :. (contactId, groupMemberId, sndFileId, rcvFileId, userContactLinkId) :. Only createdAt)
toMaybeConnection _ = Nothing
getMatchingContacts :: DB.Connection -> UserId -> Contact -> IO [Contact]
@@ -1419,7 +1413,7 @@ getConnectionEntity db user@User {userId, userContactId} agentConnId = do
DB.query
db
[sql|
SELECT connection_id, agent_conn_id, conn_level, via_contact, via_user_contact_link, via_group_link, custom_user_profile_id,
SELECT connection_id, agent_conn_id, conn_level, via_contact, via_user_contact_link, via_group_link, group_link_id, custom_user_profile_id,
conn_status, conn_type, local_alias, contact_id, group_member_id, snd_file_id, rcv_file_id, user_contact_link_id, created_at
FROM connections
WHERE user_id = ? AND agent_conn_id = ?
@@ -1516,7 +1510,7 @@ getConnectionById db User {userId} connId = ExceptT $ do
DB.query
db
[sql|
SELECT connection_id, agent_conn_id, conn_level, via_contact, via_user_contact_link, via_group_link, custom_user_profile_id,
SELECT connection_id, agent_conn_id, conn_level, via_contact, via_user_contact_link, via_group_link, group_link_id, custom_user_profile_id,
conn_status, conn_type, local_alias, contact_id, group_member_id, snd_file_id, rcv_file_id, user_contact_link_id, created_at
FROM connections
WHERE user_id = ? AND connection_id = ?
@@ -1561,7 +1555,7 @@ getGroupAndMember db User {userId, userContactId} groupMemberId =
-- from GroupMember
m.group_member_id, m.group_id, m.member_id, m.member_role, m.member_category, m.member_status,
m.invited_by, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.image, p.local_alias, p.preferences,
c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.custom_user_profile_id,
c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id,
c.conn_status, c.conn_type, c.local_alias, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at
FROM group_members m
JOIN contact_profiles p ON p.contact_profile_id = COALESCE(m.member_profile_id, m.contact_profile_id)
@@ -1736,8 +1730,24 @@ deleteGroupConnectionsAndFiles db User {userId} GroupInfo {groupId} members = do
deleteGroupItemsAndMembers :: DB.Connection -> User -> GroupInfo -> [GroupMember] -> IO ()
deleteGroupItemsAndMembers db user@User {userId} GroupInfo {groupId} members = do
DB.execute db "DELETE FROM chat_items WHERE user_id = ? AND group_id = ?" (userId, groupId)
void $ runExceptT cleanupHostGroupLinkConn_ -- to allow repeat connection via the same group link if one was used
DB.execute db "DELETE FROM group_members WHERE user_id = ? AND group_id = ?" (userId, groupId)
forM_ members $ \m -> cleanupMemberContactAndProfile_ db user m
where
cleanupHostGroupLinkConn_ = do
hostId <- getHostMemberId_ db user groupId
liftIO $
DB.execute
db
[sql|
UPDATE connections SET via_contact_uri_hash = NULL, xcontact_id = NULL
WHERE user_id = ? AND via_group_link = 1 AND contact_id IN (
SELECT contact_id
FROM group_members
WHERE user_id = ? AND group_member_id = ?
)
|]
(userId, userId, hostId)
deleteGroup :: DB.Connection -> User -> GroupInfo -> IO ()
deleteGroup db User {userId} GroupInfo {groupId, localDisplayName} = do
@@ -1803,7 +1813,7 @@ getGroupMember db user@User {userId} groupId groupMemberId =
SELECT
m.group_member_id, m.group_id, m.member_id, m.member_role, m.member_category, m.member_status,
m.invited_by, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.image, p.local_alias, p.preferences,
c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.custom_user_profile_id,
c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id,
c.conn_status, c.conn_type, c.local_alias, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at
FROM group_members m
JOIN contact_profiles p ON p.contact_profile_id = COALESCE(m.member_profile_id, m.contact_profile_id)
@@ -1825,7 +1835,7 @@ getGroupMembers db user@User {userId, userContactId} GroupInfo {groupId} = do
SELECT
m.group_member_id, m.group_id, m.member_id, m.member_role, m.member_category, m.member_status,
m.invited_by, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.image, p.local_alias, p.preferences,
c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.custom_user_profile_id,
c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id,
c.conn_status, c.conn_type, c.local_alias, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at
FROM group_members m
JOIN contact_profiles p ON p.contact_profile_id = COALESCE(m.member_profile_id, m.contact_profile_id)
@@ -1847,7 +1857,7 @@ getGroupMembersForExpiration db user@User {userId, userContactId} GroupInfo {gro
SELECT
m.group_member_id, m.group_id, m.member_id, m.member_role, m.member_category, m.member_status,
m.invited_by, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.image, p.local_alias,
c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.custom_user_profile_id,
c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id,
c.conn_status, c.conn_type, c.local_alias, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at
FROM group_members m
JOIN contact_profiles p ON p.contact_profile_id = COALESCE(m.member_profile_id, m.contact_profile_id)
@@ -1972,7 +1982,7 @@ getContactViaMember db User {userId} GroupMember {groupMemberId} =
-- Contact
ct.contact_id, ct.contact_profile_id, ct.local_display_name, ct.via_group, cp.display_name, cp.full_name, cp.image, cp.local_alias, ct.contact_used, ct.enable_ntfs, cp.preferences, ct.user_preferences, ct.created_at, ct.updated_at,
-- Connection
c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.custom_user_profile_id, c.conn_status, c.conn_type, c.local_alias,
c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id, c.conn_status, c.conn_type, c.local_alias,
c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at
FROM contacts ct
JOIN contact_profiles cp ON cp.contact_profile_id = ct.contact_profile_id
@@ -2282,7 +2292,7 @@ getViaGroupMember db User {userId, userContactId} Contact {contactId} =
-- via GroupMember
m.group_member_id, m.group_id, m.member_id, m.member_role, m.member_category, m.member_status,
m.invited_by, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.image, p.local_alias, p.preferences,
c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.custom_user_profile_id,
c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id,
c.conn_status, c.conn_type, c.local_alias, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at
FROM group_members m
JOIN contacts ct ON ct.contact_id = m.contact_id
@@ -2314,7 +2324,7 @@ getViaGroupContact db User {userId} GroupMember {groupMemberId} =
[sql|
SELECT
ct.contact_id, ct.contact_profile_id, ct.local_display_name, p.display_name, p.full_name, p.image, p.local_alias, ct.via_group, ct.contact_used, ct.enable_ntfs, p.preferences, ct.user_preferences, ct.created_at, ct.updated_at,
c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.custom_user_profile_id,
c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id,
c.conn_status, c.conn_type, c.local_alias, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at
FROM contacts ct
JOIN contact_profiles p ON ct.contact_profile_id = p.contact_profile_id
@@ -3192,7 +3202,7 @@ getDirectChatPreviews_ db User {userId} = do
-- Contact
ct.contact_id, ct.contact_profile_id, ct.local_display_name, ct.via_group, cp.display_name, cp.full_name, cp.image, cp.local_alias, ct.contact_used, ct.enable_ntfs, cp.preferences, ct.user_preferences, ct.created_at, ct.updated_at,
-- Connection
c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.custom_user_profile_id, c.conn_status, c.conn_type, c.local_alias,
c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id, c.conn_status, c.conn_type, c.local_alias,
c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at,
-- ChatStats
COALESCE(ChatStats.UnreadCount, 0), COALESCE(ChatStats.MinUnread, 0), ct.unread_chat,
@@ -3516,7 +3526,7 @@ getContact db userId contactId =
-- Contact
ct.contact_id, ct.contact_profile_id, ct.local_display_name, ct.via_group, cp.display_name, cp.full_name, cp.image, cp.local_alias, ct.contact_used, ct.enable_ntfs, cp.preferences, ct.user_preferences, ct.created_at, ct.updated_at,
-- Connection
c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.custom_user_profile_id, c.conn_status, c.conn_type, c.local_alias,
c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id, c.conn_status, c.conn_type, c.local_alias,
c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at
FROM contacts ct
JOIN contact_profiles cp ON ct.contact_profile_id = cp.contact_profile_id

View File

@@ -516,6 +516,13 @@ instance ToJSON Profile where
toJSON = J.genericToJSON J.defaultOptions {J.omitNothingFields = True}
toEncoding = J.genericToEncoding J.defaultOptions {J.omitNothingFields = True}
-- check if profiles match ignoring preferences
profilesMatch :: Profile -> Profile -> Bool
profilesMatch
Profile {displayName = n1, fullName = fn1, image = i1}
Profile {displayName = n2, fullName = fn2, image = i2} =
n1 == n2 && fn1 == fn2 && i1 == i2
data IncognitoProfile = NewIncognito Profile | ExistingIncognito LocalProfile
type LocalAlias = Text
@@ -1124,6 +1131,7 @@ data Connection = Connection
viaContact :: Maybe Int64, -- group member contact ID, if not direct connection
viaUserContactLink :: Maybe Int64, -- user contact link ID, if connected via "user address"
viaGroupLink :: Bool, -- whether contact connected via group link
groupLinkId :: Maybe GroupLinkId,
customUserProfileId :: Maybe Int64,
connType :: ConnType,
connStatus :: ConnStatus,

View File

@@ -49,7 +49,7 @@ extra-deps:
# - simplexmq-1.0.0@sha256:34b2004728ae396e3ae449cd090ba7410781e2b3cefc59259915f4ca5daa9ea8,8561
# - ../simplexmq
- github: simplex-chat/simplexmq
commit: 029fc6e781d78249d0b3a17edff7416ef22afed1
commit: d2b88a1baa390ec64b6535e32ce69f26f53f4d7a
# - ../direct-sqlcipher
- github: simplex-chat/direct-sqlcipher
commit: 34309410eb2069b029b8fc1872deb1e0db123294

View File

@@ -17,7 +17,7 @@ import qualified Data.Aeson as J
import qualified Data.ByteString.Char8 as B
import qualified Data.ByteString.Lazy.Char8 as LB
import Data.Char (isDigit)
import Data.List (isPrefixOf)
import Data.List (isPrefixOf, isSuffixOf)
import Data.Maybe (fromMaybe)
import Data.String
import qualified Data.Text as T
@@ -61,6 +61,7 @@ chatTests = do
it "create group with the same displayName" testGroupSameName
it "invitee delete group when in status invited" testGroupDeleteWhenInvited
it "re-add member in status invited" testGroupReAddInvited
it "re-add member in status invited, change role" testGroupReAddInvitedChangeRole
it "delete contact before they accept group invitation, contact joins group" testGroupDeleteInvitedContact
it "member profile is kept when deleting group if other groups have this member" testDeleteGroupMemberProfileKept
it "remove contact from group and add again" testGroupRemoveAdd
@@ -143,6 +144,7 @@ chatTests = do
it "set chat item TTL" testSetChatItemTTL
describe "group links" $ do
it "create group link, join via group link" testGroupLink
it "delete group, re-join via same link" testGroupLinkDeleteGroupRejoin
it "sending message to contact created via group link marks it used" testGroupLinkContactUsed
it "create group link, join via group link - incognito membership" testGroupLinkIncognitoMembership
describe "queue rotation" $ do
@@ -857,6 +859,46 @@ testGroupReAddInvited =
bob <## "use /j team_1 to accept"
]
testGroupReAddInvitedChangeRole :: IO ()
testGroupReAddInvitedChangeRole =
testChat2 aliceProfile bobProfile $
\alice bob -> do
connectUsers alice bob
alice ##> "/g team"
alice <## "group #team is created"
alice <## "use /a team <name> to add members"
alice ##> "/a team bob"
concurrentlyN_
[ alice <## "invitation to join the group #team sent to bob",
do
bob <## "#team: alice invites you to join the group as admin"
bob <## "use /j team to accept"
]
-- alice re-adds bob, he sees it as the same group
alice ##> "/a team bob owner"
concurrentlyN_
[ alice <## "invitation to join the group #team sent to bob",
do
bob <## "#team: alice invites you to join the group as owner"
bob <## "use /j team to accept"
]
-- bob joins as owner
bob ##> "/j team"
concurrently_
(alice <## "#team: bob joined the group")
(bob <## "#team: you joined the group")
bob ##> "/d #team"
concurrentlyN_
[ bob <## "#team: you deleted the group",
do
alice <## "#team: bob deleted the group"
alice <## "use /d #team to delete the local copy of the group"
]
bob ##> "#team hi"
bob <## "no group #team"
alice ##> "/d #team"
alice <## "#team: you deleted the group"
testGroupDeleteInvitedContact :: IO ()
testGroupDeleteInvitedContact =
testChat2 aliceProfile bobProfile $
@@ -3426,6 +3468,11 @@ testGroupLink =
alice ##> "/show link #team"
alice <## "no group link, to create: /create link #team"
alice ##> "/create link #team"
_ <- getGroupLink alice "team" True
alice ##> "/delete link #team"
alice <## "Group link is deleted - joined members will remain connected."
alice <## "To create a new group link use /create link #team"
alice ##> "/create link #team"
gLink <- getGroupLink alice "team" True
alice ##> "/show link #team"
_ <- getGroupLink alice "team" False
@@ -3471,18 +3518,20 @@ testGroupLink =
alice <## "cath_1 (Catherine): accepting request to join group #team..."
-- if contact existed it is merged
concurrentlyN_
[ do
alice <## "cath_1 (Catherine): contact is connected"
alice <## "cath_1 invited to group #team via your group link"
alice <## "contact cath_1 is merged into cath"
alice <## "use @cath <message> to send messages"
alice <## "#team: cath joined the group",
do
cath <## "alice_1 (Alice): contact is connected"
cath <## "contact alice_1 is merged into alice"
cath <## "use @alice <message> to send messages"
cath <## "#team: you joined the group"
cath <## "#team: member bob (Bob) is connected",
[ alice
<### [ "cath_1 (Catherine): contact is connected",
"contact cath_1 is merged into cath",
"use @cath <message> to send messages",
EndsWith "invited to group #team via your group link",
EndsWith "joined the group"
],
cath
<### [ "alice_1 (Alice): contact is connected",
"contact alice_1 is merged into alice",
"use @alice <message> to send messages",
"#team: you joined the group",
"#team: member bob (Bob) is connected"
],
do
bob <## "#team: alice added cath (Catherine) to the group (connecting...)"
bob <## "#team: new member cath is connected"
@@ -3512,6 +3561,62 @@ testGroupLink =
alice ##> "/show link #team"
alice <## "no group link, to create: /create link #team"
testGroupLinkDeleteGroupRejoin :: IO ()
testGroupLinkDeleteGroupRejoin =
testChat2 aliceProfile bobProfile $
\alice bob -> do
alice ##> "/g team"
alice <## "group #team is created"
alice <## "use /a team <name> to add members"
alice ##> "/create link #team"
gLink <- getGroupLink alice "team" True
bob ##> ("/c " <> gLink)
bob <## "connection request sent!"
alice <## "bob (Bob): accepting request to join group #team..."
concurrentlyN_
[ do
alice <## "bob (Bob): contact is connected"
alice <## "bob invited to group #team via your group link"
alice <## "#team: bob joined the group",
do
bob <## "alice (Alice): contact is connected"
bob <## "#team: you joined the group"
]
-- use contact so it's not deleted when deleting group
bob <##> alice
bob ##> "/l team"
concurrentlyN_
[ do
bob <## "#team: you left the group"
bob <## "use /d #team to delete the group",
alice <## "#team: bob left the group"
]
bob ##> "/d #team"
bob <## "#team: you deleted the group"
-- re-join via same link
bob ##> ("/c " <> gLink)
bob <## "connection request sent!"
alice <## "bob_1 (Bob): accepting request to join group #team..."
concurrentlyN_
[ alice
<### [ "bob_1 (Bob): contact is connected",
"contact bob_1 is merged into bob",
"use @bob <message> to send messages",
EndsWith "invited to group #team via your group link",
EndsWith "joined the group"
],
bob
<### [ "alice_1 (Alice): contact is connected",
"contact alice_1 is merged into alice",
"use @alice <message> to send messages",
"#team: you joined the group"
]
]
alice #> "#team hello"
bob <# "#team alice> hello"
bob #> "#team hi there"
alice <# "#team bob> hi there"
testGroupLinkContactUsed :: IO ()
testGroupLinkContactUsed =
testChat2 aliceProfile bobProfile $
@@ -3519,14 +3624,8 @@ testGroupLinkContactUsed =
alice ##> "/g team"
alice <## "group #team is created"
alice <## "use /a team <name> to add members"
alice ##> "/show link #team"
alice <## "no group link, to create: /create link #team"
alice ##> "/create link #team"
gLink <- getGroupLink alice "team" True
alice ##> "/show link #team"
_ <- getGroupLink alice "team" False
alice ##> "/create link #team"
alice <## "you already have link for this group, to show: /show link #team"
bob ##> ("/c " <> gLink)
bob <## "connection request sent!"
alice <## "bob (Bob): accepting request to join group #team..."
@@ -3903,7 +4002,7 @@ cc <##.. ls = do
unless prefix $ print ("expected to start from one of: " <> show ls, ", got: " <> l)
prefix `shouldBe` True
data ConsoleResponse = ConsoleString String | WithTime String
data ConsoleResponse = ConsoleString String | WithTime String | EndsWith String
deriving (Show)
instance IsString ConsoleResponse where fromString = ConsoleString
@@ -3922,6 +4021,7 @@ getInAnyOrder f cc ls = do
expected l = \case
ConsoleString s -> l == s
WithTime s -> dropTime_ l == Just s
EndsWith s -> s `isSuffixOf` l
(<###) :: TestCC -> [ConsoleResponse] -> Expectation
(<###) = getInAnyOrder id

View File

@@ -7,7 +7,9 @@ const uri = require('fast-uri')
module.exports = function (ty) {
// Keeps the same directory structure.
ty.addPassthroughCopy("src/assets/")
ty.addPassthroughCopy("src/fonts")
ty.addPassthroughCopy("src/img")
ty.addPassthroughCopy("src/video")
ty.addPassthroughCopy("src/css")
ty.addPassthroughCopy("src/js")
ty.addPassthroughCopy("src/contact/*.js")

View File

@@ -7,7 +7,7 @@
"build": "npm run build:js && npm run build:eleventy && npm run build:tailwind",
"start": "npx eleventy --serve",
"test": "echo \"Error: no test specified\" && exit 1",
"build:js": "cp ./node_modules/qrcode/build/qrcode.js ./src/contact/ && ./copy_call.sh",
"build:js": "cp ./node_modules/qrcode/build/qrcode.js ./src/js/ && ./copy_call.sh",
"build:eleventy": "eleventy",
"build:tailwind": "npx tailwindcss -i ./tailwind.css -o ./_site/css/tailwind.css",
"watch:tailwind": "npx tailwindcss -i ./tailwind.css -o ./_site/css/tailwind.css --watch"

View File

@@ -0,0 +1,52 @@
{
"sections": [
{
"id": 1,
"title": "E2E-encrypted messages with markdown and editing",
"imgLight": "/img/new/feature-1.svg",
"imgDark": "/img/new/feature-1-dark.svg"
},
{
"id": 2,
"title": "E2E-encrypted<br>images and files",
"imgLight": "/img/new/feature-2.svg",
"imgDark": "/img/new/feature-2-dark.svg"
},
{
"id": 3,
"title": "Decentralized secret groups &mdash;<br>only users know they exist",
"imgLight": "/img/new/feature-3.svg",
"imgDark": "/img/new/feature-3-dark.svg"
},
{
"id": 4,
"title": "E2E-encrypted voice messages <em>(coming soon)</em>",
"imgLight": "/img/new/feature-4.svg",
"imgDark": "/img/new/feature-4-dark.svg"
},
{
"id": 5,
"title": "Disappearing secret conversations <em>(coming soon)</em>",
"imgLight": "/img/new/feature-5.svg",
"imgDark": "/img/new/feature-5-dark.svg"
},
{
"id": 6,
"title": "E2E-encrypted<br>audio and video calls",
"imgLight": "/img/new/feature-6.svg",
"imgDark": "/img/new/feature-6-dark.svg"
},
{
"id": 7,
"title": "Portable encrypted database &mdash; move your profile to another device",
"imgLight": "/img/new/feature-7.svg",
"imgDark": "/img/new/feature-7-dark.svg"
},
{
"id": 8,
"title": "Incognito mode &mdash;<br>unique to SimpleX Chat",
"imgLight": "/img/new/feature-8.svg",
"imgDark": "/img/new/feature-8-dark.svg"
}
]
}

View File

@@ -0,0 +1,26 @@
{
"sections": [
{
"id": 1,
"imgLight": "/img/new/explained-2.svg",
"imgDark": "/img/new/explained-2.svg",
"overlayContent": {
"overlayId": "hero-overlay-1",
"title": "How does SimpleX work?",
"showImage": true,
"contentBody": "overlay_content/hero/card_1.html"
}
},
{
"id": 2,
"imgLight": "/img/new/explained-1.svg",
"imgDark": "/img/new/explained-1.svg",
"overlayContent": {
"overlayId": "hero-overlay-2",
"title": "Why user IDs are bad for privacy?",
"showImage": true,
"contentBody": "overlay_content/hero/card_2.html"
}
}
]
}

View File

@@ -0,0 +1,15 @@
{
"sections": [
{
"id": 1,
"imgLight": "/img/new/network-1.svg",
"imgDark": "/img/new/network-1-dark.svg",
"overlayContent": {
"overlayId": "simplex-network-overlay-1",
"title": "Comparison with P2P messaging protocols",
"showImage": true,
"contentBody": "overlay_content/simplex_network/card_1.html"
}
}
]
}

View File

@@ -0,0 +1,103 @@
{
"sections": [
{
"id": 10,
"title": "Temporary anonymous pairwise identifiers",
"imgLight": "/img/new/private-10.svg",
"imgDark": "/img/new/private-10-dark.svg",
"points": [
"SimpleX uses temporary anonymous pairwise addresses and credentials for each user contact or group member.",
"It allows to deliver messages without user profile identifiers, providing better meta-data privacy than alternatives."
]
},
{
"id": 6,
"title": "Out-of-band<br>key exchange",
"imgLight": "/img/new/private-6.svg",
"imgDark": "/img/new/private-6-dark.svg",
"points": [
"Many communication platforms are vulnerable to MITM attacks by servers or network providers.",
"To prevent it SimpleX apps pass one-time keys out-of-band, when you share an address as a link or a QR code."
]
},
{
"id": 1,
"title": "2-layers of<br>end-to-end encryption",
"imgLight": "/img/new/private-1.svg",
"imgDark": "/img/new/private-1-dark.svg",
"points": [
"Double-ratchet protocol &mdash;<br>OTR messaging with perfect forward secrecy and break-in recovery.",
"NaCL cryptobox in each queue to prevent traffic correlation between message queues if TLS is compromised."
]
},
{
"id": 7,
"title": "Message integrity<br>verification",
"imgLight": "/img/new/private-7.svg",
"imgDark": "/img/new/private-7-dark.svg",
"points": [
"To guarantee integrity the messages are sequentially numbered and include the hash of the previous message.",
"If any message is added, removed or changed the recipient will be alerted."
]
},
{
"id": 2,
"title": "Additional layer of<br>server encryption",
"imgLight": "/img/new/private-2.svg",
"imgDark": "/img/new/private-2-dark.svg",
"points": [
"Additional layer of server encryption for delivery to the recipient, to prevent the correlation between received and sent server traffic if TLS is compromised."
]
},
{
"id": 8,
"title": "Message mixing<br>to reduce correlation",
"imgLight": "/img/new/private-8.svg",
"imgDark": "/img/new/private-8-dark.svg",
"points": [
"SimpleX servers act as low latency mix nodes &mdash; the incoming and outgoing messages have different order."
]
},
{
"id": 3,
"title": "Secure authenticated<br>TLS transport",
"imgLight": "/img/new/private-3.svg",
"imgDark": "/img/new/private-3-dark.svg",
"points": [
"Only TLS 1.2/1.3 with strong algorithms is used for client-server connections.",
"Server fingerprint and channel binding prevent MITM and replay attacks.",
"Connection resumption is disabled to prevent session attacks."
]
},
{
"id": 4,
"title": "Optional<br>access via Tor",
"imgLight": "/img/new/private-4.svg",
"imgDark": "/img/new/private-4-dark.svg",
"points": [
"To protect your IP address you can access the servers via Tor or some other transport overlay network.",
"To use SimpleX via Tor please install <a href=\"https://guardianproject.info/apps/org.torproject.android/\" target=\"_blank\">Orbot app</a> and enable SOCKS5 proxy (or VPN <a href=\"https://apps.apple.com/us/app/orbot/id1609461599?platform=iphone\" target=\"_blank\">on iOS</a>)."
]
},
{
"id": 9,
"title": "Unidirectional<br>message queues",
"imgLight": "/img/new/private-9.svg",
"imgDark": "/img/new/private-9-dark.svg",
"points": [
"Each message queue passes messages in one direction, with the different send and receive addresses.",
"It reduces the attack vectors, compared with traditional message brokers, and available meta-data."
]
},
{
"id": 5,
"title": "Multiple layers of<br>content padding",
"imgLight": "/img/new/private-5.svg",
"imgDark": "/img/new/private-5-dark.svg",
"points": [
"SimpleX uses content padding for each encryption layer to frustrate message size attacks.",
"It makes messages of different sizes look the same to the servers and network observers."
]
}
]
}

View File

@@ -0,0 +1,43 @@
{
"sections": [
{
"id": 1,
"title": "Advertising and price discrimination",
"imgLight": "/img/new/privacy-section-1.svg",
"imgDark": "/img/new/privacy-section-1.svg",
"overlayContent": {
"overlayId": "why-privacy-matters-1",
"title": "Privacy saves you money",
"linkText": "Privacy saves you money",
"showImage": false,
"contentBody": "overlay_content/why_privacy_matters/card_1.html"
}
},
{
"id": 2,
"title": "Manipulation of elections",
"imgLight": "/img/new/privacy-section-2.svg",
"imgDark": "/img/new/privacy-section-2.svg",
"overlayContent": {
"overlayId": "why-privacy-matters-2",
"title": "Privacy gives you power",
"linkText": "Privacy gives you power",
"showImage": false,
"contentBody": "overlay_content/why_privacy_matters/card_2.html"
}
},
{
"id": 3,
"title": "Prosecution due to innocent association",
"imgLight": "/img/new/privacy-section-3.svg",
"imgDark": "/img/new/privacy-section-3.svg",
"overlayContent": {
"overlayId": "why-privacy-matters-3",
"title": "Privacy protects your freedom",
"linkText": "Privacy protects your freedom",
"showImage": false,
"contentBody": "overlay_content/why_privacy_matters/card_3.html"
}
}
]
}

View File

@@ -0,0 +1,60 @@
{
"sections": [
{
"id": 1,
"title": "You have complete privacy",
"descBody": "sections/simplex_unique/card_1.html",
"imgLight": "/img/new/unique-section-1.png",
"imgDark": "/img/new/unique-section-1-dark.png",
"overlayContent": {
"overlayId": "why-simplex-is-unique-1",
"title": "Full privacy of your identity, profile, contacts and metadata",
"linkText": "Learn more",
"showImage": true,
"contentBody": "overlay_content/why_simplex_is_unique/card_1.html"
}
},
{
"id": 2,
"title": "You are protected<br>from spam and abuse",
"descBody": "sections/simplex_unique/card_2.html",
"imgLight": "/img/new/unique-section-2.png",
"imgDark": "/img/new/unique-section-2-dark.png",
"overlayContent": {
"overlayId": "why-simplex-is-unique-2",
"title": "The best protection from spam and abuse",
"linkText": "Learn more",
"showImage": true,
"contentBody": "overlay_content/why_simplex_is_unique/card_2.html"
}
},
{
"id": 3,
"title": "You control your data",
"descBody": "sections/simplex_unique/card_3.html",
"imgLight": "/img/new/unique-section-3.png",
"imgDark": "/img/new/unique-section-3-dark.png",
"overlayContent": {
"overlayId": "why-simplex-is-unique-3",
"title": "Ownership, control and security of your data",
"linkText": "Learn more",
"showImage": true,
"contentBody": "overlay_content/why_simplex_is_unique/card_3.html"
}
},
{
"id": 4,
"title": "You own SimpleX network",
"descBody": "sections/simplex_unique/card_4.html",
"imgLight": "/img/new/unique-section-4.png",
"imgDark": "/img/new/unique-section-4-dark.png",
"overlayContent": {
"overlayId": "why-simplex-is-unique-4",
"title": "Fully decentralised &mdash; users own the SimpleX network",
"linkText": "Learn more",
"showImage": true,
"contentBody": "overlay_content/why_simplex_is_unique/card_4.html"
}
}
]
}

View File

@@ -0,0 +1,7 @@
<p>Added:</p>
<ul>
<li>protect your chats with SimpleX Lock</li>
<li>save data and avoid sharing you are online &mdash; manually accepting images</li>
<li>avoid visiting websites of the links you send &mdash; disable link previews</li>
<li>identify any lost messages in the chat</li>
</ul>

View File

@@ -0,0 +1,9 @@
<p>Added:</p>
<ul>
<li>chat database export and import</li>
<li>end-to-end encrypted audio/video calls</li>
<li>protocol privacy and performance improvements</li>
</ul>
<p style="margin-top: 6px">
Also read why having users' identifiers that all other platforms use is bad for the users, and how they reduce privacy.
</p>

View File

@@ -0,0 +1,7 @@
<p>Added:</p>
<ul>
<li>terminal app: access to messaging servers via SOCKS5 proxy (e.g., Tor).</li>
<li>mobile apps: join and leave chat groups.</li>
<li>optimized battery and traffic usage &mdash; up to 90x reduction!</li>
<li>two docker configurations for self-hosted SMP servers.</li>
</ul>

View File

@@ -0,0 +1,8 @@
<p>Added:</p>
<ul>
<li>finally, secret chat groups &mdash; nobody but members know they exist!</li>
<li>access to messaging servers via Tor on all platforms</li>
<li>advanced network settings to optimize traffic usage</li>
<li>published chat protocol</li>
<li>new app icons</li>
</ul>

View File

@@ -0,0 +1,8 @@
<p>Added:</p>
<ul>
<li>Incognito mode &mdash; use a new random profile name for each contact</li>
<li>use .onion server addresses with Tor</li>
<li>endless scrolling and search</li>
<li>choose accent color and dark mode</li>
<li>reduced APK size for direct download and in F-Droid repo from 200 to 46Mb!</li>
</ul>

View File

@@ -0,0 +1,10 @@
<p>Added:</p>
<ul>
<li>encrypted local chat database &mdash; if you already use the app, you can encrypt the database in the app settings</li>
<li>support for self-hosted WebRTC ICE servers</li>
<li>improved stability of creating new connections: more reliable groups, files and contacts</li>
<li>deleting files and media &mdash; for security and to save storage</li>
<li>For developers &mdash; TypeScript SDK for integrating with SimpleX Chat</li>
<li>support animated images in Android app</li>
<li>German language in mobile apps UI</li>
</ul>

View File

@@ -0,0 +1,19 @@
<p><em>"Have you been audited or should we just ignore you?"</em></p>
<p class="mb-[12px]">
SimpleX Chat security has been assessed by
<a href="https://www.trailofbits.com/about" target="_blank">Trail of Bits</a>,
4 issues were identified, and 3 of them are fixed in v4.2.
</p>
<p class="mb-[12px]">The new website is live: <a href="https://simplex.chat">https://simplex.chat</a></p>
<p>v4.2 is released:</p>
<ul>
<li>group links &mdash; admins can create the links for new members to join</li>
<li>auto-accept contact requests + configure whether to accept incognito and welcome message</li>
<li>small things: change group member role, mark chat as unread, send stickers and GIFs from Android keyboards.</li>
<li>manually switch contact or member to another address / server (BETA)</li>
<li>receive files faster (BETA)</li>
</ul>

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