Compare commits

...

170 Commits

Author SHA1 Message Date
Evgeny Poberezkin 0d1a70af34 android: v4.2.2 (70) 2022-11-22 14:11:23 +00:00
Evgeny Poberezkin ab91c54080 ios: version 4.2.2 (92) 2022-11-22 12:44:40 +00:00
Evgeny Poberezkin 6b71cc59c8 blog: updates (#1400) 2022-11-22 08:49:33 +00:00
Evgeny Poberezkin 33a866463d core: fix sql that was doubling a group in the list of chats when member joined the group twice (#1378) 2022-11-21 10:37:11 +00:00
Evgeny Poberezkin 02fa81e8aa mobile: show unknown content with attached file as file item (for partial forward compatibility with voice and video messages) (#1394)
* mobile: show unknown content with attached file as file item (for partial forward compatibility with voice and video messages)

* fix

* android: show unknown files
2022-11-21 07:42:36 +00:00
Daniel Nathan Gray a8b9200c9a Fix dead links to audit (#1374) (#1374) 2022-11-17 17:26:19 +00:00
Evgeny Poberezkin 002a081b3b readme: user groups (#1357)
* readme: user groups

* corrections

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

Co-authored-by: JRoberts <8711996+jr-simplex@users.noreply.github.com>
2022-11-12 14:21:33 +00:00
JRoberts c2b76a75b5 android: fix crash on restoring from backup (#1350)
* Restoring app's data from backup tools will still allow to enter passphrase instead of just crashing

(cherry picked from commit 256243dc8c)

* corrections

Co-authored-by: Avently <7953703+avently@users.noreply.github.com>
2022-11-12 17:19:56 +04:00
Evgeny Poberezkin 2742fc3ca9 site: make onion location http 2022-11-12 12:08:37 +00:00
Evgeny Poberezkin f3731799bc readme: update roadmap (#1355) 2022-11-12 12:07:11 +00:00
Evgeny Poberezkin 7a78dfd3e3 site: onion location (#1356) 2022-11-12 12:04:23 +00:00
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
JRoberts b2f663dde3 android: version 4.2-beta.3 (66) 2022-11-05 20:25:29 +04:00
JRoberts 038b936bfe ios: version 4.2 (88) 2022-11-05 20:14:26 +04:00
JRoberts 20def8c7a0 android: shorten user address and group link texts (#1303) 2022-11-05 19:56:28 +04:00
JRoberts a9b4489f4f ios: more group link translations (#1302) 2022-11-05 19:56:07 +04:00
JRoberts 5ca21dea13 mobile: seamless transition from group link to group; ios: group link alert (#1296) 2022-11-05 17:48:57 +04:00
JRoberts 80ca80f6d8 core: rename CRGroupData constructor to CRDataGroup (#1299) 2022-11-05 15:04:39 +04:00
JRoberts 687a741723 core: fix CReqClientData JSON encoding 2022-11-05 12:53:41 +04:00
Stanislav Dmitrenko 54ab4e979a android: fix pending connection and contact request previews height (#1297) 2022-11-04 22:35:51 +04:00
Evgeny Poberezkin b6696e901b core: update receiveChunks option to 6 (~90kb) 2022-11-04 17:51:57 +00:00
Evgeny Poberezkin 89de5497ef core: update chat preferences (#1292)
* core: update chat preferences

* refactor, types

* rename types

* rename types

* make voice on by default

* create new user with empty preferences

* fix test
2022-11-04 21:05:21 +04:00
JRoberts 1bf3154488 core: add hostContact to CRUserAcceptedGroupSent (to transition from pending connection to group in ui) (#1295) 2022-11-04 19:46:27 +04:00
JRoberts c78acfda33 mobile: group link ux on joining side (#1294) 2022-11-04 15:33:29 +04:00
Stanislav Dmitrenko 1432a04927 android: Make ChatList item the same height as it would be with two lines message (#1293)
* android: Make ChatList item the same height as it would be with two lines message

* Different way of doing the same
2022-11-04 13:07:53 +04:00
JRoberts d432dfba21 core: include pending group link connections into chat previews on joining side (#1291) 2022-11-04 12:00:03 +04:00
JRoberts 5243613045 core: group link connection request uri data; automatically join groups over group links (#1275) 2022-11-03 14:46:36 +04:00
Stanislav Dmitrenko 83599adc80 android: Switching connection (#1287)
* android: Switching connection

* Dividers

* Strings

* Strings2

* Strings3
2022-11-02 16:38:59 +00:00
JRoberts 538992eb95 ios: 4.2-beta.3 translations (#1286) 2022-11-02 20:37:14 +04:00
Stanislav Dmitrenko 658daf56bb android: Inline file transfers (#1288) 2022-11-02 16:26:53 +00:00
Evgeny Poberezkin 7d31862576 ios: refactor group default (#1285) 2022-11-02 10:47:18 +00:00
Evgeny Poberezkin 7a1d0eac9d ios: option to transfer files faster (inline) (#1284) 2022-11-02 10:32:08 +00:00
Stanislav Dmitrenko d851396113 android: Better scroll of zoomed images in gallery (#1283) 2022-11-02 10:26:19 +00:00
Evgeny Poberezkin cbdd9b9e37 ios: switch contact and member to another address (#1282)
* ios: switch contact and member to another address

* update simplexmq (JSON encoding)
2022-11-02 09:48:20 +00:00
Evgeny Poberezkin d5fc0d7dfc core: update event name, ios: types/api/ui (wip) to switch connection to another address, fix contact/member info view, fix setting multiple servers (#1281)
* core: update event name, ios: types/api/ui (wip) to switch connection to another address, fix contact/member info view, fix setting multiple servers

* fix

* update strings

Co-authored-by: JRoberts <8711996+jr-simplex@users.noreply.github.com>
2022-11-01 20:30:53 +00:00
Stanislav Dmitrenko 0d0de1da86 core: Test for incognito mode (#1280) 2022-11-01 16:05:05 +00:00
Stanislav Dmitrenko 4e5a5c11dc core: Chat preferences (#1261)
* core: Preferences

* Changes

* fix types

* Follow up

* Review

* Review

* update logic

* update

* update 2

* Tests

* Fixed a bug and tests

* Voice -> voice messages

* refactor

* fix

Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
2022-11-01 14:32:49 +00:00
Stanislav Dmitrenko 14038ce370 android: Chat preferences (#1278) 2022-11-01 13:43:58 +00:00
Evgeny Poberezkin a72f603e13 core: switch connection (#1277)
* core: switch connection

* chat items for SWITCH

* additional events for connection switch

* update simplexmq

* test

* comment test output

* update messages for connection switch

Co-authored-by: JRoberts <8711996+jr-simplex@users.noreply.github.com>
2022-11-01 13:26:08 +00:00
JRoberts 85609ef217 android: version 4.2-beta.2 (65) 2022-11-01 10:34:17 +04:00
JRoberts 7631d59695 ios: version 4.2 (87) 2022-11-01 10:09:34 +04:00
Evgeny Poberezkin 38f305bb34 run nix build 2022-10-31 22:02:56 +00:00
JRoberts 290ef9de61 core: update simplexmq (queue rotation) (#1276) 2022-10-31 14:29:38 +04:00
mlanp f38d3b4d7f android / iOS: german translations for v4.2 (#1266)
* android / iOS: german translations for v4.2

* corrections

* Localizable.strings

Co-authored-by: JRoberts <8711996+jr-simplex@users.noreply.github.com>
2022-10-29 14:54:49 +04:00
JRoberts 8f638df7a9 mobile: merge contacts (#1271) 2022-10-28 20:05:04 +04:00
JRoberts 8a121b4442 ci: disable test step for ubuntu-20.04 (#1270) 2022-10-28 15:27:34 +04:00
JRoberts 1bb7a954f5 ios: update library 2022-10-28 15:21:12 +04:00
JRoberts ddd8bd9061 ci: github build tests timeout (#1267) 2022-10-28 14:46:20 +04:00
JRoberts 179b9e093f core: merge contacts when connecting via group link (#1265) 2022-10-27 23:38:03 +04:00
JRoberts 352a4f3d2a core: clean up incognito profiles (#1262) 2022-10-27 14:25:48 +04:00
JRoberts e06d4e5c85 ios: translations for auto-accept (#1263) 2022-10-27 10:47:48 +04:00
JRoberts 385ebd2298 core: update deleteGroupMember logic and its usages (no items & expiration) (#1258) 2022-10-26 13:37:17 +04:00
JRoberts a20f0050b9 4.2.0 2022-10-26 11:36:06 +04:00
JRoberts e17e9c641d android: version 4.2-beta.0 (64) 2022-10-26 11:33:51 +04:00
JRoberts 2ac6866638 ios: version 4.2 (86) 2022-10-26 11:13:08 +04:00
JRoberts d7f319aa9e core: mark group contacts as used on send, receive, api (#1253) 2022-10-25 12:50:26 +04:00
Evgeny Poberezkin 1e10b0a49c website: test pages for hero animations (#1255) 2022-10-24 21:00:35 +01:00
JRoberts e9c321eaad android: mark untranslated texts 2022-10-24 20:48:23 +04:00
Stanislav Dmitrenko c58d069fbd android: Sender name in chatlist (#1251)
* android: Sender name in chatlist

* Sender is bold

* RTL for sender text
2022-10-24 16:47:03 +01:00
Stanislav Dmitrenko 15c8945fd3 android: Settings to auto-accept contact requests (#1250)
* android: Settings to auto-accept contact requests

* Link will not be recreated when not needed

* Layout

* Layout

* Button rename

Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
2022-10-24 16:45:12 +01:00
JRoberts 2109dca1c7 android: allow ContactConnected response to have omitted field (#1252) 2022-10-24 17:40:20 +04:00
JRoberts 7ef7d08c00 mobile: use viaGroupLink to avoid adding missing chats on subscriptions (#1249) 2022-10-24 16:22:00 +04:00
Evgeny Poberezkin e463e19114 rfc: chat settings (#1096)
* rfc: chat settings

* update doc

* update doc

* update doc

* comment in JSON
2022-10-24 12:45:33 +01:00
JRoberts deeb2e891a core: add viaGroupLink to Connection (#1248) 2022-10-24 14:28:58 +04:00
Evgeny Poberezkin 5265667c0c ios: settings to auto-accept contact requests (#1246)
* ios: settings to auto-accept contact requests

* use NavigationView

* fix share sheet, layout

* move buttons
2022-10-24 11:25:36 +01:00
JRoberts 15c1f9f9c8 core: group link contact connecting(ed) events to avoid adding previews in ui (#1242)
* core: group link contact connecting(ed) events to avoid adding ui previews

* fix test

* refactor

* ios types

* android types

* type in bot
2022-10-23 21:18:15 +01:00
Evgeny Poberezkin 341199d599 mobile: update type for UserContactLink changes, add addressAutoAccept API (#1245) 2022-10-23 11:16:56 +01:00
Evgeny Poberezkin dfb6dafd6f core: record constructors for UserContactLink (#1244) 2022-10-22 22:33:59 +01:00
Evgeny Poberezkin 7f544da6cf core: debug chat and agent locks, update simplexmq (#1243)
* core: debug chat and agent locks, update simplexmq

* add connId

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

* update lock strings

* fix encoding test

Co-authored-by: JRoberts <8711996+jr-simplex@users.noreply.github.com>
2022-10-22 21:22:44 +01:00
JRoberts d0a0a0461f mobile: change role item texts, uncomment button (#1241)
* mobile: change role item texts, uncomment button

* Update apps/android/app/src/main/res/values-de/strings.xml

Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>

Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
2022-10-22 18:40:21 +04:00
JRoberts 1dca577ca8 mobile: invited via group link chat item (#1239) 2022-10-22 14:23:23 +04:00
JRoberts 26984b62fe core: delete broken chat item when removing invited member connected via group link; test removing invited member 2022-10-22 14:23:03 +04:00
Stanislav Dmitrenko 1470b8d128 core: auto accept via address and incognito mode specified (#1233)
* core: Auto accept via address and incognito mode specified

* Fix test

* Refactoring

* No forcing

* Apply suggestions from code review

* refactor

* refactor AutoAccept

* Test

* Test

* allow different test output order

* rename

* rename

Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
Co-authored-by: JRoberts <8711996+jr-simplex@users.noreply.github.com>
2022-10-21 17:14:12 +01:00
JRoberts 5bcb725ea5 core: exclude contacts accepted via group link from chat previews (#1234)
* RGEInvitedViaGroupLink

* CRSentGroupInvitationViaLink

* via_group_link filtering

* reset

* refactor

* remove brackets
2022-10-21 17:35:07 +04:00
Stanislav Dmitrenko 7f9c4ede02 android: Mark chat unread (#1235)
* android: Mark chat unread

* Fix

* Fix2

* Fix3

* Refactoring

* Icon

* update icon

Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
2022-10-21 13:29:08 +01:00
Evgeny Poberezkin 34a74da0b9 ios: mark chat unread (#1237)
* ios: mark unread (wip)

* mark unread works
2022-10-21 12:32:11 +01:00
JRoberts 98cb1c39f2 core: allow to delete contacts that are in groups; group contacts management rfc (#1229) 2022-10-20 19:27:00 +04:00
Evgeny Poberezkin c4fc8a97b1 core: option to receive file inline up to maximum "offered" size (#1232)
* core: option to receive file inline up to maximum "offered" size

* comment
2022-10-20 14:32:20 +01:00
Stanislav Dmitrenko 9edb54b45c android: Unencrypted all fix (#1231) 2022-10-20 10:16:15 +01:00
Stanislav Dmitrenko 213b586f8f core: Forcing chat unread (#1228)
* core: Forcing chat unread

* Implementation

* Renaming

* Removed unused code

* test

Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
2022-10-19 19:38:44 +01:00
Stanislav Dmitrenko e0582d656e android: Different width for image in a message based on orientation (#1227)
* android: Different width for image in a message based on orientation

* Remember

Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
2022-10-18 19:49:27 +01:00
JRoberts 65f8ad4423 Merge branch 'stable' 2022-10-18 18:18:36 +04:00
JRoberts b115bda8f5 4.1.1 2022-10-18 18:06:47 +04:00
JRoberts 85ab37ae5d android: version 4.1 (63) 2022-10-18 18:05:08 +04:00
JRoberts dc5f6673a2 ios: version 4.1 (85) 2022-10-18 17:54:37 +04:00
Evgeny Poberezkin ada4c1d2ec Merge branch 'stable' 2022-10-18 13:11:56 +01:00
Stanislav Dmitrenko 1f8bfbe3f5 android: Fixed conflics (#1225)
* ios: use transparent background for images without text and quote (#1224)

* android: Some small fixes (#1221)

* android: Some small fixes

* Alpha in preview image

* ImageView width limitation for portrait images

* Sharing files with a text

* Do not create new link on orientation change

* Skipping quoted item when applying transparent background

* Commented out sharing a text of image message

* Revert "ImageView width limitation for portrait images"

This reverts commit b1f20b51da.

* White color

Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
2022-10-18 13:01:37 +01:00
Stanislav Dmitrenko cf67c951fc android: Scrolling state will be preserved (#1222)
* android: Scrolling state will be preserved

* Auth changes in terminal view

* Changes
2022-10-18 11:05:31 +01:00
Stanislav Dmitrenko b78a5931e9 android: Some small fixes (#1221)
* android: Some small fixes

* Alpha in preview image

* ImageView width limitation for portrait images

* Sharing files with a text

* Do not create new link on orientation change

* Skipping quoted item when applying transparent background

* Commented out sharing a text of image message

* Revert "ImageView width limitation for portrait images"

This reverts commit b1f20b51da.

* White color
2022-10-18 10:45:49 +01:00
JRoberts e57b9f4cea core: don't calculate chat stats to speed up loading individual chats (#1218) 2022-10-18 10:16:28 +01:00
Evgeny Poberezkin 104e1040bf ios: use transparent background for images without text and quote (#1224) 2022-10-18 10:07:45 +01:00
JRoberts 2c347cb7b9 core: CRSubscriptionEnd (#1223)
* core: CRSubscriptionEnd

* refactor

Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
2022-10-17 21:35:29 +01:00
Stanislav Dmitrenko fad60a1d24 android: EditText instead of BasicTextView with ability to get images… (#1213)
* android: EditText instead of BasicTextView with ability to get images from keyboard

* Padding

* Empty, not blank

* Keyboard handling

* Setting text of edited message and bigger padding
2022-10-17 21:25:42 +01:00
Stanislav Dmitrenko 61ed866c24 android: Better way of saving even big files (#1212) 2022-10-16 13:26:10 +01:00
Stanislav Dmitrenko 656c10976b android: Fix autoscroll to bottom when going from group to direct (#1209)
* android: Fix autoscroll to bottom when going from group to direct
- and makes scrolling smoother because of -1 number of recompose

* Rename
2022-10-16 12:41:01 +01:00
Evgeny Poberezkin 63f40f030a android: change indigo color to improve dark mode readability (#1219) 2022-10-15 21:55:37 +01:00
Evgeny Poberezkin dec8e136f9 android: increase paddings in link views (#1217)
* android: increase paddings in link views

* increase padding on scan screen

* Update apps/android/app/src/main/java/chat/simplex/app/views/newchat/AddContactView.kt

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

* Update apps/android/app/src/main/java/chat/simplex/app/views/newchat/AddContactView.kt

Co-authored-by: JRoberts <8711996+jr-simplex@users.noreply.github.com>
2022-10-15 18:49:10 +01:00
JRoberts a525b4e5db ios: group links (#1214)
* libraries

* api

* ui

* via nav link

* translations

* align android translations
2022-10-15 18:09:25 +04:00
JRoberts 2f8b4a3e93 Merge branch 'stable' 2022-10-15 16:15:07 +04:00
JRoberts 39818b6fbb ios: version 4.1 (84) 2022-10-15 16:14:32 +04:00
JRoberts 3523c7ebd7 ios: fix ContactConnectionInfo share sheet (#1215) 2022-10-15 15:47:04 +04:00
JRoberts 1b3984f52f core: create group invitation connection asynchronously on group link auto-accept (#1211) 2022-10-15 14:48:07 +04:00
Stanislav Dmitrenko 560807b4b7 android: Group links (#1210)
* android: Group links

* Alerts with network errors

* Alert for get link too

* Layout changes

* Divider

* Revert "Alert for get link too"

This reverts commit b0eaf50cdc.

* Update apps/android/app/src/main/res/values-de/strings.xml

* Update apps/android/app/src/main/res/values/strings.xml

* Update apps/android/app/src/main/res/values/strings.xml

* Update apps/android/app/src/main/res/values-ru/strings.xml

* Update apps/android/app/src/main/res/values-ru/strings.xml

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

* apiDeleteGroupLink call

* names

Co-authored-by: JRoberts <8711996+jr-simplex@users.noreply.github.com>
2022-10-14 19:30:09 +04:00
Evgeny Poberezkin fb03a119ea core: support inline file transfers (#1187)
* core: support inline file transfers

* parameterize ChatMessage

* send files inline when accepted

* accept inline file transfers (almost works)

* db error SERcvFileInvalid

* inline file transfer works (TODO fix test)

* inline file transfer tests, change encodings

* fixture

* combine messages into x.file.acpt.inv, refactor

* inline file mode

* decide whether to receive file inline on the recipient side, not only via file invitation

* test inline files "sent" mode

* check that file was offered inline

* update schema

* enable encryption tests

* test name

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

* fix the list of rcv files to subscribe too

Co-authored-by: JRoberts <8711996+jr-simplex@users.noreply.github.com>
2022-10-14 13:06:33 +01:00
JRoberts f7da034cf1 core: use acceptContactAsync on auto-accept, reuse incognito profile for contacts accepted via group link (#1208)
* update simplexmq (acceptContactAsync)

* acceptContactRequestAsync, single profile

* refactor

* refactor 2

* refactor

* Update src/Simplex/Chat/Store.hs

Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>

* refactor

Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
2022-10-14 14:57:01 +04:00
Stanislav Dmitrenko 4a259e44b1 android: Link preview with image URLs and more options for finding image on a site (#1207) 2022-10-13 19:11:59 +01:00
Stanislav Dmitrenko 799d9443bd android: Multiple images sharing (#1206) 2022-10-13 18:39:24 +01:00
JRoberts 3bf8361911 core: group links (#1194) 2022-10-13 17:12:22 +04:00
JRoberts 9670da4646 android: version 4.1 (62) 2022-10-13 14:14:01 +04:00
Stanislav Dmitrenko 1e22028189 android: Fix connecting via link from Firefox (#1205)
* android: Fix connecting via link from Firefox

* Replace slash
2022-10-13 10:49:57 +01:00
403 changed files with 13753 additions and 3815 deletions
+2 -1
View File
@@ -108,7 +108,8 @@ jobs:
echo "::set-output name=bin_path::$(cabal list-bin simplex-chat)"
- name: Unix test
if: matrix.os != 'windows-latest'
if: matrix.os != 'windows-latest' && matrix.os != 'ubuntu-20.04'
timeout-minutes: 10
shell: bash
run: cabal test --test-show-details=direct
+12 -6
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.md).
### 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
+74 -27
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,10 @@
- [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)
- [Join a user group](#join-a-user-group)
- [Contribute](#contribute)
- [Help us with donations](#help-us-with-donations)
- [Disclaimers, Security contact, License](#disclaimers)
## Why privacy matters
@@ -83,6 +85,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 +153,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,42 +178,69 @@ 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.
- 🏗 SMP queue redundancy and rotation.
- 🏗 Links to join groups and improve groups stability.
- Feeds/broadcasts
- Disappearing messages, with mutual agreement.
- Voice messages
- Video messages
- Automatic chat history deletion.
- ✅ Links to join groups and improve groups stability.
- 🏗 SMP queue redundancy and rotation (manual is supported).
- 🏗 Voice messages (with recipient opt-out per contact).
- 🏗 Basic authentication for SMP servers (to authorize creating new queues).
- View deleted messages, full message deletion by sender (with recipient opt-in per contact).
- Block screenshots and view in recent apps.
- Optionally avoid re-using the same TCP session for multiple connections.
- Access password/pin (with optional alternative access password).
- Ephemeral/disappearing/OTR conversations with the existing contacts.
- Media server to optimize sending large files to groups.
- Video messages.
- Message delivery confirmation (with sender opt-in or opt-out per contact, TBC).
- Multiple user profiles in the same chat database.
- Advanced server configuration.
- Feeds/broadcasts.
- Unconfirmed: disappearing messages (with recipient opt-in per-contact).
- Web widgets for custom interactivity in the chats.
- Message delivery confirmation.
- Programmable chat automations / rules (automatic replies/forward/deletion/sending, reminders, etc.).
- Supporting the same profile on multiple devices.
- Desktop client.
- Privacy-preserving identity server for optional DNS-based contact/group addresses to simplify connection and discovery, but not used to deliver messages:
- keep all your contacts and groups even if you lose the domain.
- the server doesn't have information about your contacts and groups.
- 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
## Join a user group
I will get straight to the point: I ask you to support SimpleX Chat with donations.
You can join a general group with more than 100 members: [#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).
We are prioritizing users privacy and security - it would be impossible without your support we were lucky to have so far.
You can also join smaller 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).
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.
You can join these groups either by opening these links in the app or by opening them in a desktop browser and scanning QR code.
Let us know if you'd like to add some other countries to the list.
Join via the app to share what's going on and ask any questions!
## Contribute
We would love to have you join the development! You can contribute to SimpleX Chat with:
- 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.
## 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 +248,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 +256,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.md).
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 +290,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)
+1
View File
@@ -10,6 +10,7 @@
/.idea/deploymentTargetDropDown.xml
/.idea/misc.xml
/.idea/uiDesigner.xml
/.idea/kotlinc.xml
.DS_Store
/build
/captures
+2 -2
View File
@@ -11,8 +11,8 @@ android {
applicationId "chat.simplex.app"
minSdk 29
targetSdk 32
versionCode 61
versionName "4.1"
versionCode 70
versionName "4.2.2"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
ndk {
@@ -69,6 +69,11 @@
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="*/*" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.SEND_MULTIPLE" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="image/*" />
</intent-filter>
</activity>
<activity-alias
@@ -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 {
@@ -98,11 +98,21 @@ class MainActivity: FragmentActivity() {
override fun onStart() {
super.onStart()
val enteredBackgroundVal = enteredBackground.value
if (enteredBackgroundVal == null || elapsedRealtime() - enteredBackgroundVal >= 30 * 1e+3) {
if (enteredBackgroundVal == null || elapsedRealtime() - enteredBackgroundVal >= 30_000) {
runAuthenticate()
}
}
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()
}
@@ -398,13 +389,24 @@ fun processExternalIntent(intent: Intent?, chatModel: ChatModel) {
chatModel.sharedContent.value = SharedContent.Text(it)
}
intent.type?.startsWith("image/") == true -> (intent.getParcelableExtra<Parcelable>(Intent.EXTRA_STREAM) as? Uri)?.let {
chatModel.sharedContent.value = SharedContent.Image(it)
chatModel.sharedContent.value = SharedContent.Images(intent.getStringExtra(Intent.EXTRA_TEXT) ?: "", listOf(it))
} // All other mime types
else -> (intent.getParcelableExtra<Parcelable>(Intent.EXTRA_STREAM) as? Uri)?.let {
chatModel.sharedContent.value = SharedContent.File(it)
chatModel.sharedContent.value = SharedContent.File(intent.getStringExtra(Intent.EXTRA_TEXT) ?: "", it)
}
}
}
Intent.ACTION_SEND_MULTIPLE -> {
// Close active chat and show a list of chats
chatModel.chatId.value = null
chatModel.clearOverlays.value = true
when {
intent.type?.startsWith("image/") == true -> (intent.getParcelableArrayListExtra<Parcelable>(Intent.EXTRA_STREAM) as? List<Uri>)?.let {
chatModel.sharedContent.value = SharedContent.Images(intent.getStringExtra(Intent.EXTRA_TEXT) ?: "", it)
} // All other mime types
else -> {}
}
}
}
}
@@ -414,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)
}
}
)
@@ -38,7 +38,7 @@ class ChatModel(val controller: ChatController) {
val groupMembers = mutableStateListOf<GroupMember>()
val terminalItems = mutableStateListOf<TerminalItem>()
val userAddress = mutableStateOf<String?>(null)
val userAddress = mutableStateOf<UserContactLinkRec?>(null)
val userSMPServers = mutableStateOf<(List<String>)?>(null)
val chatItemTTL = mutableStateOf<ChatItemTTL>(ChatItemTTL.None)
@@ -90,7 +90,7 @@ class ChatModel(val controller: ChatController) {
fun updateContactConnection(contactConnection: PendingContactConnection) = updateChat(ChatInfo.ContactConnection(contactConnection))
fun updateContact(contact: Contact) = updateChat(ChatInfo.Direct(contact), addMissing = !contact.isIndirectContact)
fun updateContact(contact: Contact) = updateChat(ChatInfo.Direct(contact), addMissing = !contact.isIndirectContact && !contact.viaGroupLink)
fun updateGroup(groupInfo: GroupInfo) = updateChat(ChatInfo.Group(groupInfo))
@@ -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 }
@@ -387,7 +392,7 @@ data class Chat (
val id: String get() = chatInfo.id
@Serializable
data class ChatStats(val unreadCount: Int = 0, val minUnreadItemId: Long = 0)
data class ChatStats(val unreadCount: Int = 0, val minUnreadItemId: Long = 0, val unreadChat: Boolean = false)
@Serializable
data class ServerInfo(val networkStatus: NetworkStatus)
@@ -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
@@ -523,6 +528,8 @@ data class Contact(
val activeConn: Connection,
val viaGroup: Long? = null,
val chatSettings: ChatSettings,
// User applies his preferences for the contact here. Named user_preferences on the contact in DB
val userPreferences: ChatPreferences,
override val createdAt: Instant,
override val updatedAt: Instant
): SomeChat, NamedChat {
@@ -540,6 +547,9 @@ data class Contact(
val isIndirectContact: Boolean get() =
activeConn.connLevel > 0 || viaGroup != null
val viaGroupLink: Boolean get() =
activeConn.viaGroupLink
val contactConnIncognito =
activeConn.customUserProfileId != null
@@ -550,6 +560,7 @@ data class Contact(
profile = LocalProfile.sampleData,
activeConn = Connection.sampleData,
chatSettings = ChatSettings(true),
userPreferences = ChatPreferences(),
createdAt = Clock.System.now(),
updatedAt = Clock.System.now()
)
@@ -571,10 +582,10 @@ class ContactSubStatus(
)
@Serializable
class Connection(val connId: Long, val connStatus: ConnStatus, val connLevel: Int, val customUserProfileId: Long? = null) {
class Connection(val connId: Long, val connStatus: ConnStatus, val connLevel: Int, val viaGroupLink: Boolean, val customUserProfileId: Long? = null) {
val id: ChatId get() = ":$connId"
companion object {
val sampleData = Connection(connId = 1, connStatus = ConnStatus.Ready, connLevel = 0, customUserProfileId = null)
val sampleData = Connection(connId = 1, connStatus = ConnStatus.Ready, connLevel = 0, viaGroupLink = false, customUserProfileId = null)
}
}
@@ -583,7 +594,9 @@ class Profile(
override val displayName: String,
override val fullName: String,
override val image: String? = null,
override val localAlias : String = ""
override val localAlias : String = "",
// Contact applies his preferences here
val preferences: ChatPreferences? = null
): NamedChat {
val profileViewName: String
get() {
@@ -607,6 +620,8 @@ class LocalProfile(
override val fullName: String,
override val image: String? = null,
override val localAlias: String,
// Contact applies his preferences here
val preferences: ChatPreferences? = null
): NamedChat {
val profileViewName: String = localAlias.ifEmpty { if (fullName == "" || displayName == fullName) displayName else "$displayName ($fullName)" }
@@ -636,6 +651,7 @@ data class GroupInfo (
val membership: GroupMember,
val hostConnCustomUserProfileId: Long? = null,
val chatSettings: ChatSettings,
// val groupPreferences: GroupPreferences? = null,
override val createdAt: Instant,
override val updatedAt: Instant
): SomeChat, NamedChat {
@@ -689,7 +705,7 @@ class GroupProfile (
}
@Serializable
class GroupMember (
data class GroupMember (
val groupMemberId: Long,
val groupId: Long,
val memberId: String,
@@ -771,6 +787,12 @@ class GroupMember (
}
}
@Serializable
class GroupMemberRef(
val groupMemberId: Long,
val profile: Profile
)
@Serializable
enum class GroupMemberRole(val memberRole: String) {
@SerialName("member") Member("member"), // order matters in comparisons
@@ -902,6 +924,7 @@ class PendingContactConnection(
val pccAgentConnId: String,
val pccConnStatus: ConnStatus,
val viaContactUri: Boolean,
val groupLinkId: String? = null,
val customUserProfileId: Long? = null,
val connReqInv: String? = null,
override val localAlias: String,
@@ -940,8 +963,11 @@ class PendingContactConnection(
return if (initiated == null) "" else generalGetString(
if (initiated && !viaContactUri)
if (incognito) R.string.description_you_shared_one_time_link_incognito else R.string.description_you_shared_one_time_link
else if (viaContactUri )
if (incognito) R.string.description_via_contact_address_link_incognito else R.string.description_via_contact_address_link
else if (viaContactUri)
if (groupLinkId != null)
if (incognito) R.string.description_via_group_link_incognito else R.string.description_via_group_link
else
if (incognito) R.string.description_via_contact_address_link_incognito else R.string.description_via_contact_address_link
else
if (incognito) R.string.description_via_one_time_link_incognito else R.string.description_via_one_time_link
)
@@ -1040,6 +1066,7 @@ data class ChatItem (
is RcvGroupEvent.MemberRole -> true
is RcvGroupEvent.UserRole -> false
is RcvGroupEvent.MemberDeleted -> false
is RcvGroupEvent.InvitedViaGroupLink -> false
}
is CIContent.SndGroupEventContent -> true
else -> false
@@ -1209,6 +1236,8 @@ sealed class CIContent: ItemContent {
@Serializable @SerialName("sndGroupInvitation") class SndGroupInvitation(val groupInvitation: CIGroupInvitation, val memberRole: GroupMemberRole): CIContent() { override val msgContent: MsgContent? get() = null }
@Serializable @SerialName("rcvGroupEvent") class RcvGroupEventContent(val rcvGroupEvent: RcvGroupEvent): CIContent() { override val msgContent: MsgContent? get() = null }
@Serializable @SerialName("sndGroupEvent") class SndGroupEventContent(val sndGroupEvent: SndGroupEvent): CIContent() { override val msgContent: MsgContent? get() = null }
@Serializable @SerialName("rcvConnEvent") class RcvConnEventContent(val rcvConnEvent: RcvConnEvent): CIContent() { override val msgContent: MsgContent? get() = null }
@Serializable @SerialName("sndConnEvent") class SndConnEventContent(val sndConnEvent: SndConnEvent): CIContent() { override val msgContent: MsgContent? get() = null }
override val text: String get() = when(this) {
is SndMsgContent -> msgContent.text
@@ -1222,6 +1251,8 @@ sealed class CIContent: ItemContent {
is SndGroupInvitation -> groupInvitation.text
is RcvGroupEventContent -> rcvGroupEvent.text
is SndGroupEventContent -> sndGroupEvent.text
is RcvConnEventContent -> rcvConnEvent.text
is SndConnEventContent -> sndConnEvent.text
}
}
@@ -1547,17 +1578,19 @@ sealed class RcvGroupEvent() {
@Serializable @SerialName("userDeleted") class UserDeleted(): RcvGroupEvent()
@Serializable @SerialName("groupDeleted") class GroupDeleted(): RcvGroupEvent()
@Serializable @SerialName("groupUpdated") class GroupUpdated(val groupProfile: GroupProfile): RcvGroupEvent()
@Serializable @SerialName("invitedViaGroupLink") class InvitedViaGroupLink(): RcvGroupEvent()
val text: String get() = when (this) {
is MemberAdded -> String.format(generalGetString(R.string.rcv_group_event_member_added), profile.profileViewName)
is MemberConnected -> generalGetString(R.string.rcv_group_event_member_connected)
is MemberLeft -> generalGetString(R.string.rcv_group_event_member_left)
is MemberRole -> String.format(generalGetString(R.string.member_role), profile.profileViewName, role.text)
is UserRole -> String.format(generalGetString(R.string.your_member_role), role.text)
is MemberRole -> String.format(generalGetString(R.string.rcv_group_event_changed_member_role), profile.profileViewName, role.text)
is UserRole -> String.format(generalGetString(R.string.rcv_group_event_changed_your_role), role.text)
is MemberDeleted -> String.format(generalGetString(R.string.rcv_group_event_member_deleted), profile.profileViewName)
is UserDeleted -> generalGetString(R.string.rcv_group_event_user_deleted)
is GroupDeleted -> generalGetString(R.string.rcv_group_event_group_deleted)
is GroupUpdated -> generalGetString(R.string.rcv_group_event_updated_group_profile)
is InvitedViaGroupLink -> generalGetString(R.string.rcv_group_event_invited_via_your_group_link)
}
}
@@ -1570,14 +1603,54 @@ sealed class SndGroupEvent() {
@Serializable @SerialName("groupUpdated") class GroupUpdated(val groupProfile: GroupProfile): SndGroupEvent()
val text: String get() = when (this) {
is MemberRole -> String.format(generalGetString(R.string.member_role), profile.profileViewName, role.text)
is UserRole -> String.format(generalGetString(R.string.your_member_role), role.text)
is MemberRole -> String.format(generalGetString(R.string.snd_group_event_changed_member_role), profile.profileViewName, role.text)
is UserRole -> String.format(generalGetString(R.string.snd_group_event_changed_role_for_yourself), role.text)
is MemberDeleted -> String.format(generalGetString(R.string.snd_group_event_member_deleted), profile.profileViewName)
is UserLeft -> generalGetString(R.string.snd_group_event_user_left)
is GroupUpdated -> generalGetString(R.string.snd_group_event_group_profile_updated)
}
}
@Serializable
sealed class RcvConnEvent {
@Serializable @SerialName("switchQueue") class SwitchQueue(val phase: SwitchPhase): RcvConnEvent()
val text: String get() = when (this) {
is SwitchQueue -> when (phase) {
SwitchPhase.Completed -> generalGetString(R.string.rcv_conn_event_switch_queue_phase_completed)
else -> generalGetString(R.string.rcv_conn_event_switch_queue_phase_changing)
}
}
}
@Serializable
sealed class SndConnEvent {
@Serializable @SerialName("switchQueue") class SwitchQueue(val phase: SwitchPhase, val member: GroupMemberRef? = null): SndConnEvent()
val text: String
get() = when (this) {
is SwitchQueue -> {
member?.profile?.profileViewName?.let {
return when (phase) {
SwitchPhase.Completed -> String.format(generalGetString(R.string.snd_conn_event_switch_queue_phase_completed_for_member), it)
else -> String.format(generalGetString(R.string.snd_conn_event_switch_queue_phase_changing_for_member), it)
}
}
when (phase) {
SwitchPhase.Completed -> generalGetString(R.string.snd_conn_event_switch_queue_phase_completed)
else -> generalGetString(R.string.snd_conn_event_switch_queue_phase_changing)
}
}
}
}
@Serializable
enum class SwitchPhase {
@SerialName("started") Started,
@SerialName("confirmed") Confirmed,
@SerialName("completed") Completed
}
sealed class ChatItemTTL: Comparable<ChatItemTTL?> {
object Day: ChatItemTTL()
object Week: ChatItemTTL()
@@ -89,6 +89,7 @@ class AppPreferences(val context: Context) {
val laNoticeShown = mkBoolPreference(SHARED_PREFS_LA_NOTICE_SHOWN, false)
val webrtcIceServers = mkStrPreference(SHARED_PREFS_WEBRTC_ICE_SERVERS, null)
val privacyAcceptImages = mkBoolPreference(SHARED_PREFS_PRIVACY_ACCEPT_IMAGES, true)
val privacyTransferImagesInline = mkBoolPreference(SHARED_PREFS_PRIVACY_TRANSFER_IMAGES_INLINE, false)
val privacyLinkPreviews = mkBoolPreference(SHARED_PREFS_PRIVACY_LINK_PREVIEWS, true)
val experimentalCalls = mkBoolPreference(SHARED_PREFS_EXPERIMENTAL_CALLS, false)
val chatArchiveName = mkStrPreference(SHARED_PREFS_CHAT_ARCHIVE_NAME, null)
@@ -178,6 +179,7 @@ class AppPreferences(val context: Context) {
private const val SHARED_PREFS_LA_NOTICE_SHOWN = "LANoticeShown"
private const val SHARED_PREFS_WEBRTC_ICE_SERVERS = "WebrtcICEServers"
private const val SHARED_PREFS_PRIVACY_ACCEPT_IMAGES = "PrivacyAcceptImages"
private const val SHARED_PREFS_PRIVACY_TRANSFER_IMAGES_INLINE = "PrivacyTransferImagesInline"
private const val SHARED_PREFS_PRIVACY_LINK_PREVIEWS = "PrivacyLinkPreviews"
private const val SHARED_PREFS_EXPERIMENTAL_CALLS = "ExperimentalCalls"
private const val SHARED_PREFS_CHAT_ARCHIVE_NAME = "ChatArchiveName"
@@ -242,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 {
@@ -258,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)
}
}
}
@@ -285,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
@@ -496,6 +506,24 @@ open class ChatController(var ctrl: ChatCtrl?, val ntfManager: NtfManager, val a
return null
}
suspend fun apiSwitchContact(contactId: Long) {
return when (val r = sendCmd(CC.APISwitchContact(contactId))) {
is CR.CmdOk -> {}
else -> {
apiErrorAlert("apiSwitchContact", generalGetString(R.string.connection_error), r)
}
}
}
suspend fun apiSwitchGroupMember(groupId: Long, groupMemberId: Long) {
return when (val r = sendCmd(CC.APISwitchGroupMember(groupId, groupMemberId))) {
is CR.CmdOk -> {}
else -> {
apiErrorAlert("apiSwitchGroupMember", generalGetString(R.string.error_changing_address), r)
}
}
}
suspend fun apiAddContact(): String? {
val r = sendCmd(CC.AddContact())
return when (r) {
@@ -552,15 +580,6 @@ open class ChatController(var ctrl: ChatCtrl?, val ntfManager: NtfManager, val a
r is CR.ContactDeleted && type == ChatType.Direct -> return true
r is CR.ContactConnectionDeleted && type == ChatType.ContactConnection -> return true
r is CR.GroupDeletedUser && type == ChatType.Group -> return true
r is CR.ChatCmdError -> {
val e = r.chatError
if (e is ChatError.ChatErrorChat && e.errorType is ChatErrorType.ContactGroups) {
AlertManager.shared.showAlertMsg(
generalGetString(R.string.cannot_delete_contact),
String.format(generalGetString(R.string.contact_cannot_be_deleted_as_they_are_in_groups), e.errorType.contact.displayName, e.errorType.groupNames.joinToString(", "))
)
}
}
else -> {
val titleId = when (type) {
ChatType.Direct -> R.string.error_deleting_contact
@@ -617,6 +636,13 @@ open class ChatController(var ctrl: ChatCtrl?, val ntfManager: NtfManager, val a
return null
}
suspend fun apiSetContactPrefs(contactId: Long, prefs: ChatPreferences): Contact? {
val r = sendCmd(CC.ApiSetContactPrefs(contactId, prefs))
if (r is CR.ContactPrefsUpdated) return r.toContact
Log.e(TAG, "apiSetContactPrefs bad response: ${r.responseType} ${r.details}")
return null
}
suspend fun apiCreateUserAddress(): String? {
val r = sendCmd(CC.CreateMyAddress())
return when (r) {
@@ -637,9 +663,9 @@ open class ChatController(var ctrl: ChatCtrl?, val ntfManager: NtfManager, val a
return false
}
private suspend fun apiGetUserAddress(): String? {
private suspend fun apiGetUserAddress(): UserContactLinkRec? {
val r = sendCmd(CC.ShowMyAddress())
if (r is CR.UserContactLink) return r.connReqContact
if (r is CR.UserContactLink) return r.contactLink
if (r is CR.ChatCmdError && r.chatError is ChatError.ChatErrorStore
&& r.chatError.storeError is StoreError.UserContactLinkNotFound) {
return null
@@ -648,6 +674,17 @@ open class ChatController(var ctrl: ChatCtrl?, val ntfManager: NtfManager, val a
return null
}
suspend fun userAddressAutoAccept(autoAccept: AutoAccept?): UserContactLinkRec? {
val r = sendCmd(CC.AddressAutoAccept(autoAccept))
if (r is CR.UserContactLinkUpdated) return r.contactLink
if (r is CR.ChatCmdError && r.chatError is ChatError.ChatErrorStore
&& r.chatError.storeError is StoreError.UserContactLinkNotFound) {
return null
}
Log.e(TAG, "userAddressAutoAccept bad response: ${r.responseType} ${r.details}")
return null
}
suspend fun apiAcceptContactRequest(contactReqId: Long): Contact? {
val r = sendCmd(CC.ApiAcceptContact(contactReqId))
return when {
@@ -723,8 +760,15 @@ open class ChatController(var ctrl: ChatCtrl?, val ntfManager: NtfManager, val a
return false
}
suspend fun apiReceiveFile(fileId: Long): AChatItem? {
val r = sendCmd(CC.ReceiveFile(fileId))
suspend fun apiChatUnread(type: ChatType, id: Long, unreadChat: Boolean): Boolean {
val r = sendCmd(CC.ApiChatUnread(type, id, unreadChat))
if (r is CR.CmdOk) return true
Log.e(TAG, "apiChatUnread bad response: ${r.responseType} ${r.details}")
return false
}
suspend fun apiReceiveFile(fileId: Long, inline: Boolean): AChatItem? {
val r = sendCmd(CC.ReceiveFile(fileId, inline))
return when (r) {
is CR.RcvFileAccepted -> r.chatItem
is CR.RcvFileAcceptedSndCancelled -> {
@@ -839,6 +883,40 @@ open class ChatController(var ctrl: ChatCtrl?, val ntfManager: NtfManager, val a
}
}
suspend fun apiCreateGroupLink(groupId: Long): String? {
return when (val r = sendCmd(CC.APICreateGroupLink(groupId))) {
is CR.GroupLinkCreated -> r.connReqContact
else -> {
if (!(networkErrorAlert(r))) {
apiErrorAlert("apiCreateGroupLink", generalGetString(R.string.error_creating_link_for_group), r)
}
null
}
}
}
suspend fun apiDeleteGroupLink(groupId: Long): Boolean {
return when (val r = sendCmd(CC.APIDeleteGroupLink(groupId))) {
is CR.GroupLinkDeleted -> true
else -> {
if (!(networkErrorAlert(r))) {
apiErrorAlert("apiDeleteGroupLink", generalGetString(R.string.error_deleting_link_for_group), r)
}
false
}
}
}
suspend fun apiGetGroupLink(groupId: Long): String? {
return when (val r = sendCmd(CC.APIGetGroupLink(groupId))) {
is CR.GroupLink -> r.connReqContact
else -> {
Log.e(TAG, "apiGetGroupLink bad response: ${r.responseType} ${r.details}")
null
}
}
}
private fun networkErrorAlert(r: CR): Boolean {
return when {
r is CR.ChatCmdError && r.chatError is ChatError.ChatErrorAgent
@@ -880,16 +958,20 @@ open class ChatController(var ctrl: ChatCtrl?, val ntfManager: NtfManager, val a
chatModel.removeChat(r.connection.id)
}
is CR.ContactConnected -> {
chatModel.updateContact(r.contact)
chatModel.dismissConnReqView(r.contact.activeConn.id)
chatModel.removeChat(r.contact.activeConn.id)
chatModel.updateNetworkStatus(r.contact.id, Chat.NetworkStatus.Connected())
ntfManager.notifyContactConnected(r.contact)
if (!r.contact.viaGroupLink) {
chatModel.updateContact(r.contact)
chatModel.dismissConnReqView(r.contact.activeConn.id)
chatModel.removeChat(r.contact.activeConn.id)
chatModel.updateNetworkStatus(r.contact.id, Chat.NetworkStatus.Connected())
ntfManager.notifyContactConnected(r.contact)
}
}
is CR.ContactConnecting -> {
chatModel.updateContact(r.contact)
chatModel.dismissConnReqView(r.contact.activeConn.id)
chatModel.removeChat(r.contact.activeConn.id)
if (!r.contact.viaGroupLink) {
chatModel.updateContact(r.contact)
chatModel.dismissConnReqView(r.contact.activeConn.id)
chatModel.removeChat(r.contact.activeConn.id)
}
}
is CR.ReceivedContactRequest -> {
val contactRequest = r.contactRequest
@@ -903,6 +985,14 @@ open class ChatController(var ctrl: ChatCtrl?, val ntfManager: NtfManager, val a
chatModel.updateChatInfo(cInfo)
}
}
is CR.ContactsMerged -> {
if (chatModel.hasChat(r.mergedContact.id)) {
if (chatModel.chatId.value == r.mergedContact.id) {
chatModel.chatId.value = r.intoContact.id
}
chatModel.removeChat(r.mergedContact.id)
}
}
is CR.ContactsSubscribed -> updateContactsStatus(r.contactRefs, Chat.NetworkStatus.Connected())
is CR.ContactsDisconnected -> updateContactsStatus(r.contactRefs, Chat.NetworkStatus.Disconnected())
is CR.ContactSubError -> processContactSubError(r.contact, r.chatError)
@@ -953,9 +1043,16 @@ 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 -> {
chatModel.updateGroup(r.groupInfo)
if (r.hostContact != null) {
chatModel.dismissConnReqView(r.hostContact.activeConn.id)
chatModel.removeChat(r.hostContact.activeConn.id)
}
}
is CR.JoinedGroupMemberConnecting ->
chatModel.upsertGroupMember(r.groupInfo, r.member)
is CR.DeletedMemberUser -> // TODO update user member
@@ -1056,7 +1153,8 @@ open class ChatController(var ctrl: ChatCtrl?, val ntfManager: NtfManager, val a
}
suspend fun receiveFile(fileId: Long) {
val chatItem = apiReceiveFile(fileId)
val inline = appPrefs.privacyTransferImagesInline.get()
val chatItem = apiReceiveFile(fileId, inline)
if (chatItem != null) {
chatItemSimpleUpdate(chatItem)
}
@@ -1253,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
@@ -1366,6 +1458,9 @@ sealed class CC {
class ApiLeaveGroup(val groupId: Long): CC()
class ApiListMembers(val groupId: Long): CC()
class ApiUpdateGroupProfile(val groupId: Long, val groupProfile: GroupProfile): CC()
class APICreateGroupLink(val groupId: Long): CC()
class APIDeleteGroupLink(val groupId: Long): CC()
class APIGetGroupLink(val groupId: Long): CC()
class GetUserSMPServers: CC()
class SetUserSMPServers(val smpServers: List<String>): CC()
class APISetChatItemTTL(val seconds: Long?): CC()
@@ -1375,6 +1470,8 @@ sealed class CC {
class APISetChatSettings(val type: ChatType, val id: Long, val chatSettings: ChatSettings): CC()
class APIContactInfo(val contactId: Long): CC()
class APIGroupMemberInfo(val groupId: Long, val groupMemberId: Long): CC()
class APISwitchContact(val contactId: Long): CC()
class APISwitchGroupMember(val groupId: Long, val groupMemberId: Long): CC()
class AddContact: CC()
class Connect(val connReq: String): CC()
class ApiDeleteChat(val type: ChatType, val id: Long): CC()
@@ -1384,9 +1481,11 @@ sealed class CC {
class ApiParseMarkdown(val text: String): CC()
class ApiSetContactAlias(val contactId: Long, val localAlias: String): CC()
class ApiSetConnectionAlias(val connId: Long, val localAlias: String): CC()
class ApiSetContactPrefs(val contactId: Long, val prefs: ChatPreferences): CC()
class CreateMyAddress: CC()
class DeleteMyAddress: CC()
class ShowMyAddress: CC()
class AddressAutoAccept(val autoAccept: AutoAccept?): CC()
class ApiSendCallInvitation(val contact: Contact, val callType: CallType): CC()
class ApiRejectCall(val contact: Contact): CC()
class ApiSendCallOffer(val contact: Contact, val callOffer: WebRTCCallOffer): CC()
@@ -1397,7 +1496,8 @@ sealed class CC {
class ApiAcceptContact(val contactReqId: Long): CC()
class ApiRejectContact(val contactReqId: Long): CC()
class ApiChatRead(val type: ChatType, val id: Long, val range: ItemRange): CC()
class ReceiveFile(val fileId: Long): CC()
class ApiChatUnread(val type: ChatType, val id: Long, val unreadChat: Boolean): CC()
class ReceiveFile(val fileId: Long, val inline: Boolean): CC()
val cmdString: String get() = when (this) {
is Console -> cmd
@@ -1424,6 +1524,9 @@ sealed class CC {
is ApiLeaveGroup -> "/_leave #$groupId"
is ApiListMembers -> "/_members #$groupId"
is ApiUpdateGroupProfile -> "/_group_profile #$groupId ${json.encodeToString(groupProfile)}"
is APICreateGroupLink -> "/_create link #$groupId"
is APIDeleteGroupLink -> "/_delete link #$groupId"
is APIGetGroupLink -> "/_get link #$groupId"
is GetUserSMPServers -> "/smp_servers"
is SetUserSMPServers -> "/smp_servers ${smpServersStr(smpServers)}"
is APISetChatItemTTL -> "/_ttl ${chatItemTTLStr(seconds)}"
@@ -1433,6 +1536,8 @@ sealed class CC {
is APISetChatSettings -> "/_settings ${chatRef(type, id)} ${json.encodeToString(chatSettings)}"
is APIContactInfo -> "/_info @$contactId"
is APIGroupMemberInfo -> "/_info #$groupId $groupMemberId"
is APISwitchContact -> "/_switch @$contactId"
is APISwitchGroupMember -> "/_switch #$groupId $groupMemberId"
is AddContact -> "/connect"
is Connect -> "/connect $connReq"
is ApiDeleteChat -> "/_delete ${chatRef(type, id)}"
@@ -1442,9 +1547,11 @@ sealed class CC {
is ApiParseMarkdown -> "/_parse $text"
is ApiSetContactAlias -> "/_set alias @$contactId ${localAlias.trim()}"
is ApiSetConnectionAlias -> "/_set alias :$connId ${localAlias.trim()}"
is ApiSetContactPrefs -> "/_set prefs @$contactId ${json.encodeToString(prefs)}"
is CreateMyAddress -> "/address"
is DeleteMyAddress -> "/delete_address"
is ShowMyAddress -> "/show_address"
is AddressAutoAccept -> "/auto_accept ${AutoAccept.cmdString(autoAccept)}"
is ApiAcceptContact -> "/_accept $contactReqId"
is ApiRejectContact -> "/_reject $contactReqId"
is ApiSendCallInvitation -> "/_call invite @${contact.apiId} ${json.encodeToString(callType)}"
@@ -1455,7 +1562,8 @@ sealed class CC {
is ApiEndCall -> "/_call end @${contact.apiId}"
is ApiCallStatus -> "/_call status @${contact.apiId} ${callStatus.value}"
is ApiChatRead -> "/_read chat ${chatRef(type, id)} from=${range.from} to=${range.to}"
is ReceiveFile -> "/freceive $fileId"
is ApiChatUnread -> "/_unread chat ${chatRef(type, id)} ${onOff(unreadChat)}"
is ReceiveFile -> "/freceive $fileId inline=${onOff(inline)}"
}
val cmdType: String get() = when (this) {
@@ -1483,6 +1591,9 @@ sealed class CC {
is ApiLeaveGroup -> "apiLeaveGroup"
is ApiListMembers -> "apiListMembers"
is ApiUpdateGroupProfile -> "apiUpdateGroupProfile"
is APICreateGroupLink -> "apiCreateGroupLink"
is APIDeleteGroupLink -> "apiDeleteGroupLink"
is APIGetGroupLink -> "apiGetGroupLink"
is GetUserSMPServers -> "getUserSMPServers"
is SetUserSMPServers -> "setUserSMPServers"
is APISetChatItemTTL -> "apiSetChatItemTTL"
@@ -1492,6 +1603,8 @@ sealed class CC {
is APISetChatSettings -> "/apiSetChatSettings"
is APIContactInfo -> "apiContactInfo"
is APIGroupMemberInfo -> "apiGroupMemberInfo"
is APISwitchContact -> "apiSwitchContact"
is APISwitchGroupMember -> "apiSwitchGroupMember"
is AddContact -> "addContact"
is Connect -> "connect"
is ApiDeleteChat -> "apiDeleteChat"
@@ -1501,9 +1614,11 @@ sealed class CC {
is ApiParseMarkdown -> "apiParseMarkdown"
is ApiSetContactAlias -> "apiSetContactAlias"
is ApiSetConnectionAlias -> "apiSetConnectionAlias"
is ApiSetContactPrefs -> "apiSetContactPrefs"
is CreateMyAddress -> "createMyAddress"
is DeleteMyAddress -> "deleteMyAddress"
is ShowMyAddress -> "showMyAddress"
is AddressAutoAccept -> "addressAutoAccept"
is ApiAcceptContact -> "apiAcceptContact"
is ApiRejectContact -> "apiRejectContact"
is ApiSendCallInvitation -> "apiSendCallInvitation"
@@ -1514,6 +1629,7 @@ sealed class CC {
is ApiEndCall -> "apiEndCall"
is ApiCallStatus -> "apiCallStatus"
is ApiChatRead -> "apiChatRead"
is ApiChatUnread -> "apiChatUnread"
is ReceiveFile -> "receiveFile"
}
@@ -1537,7 +1653,7 @@ sealed class CC {
companion object {
fun chatRef(chatType: ChatType, id: Long) = "${chatType.type}${id}"
fun smpServersStr(smpServers: List<String>) = if (smpServers.isEmpty()) "default" else smpServers.joinToString(separator = ",")
fun smpServersStr(smpServers: List<String>) = if (smpServers.isEmpty()) "default" else smpServers.joinToString(separator = ";")
}
}
@@ -1585,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
)
@@ -1594,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
)
@@ -1646,6 +1762,32 @@ data class ChatSettings(
val enableNtfs: Boolean
)
@Serializable
data class ChatPreferences(
val voice: ChatPreference? = null
) {
companion object {
val default = ChatPreferences(
voice = ChatPreference(allow = PrefAllowed.NO)
)
val empty = ChatPreferences(
voice = null
)
}
}
@Serializable
data class ChatPreference(
val allow: PrefAllowed
)
@Serializable
enum class PrefAllowed {
@SerialName("always") ALWAYS,
@SerialName("yes") YES,
@SerialName("no") NO
}
val json = Json {
prettyPrint = true
ignoreUnknownKeys = true
@@ -1698,11 +1840,13 @@ sealed class CR {
@Serializable @SerialName("userProfileUpdated") class UserProfileUpdated(val fromProfile: Profile, val toProfile: Profile): CR()
@Serializable @SerialName("contactAliasUpdated") class ContactAliasUpdated(val toContact: Contact): CR()
@Serializable @SerialName("connectionAliasUpdated") class ConnectionAliasUpdated(val toConnection: PendingContactConnection): CR()
@Serializable @SerialName("contactPrefsUpdated") class ContactPrefsUpdated(val toContact: Contact): CR()
@Serializable @SerialName("apiParsedMarkdown") class ParsedMarkdown(val formattedText: List<FormattedText>? = null): CR()
@Serializable @SerialName("userContactLink") class UserContactLink(val connReqContact: String): CR()
@Serializable @SerialName("userContactLink") class UserContactLink(val contactLink: UserContactLinkRec): CR()
@Serializable @SerialName("userContactLinkUpdated") class UserContactLinkUpdated(val contactLink: UserContactLinkRec): CR()
@Serializable @SerialName("userContactLinkCreated") class UserContactLinkCreated(val connReqContact: String): CR()
@Serializable @SerialName("userContactLinkDeleted") class UserContactLinkDeleted: CR()
@Serializable @SerialName("contactConnected") class ContactConnected(val contact: Contact): CR()
@Serializable @SerialName("contactConnected") class ContactConnected(val contact: Contact, val userCustomProfile: Profile? = null): CR()
@Serializable @SerialName("contactConnecting") class ContactConnecting(val contact: Contact): CR()
@Serializable @SerialName("receivedContactRequest") class ReceivedContactRequest(val contactRequest: UserContactRequest): CR()
@Serializable @SerialName("acceptingContactRequest") class AcceptingContactRequest(val contact: Contact): CR()
@@ -1724,7 +1868,7 @@ sealed class CR {
// group events
@Serializable @SerialName("groupCreated") class GroupCreated(val groupInfo: GroupInfo): CR()
@Serializable @SerialName("sentGroupInvitation") class SentGroupInvitation(val groupInfo: GroupInfo, val contact: Contact, val member: GroupMember): CR()
@Serializable @SerialName("userAcceptedGroupSent") class UserAcceptedGroupSent (val groupInfo: GroupInfo): CR()
@Serializable @SerialName("userAcceptedGroupSent") class UserAcceptedGroupSent (val groupInfo: GroupInfo, val hostContact: Contact? = null): CR()
@Serializable @SerialName("userDeletedMember") class UserDeletedMember(val groupInfo: GroupInfo, val member: GroupMember): CR()
@Serializable @SerialName("leftMemberUser") class LeftMemberUser(val groupInfo: GroupInfo): CR()
@Serializable @SerialName("groupMembers") class GroupMembers(val group: Group): CR()
@@ -1744,6 +1888,9 @@ sealed class CR {
@Serializable @SerialName("connectedToGroupMember") class ConnectedToGroupMember(val groupInfo: GroupInfo, val member: GroupMember): CR()
@Serializable @SerialName("groupRemoved") class GroupRemoved(val groupInfo: GroupInfo): CR() // unused
@Serializable @SerialName("groupUpdated") class GroupUpdated(val toGroup: GroupInfo): CR()
@Serializable @SerialName("groupLinkCreated") class GroupLinkCreated(val groupInfo: GroupInfo, val connReqContact: String): CR()
@Serializable @SerialName("groupLink") class GroupLink(val groupInfo: GroupInfo, val connReqContact: String): CR()
@Serializable @SerialName("groupLinkDeleted") class GroupLinkDeleted(val groupInfo: GroupInfo): CR()
// receiving file events
@Serializable @SerialName("rcvFileAccepted") class RcvFileAccepted(val chatItem: AChatItem): CR()
@Serializable @SerialName("rcvFileAcceptedSndCancelled") class RcvFileAcceptedSndCancelled(val rcvFileTransfer: RcvFileTransfer): CR()
@@ -1790,8 +1937,10 @@ sealed class CR {
is UserProfileUpdated -> "userProfileUpdated"
is ContactAliasUpdated -> "contactAliasUpdated"
is ConnectionAliasUpdated -> "connectionAliasUpdated"
is ContactPrefsUpdated -> "contactPrefsUpdated"
is ParsedMarkdown -> "apiParsedMarkdown"
is UserContactLink -> "userContactLink"
is UserContactLinkUpdated -> "userContactLinkUpdated"
is UserContactLinkCreated -> "userContactLinkCreated"
is UserContactLinkDeleted -> "userContactLinkDeleted"
is ContactConnected -> "contactConnected"
@@ -1835,6 +1984,9 @@ sealed class CR {
is ConnectedToGroupMember -> "connectedToGroupMember"
is GroupRemoved -> "groupRemoved"
is GroupUpdated -> "groupUpdated"
is GroupLinkCreated -> "groupLinkCreated"
is GroupLink -> "groupLink"
is GroupLinkDeleted -> "groupLinkDeleted"
is RcvFileAcceptedSndCancelled -> "rcvFileAcceptedSndCancelled"
is RcvFileAccepted -> "rcvFileAccepted"
is RcvFileStart -> "rcvFileStart"
@@ -1880,8 +2032,10 @@ sealed class CR {
is UserProfileUpdated -> json.encodeToString(toProfile)
is ContactAliasUpdated -> json.encodeToString(toContact)
is ConnectionAliasUpdated -> json.encodeToString(toConnection)
is ContactPrefsUpdated -> json.encodeToString(toContact)
is ParsedMarkdown -> json.encodeToString(formattedText)
is UserContactLink -> connReqContact
is UserContactLink -> contactLink.responseDetails
is UserContactLinkUpdated -> contactLink.responseDetails
is UserContactLinkCreated -> connReqContact
is UserContactLinkDeleted -> noDetails()
is ContactConnected -> json.encodeToString(contact)
@@ -1925,6 +2079,9 @@ sealed class CR {
is ConnectedToGroupMember -> "groupInfo: $groupInfo\nmember: $member"
is GroupRemoved -> json.encodeToString(groupInfo)
is GroupUpdated -> json.encodeToString(toGroup)
is GroupLinkCreated -> "groupInfo: $groupInfo\nconnReqContact: $connReqContact"
is GroupLink -> "groupInfo: $groupInfo\nconnReqContact: $connReqContact"
is GroupLinkDeleted -> json.encodeToString(groupInfo)
is RcvFileAcceptedSndCancelled -> noDetails()
is RcvFileAccepted -> json.encodeToString(chatItem)
is RcvFileStart -> json.encodeToString(chatItem)
@@ -1981,6 +2138,24 @@ abstract class TerminalItem {
@Serializable
class ConnectionStats(val rcvServers: List<String>?, val sndServers: List<String>?)
@Serializable
class UserContactLinkRec(val connReqContact: String, val autoAccept: AutoAccept? = null) {
val responseDetails: String get() = "connReqContact: ${connReqContact}\nautoAccept: ${AutoAccept.cmdString(autoAccept)}"
}
@Serializable
class AutoAccept(val acceptIncognito: Boolean, val autoReply: MsgContent?) {
companion object {
fun cmdString(autoAccept: AutoAccept?): String {
if (autoAccept == null) return "off"
val s = "on" + if (autoAccept.acceptIncognito) " incognito=on" else ""
val msg = autoAccept.autoReply ?: return s
return s + " " + msg.cmdString
}
}
}
@Serializable
sealed class ChatError {
val string: String get() = when (this) {
@@ -2000,12 +2175,10 @@ sealed class ChatErrorType {
val string: String get() = when (this) {
is NoActiveUser -> "noActiveUser"
is InvalidConnReq -> "invalidConnReq"
is ContactGroups -> "groupNames $groupNames"
is СommandError -> "commandError $message"
}
@Serializable @SerialName("noActiveUser") class NoActiveUser: ChatErrorType()
@Serializable @SerialName("invalidConnReq") class InvalidConnReq: ChatErrorType()
@Serializable @SerialName("contactGroups") class ContactGroups(val contact: Contact, val groupNames: List<String>): ChatErrorType()
@Serializable @SerialName("commandError") class СommandError(val message: String): ChatErrorType()
}
@@ -7,7 +7,7 @@ val Purple500 = Color(0xFF6200EE)
val Purple700 = Color(0xFF3700B3)
val Teal200 = Color(0xFF03DAC5)
val Gray = Color(0x22222222)
val Indigo = Color(0xff330099)
val Indigo = Color(0xFF9966FF)
val SimplexBlue = Color(0, 136, 255, 255) // If this value changes also need to update #0088ff in string resource files
val SimplexGreen = Color(77, 218, 103, 255)
val SecretColor = Color(0x40808080)
@@ -2,6 +2,7 @@ package chat.simplex.app.views
import android.content.Context
import android.content.res.Configuration
import android.os.SystemClock
import androidx.activity.compose.BackHandler
import androidx.compose.foundation.*
import androidx.compose.foundation.layout.*
@@ -24,25 +25,34 @@ 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
import com.google.accompanist.insets.navigationBarsWithImePadding
private val lastSuccessfulAuth: MutableState<Long?> = mutableStateOf(null)
@Composable
fun TerminalView(chatModel: ChatModel, close: () -> Unit) {
val composeState = remember { mutableStateOf(ComposeState(useLinkPreviews = false)) }
BackHandler(onBack = close)
val authorized = remember { mutableStateOf(!chatModel.controller.appPrefs.performLA.get()) }
val lastSuccessfulAuth = remember { lastSuccessfulAuth }
BackHandler(onBack = {
lastSuccessfulAuth.value = null
close()
})
val authorized = remember { !chatModel.controller.appPrefs.performLA.get() }
val context = LocalContext.current
LaunchedEffect(authorized.value) {
if (!authorized.value) {
runAuth(authorized = authorized, context)
LaunchedEffect(lastSuccessfulAuth.value) {
if (!authorized && !authorizedPreviously(lastSuccessfulAuth)) {
runAuth(lastSuccessfulAuth, context)
}
}
if (authorized.value) {
if (authorized || authorizedPreviously(lastSuccessfulAuth)) {
LaunchedEffect(Unit) {
// Update auth each time user visits this screen in authenticated state just to prolong authorized time
lastSuccessfulAuth.value = SystemClock.elapsedRealtime()
}
TerminalLayout(
chatModel.terminalItems,
composeState,
@@ -61,7 +71,7 @@ fun TerminalView(chatModel: ChatModel, close: () -> Unit) {
stringResource(R.string.auth_unlock),
icon = Icons.Outlined.Lock,
click = {
runAuth(authorized = authorized, context)
runAuth(lastSuccessfulAuth, context)
}
)
}
@@ -70,15 +80,18 @@ fun TerminalView(chatModel: ChatModel, close: () -> Unit) {
}
}
private fun runAuth(authorized: MutableState<Boolean>, context: Context) {
private fun authorizedPreviously(lastSuccessfulAuth: State<Long?>): Boolean =
lastSuccessfulAuth.value?.let { SystemClock.elapsedRealtime() - it < 30_000 } ?: false
private fun runAuth(lastSuccessfulAuth: MutableState<Long?>, context: Context) {
authenticate(
generalGetString(R.string.auth_open_chat_console),
generalGetString(R.string.auth_log_in_using_credential),
context as FragmentActivity,
completed = { laResult ->
when (laResult) {
LAResult.Success, LAResult.Unavailable -> authorized.value = true
is LAResult.Error, LAResult.Failed -> authorized.value = false
lastSuccessfulAuth.value = when (laResult) {
LAResult.Success, LAResult.Unavailable -> SystemClock.elapsedRealtime()
is LAResult.Error, LAResult.Failed -> null
}
}
)
@@ -139,13 +152,19 @@ fun TerminalLayout(
}
}
private var lazyListState = 0 to 0
@Composable
fun TerminalLog(terminalItems: List<TerminalItem>) {
val listState = rememberLazyListState()
val listState = rememberLazyListState(lazyListState.first, lazyListState.second)
DisposableEffect(Unit) {
onDispose { lazyListState = listState.firstVisibleItemIndex to listState.firstVisibleItemScrollOffset }
}
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,
@@ -154,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)
@@ -93,7 +93,7 @@ sealed class WCallResponse {
@Serializable class WebRTCSession(val rtcSession: String, val rtcIceCandidates: String)
@Serializable class WebRTCExtraInfo(val rtcIceCandidates: String)
@Serializable class CallType(val media: CallMediaType, val capabilities: CallCapabilities)
@Serializable class RcvCallInvitation(val contact: Contact, val callType: CallType, val sharedKey: String?, val callTs: Instant) {
@Serializable class RcvCallInvitation(val contact: Contact, val callType: CallType, val sharedKey: String? = null, val callTs: Instant) {
val callTypeText: String get() = generalGetString(when(callType.media) {
CallMediaType.Video -> if (sharedKey == null) R.string.video_call_no_encryption else R.string.encrypted_video_call
CallMediaType.Audio -> if (sharedKey == null) R.string.audio_call_no_encryption else R.string.encrypted_audio_call
@@ -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,10 +60,13 @@ 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) },
switchContactAddress = {
showSwitchContactAddressAlert(chatModel, contact.contactId)
}
)
}
}
@@ -117,6 +119,7 @@ fun ChatInfoLayout(
onLocalAliasChanged: (String) -> Unit,
deleteContact: () -> Unit,
clearChat: () -> Unit,
switchContactAddress: () -> Unit,
) {
Column(
Modifier
@@ -142,8 +145,12 @@ fun ChatInfoLayout(
SectionSpacer()
if (connStats != null) {
SectionView(title = stringResource(R.string.conn_stats_section_title_servers)) {
SectionView(title = stringResource(R.string.conn_stats_section_title_servers)) {
if (developerTools) {
SwitchAddressButton(switchContactAddress)
SectionDivider()
}
if (connStats != null) {
SectionItemView({
AlertManager.shared.showAlertMsg(
generalGetString(R.string.network_status),
@@ -162,8 +169,8 @@ fun ChatInfoLayout(
SimplexServers(stringResource(R.string.sending_via), sndServers)
}
}
SectionSpacer()
}
SectionSpacer()
SectionView {
ClearChatButton(clearChat)
SectionDivider()
@@ -231,7 +238,9 @@ fun LocalAliasEditor(
color = HighOrLowlight
)
},
leadingIcon = if (leadingIcon) {{ Icon(Icons.Default.Edit, null, Modifier.padding(start = 7.dp)) }} else null,
leadingIcon = if (leadingIcon) {
{ Icon(Icons.Default.Edit, null, Modifier.padding(start = 7.dp)) }
} else null,
color = HighOrLowlight,
focus = focus,
textStyle = TextStyle.Default.copy(textAlign = if (value.isEmpty() || !center) TextAlign.Start else TextAlign.Center),
@@ -311,6 +320,13 @@ fun SimplexServers(text: String, servers: List<String>) {
}
}
@Composable
fun SwitchAddressButton(onClick: () -> Unit) {
SectionItemView(onClick) {
Text(stringResource(R.string.switch_receiving_address), color = MaterialTheme.colors.primary)
}
}
@Composable
fun ClearChatButton(onClick: () -> Unit) {
SettingsActionItem(
@@ -333,13 +349,27 @@ 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)
}
}
private fun showSwitchContactAddressAlert(m: ChatModel, contactId: Long) {
AlertManager.shared.showAlertMsg(
title = generalGetString(R.string.switch_receiving_address_question),
text = generalGetString(R.string.switch_receiving_address_desc),
confirmText = generalGetString(R.string.switch_verb),
onConfirm = {
switchContactAddress(m, contactId)
}
)
}
private fun switchContactAddress(m: ChatModel, contactId: Long) = withApi {
m.controller.apiSwitchContact(contactId)
}
@Preview
@Composable
fun PreviewChatInfoLayout() {
@@ -356,7 +386,9 @@ fun PreviewChatInfoLayout() {
connStats = null,
onLocalAliasChanged = {},
customUserProfile = null,
deleteContact = {}, clearChat = {}
deleteContact = {},
clearChat = {},
switchContactAddress = {},
)
}
}
@@ -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.*
@@ -48,11 +50,10 @@ import kotlinx.coroutines.flow.*
import kotlinx.datetime.Clock
import java.io.File
import kotlin.math.sign
import kotlin.math.roundToInt
@Composable
fun ChatView(chatModel: ChatModel) {
var activeChat by remember { mutableStateOf(chatModel.chats.firstOrNull { chat -> chat.chatInfo.id == chatModel.chatId.value }) }
val activeChat = remember { mutableStateOf(chatModel.chats.firstOrNull { chat -> chat.chatInfo.id == chatModel.chatId.value }) }
val searchText = rememberSaveable { mutableStateOf("") }
val user = chatModel.currentUser.value
val useLinkPreviews = chatModel.controller.appPrefs.privacyLinkPreviews.get()
@@ -66,23 +67,36 @@ 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 {
activeChat = if (chatModel.chatId.value == null) {
null
} else {
// 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!!)
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
activeChat.value = chatModel.getChat(chatModel.chatId.value!!)
}
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
}}
}
}
}
if (activeChat == null || user == null) {
val view = LocalView.current
if (activeChat.value == null || user == null) {
chatModel.chatId.value = null
} else {
val chat = activeChat!!
val chat = activeChat.value!!
BackHandler { chatModel.chatId.value = null }
// We need to have real unreadCount value for displaying it inside top right button
// Having activeChat reloaded on every change in it is inefficient (UI lags)
@@ -112,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 = it
}
ChatInfoView(chatModel, cInfo.contact, contactInfo?.first, contactInfo?.second, chat.chatInfo.localAlias, close)
}
} else if (cInfo is ChatInfo.Group) {
setGroupMembers(cInfo.groupInfo, chatModel)
@@ -132,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 ->
@@ -175,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!")
@@ -183,6 +201,7 @@ fun ChatView(chatModel: ChatModel) {
}
},
addMembers = { groupInfo ->
hideKeyboard(view)
withApi {
setGroupMembers(groupInfo, chatModel)
ModalManager.shared.showModalCloseable(true) { close ->
@@ -447,16 +466,7 @@ fun BoxWithConstraintsScope.ChatItemsList(
val scope = rememberCoroutineScope()
val uriHandler = LocalUriHandler.current
val cxt = LocalContext.current
// Helps to scroll to bottom after moving from Group to Direct chat
// and prevents scrolling to bottom on orientation change
var shouldAutoScroll by rememberSaveable { mutableStateOf(true) }
LaunchedEffect(chat.chatInfo.apiId, chat.chatInfo.chatType, shouldAutoScroll) {
if (shouldAutoScroll && listState.firstVisibleItemIndex != 0) {
scope.launch { listState.scrollToItem(0) }
}
// Don't autoscroll next time until it will be needed
shouldAutoScroll = false
}
ScrollToBottom(chat.id, listState)
var prevSearchEmptiness by rememberSaveable { mutableStateOf(searchValue.value.isEmpty()) }
// Scroll to bottom when search value changes from something to nothing and back
LaunchedEffect(searchValue.value.isEmpty()) {
@@ -548,7 +558,7 @@ fun BoxWithConstraintsScope.ChatItemsList(
ChatItemView(user, chat.chatInfo, cItem, composeState, cxt, uriHandler, provider, showMember = showMember, chatModelIncognito = chatModelIncognito, useLinkPreviews = useLinkPreviews, deleteMessage = deleteMessage, receiveFile = receiveFile, joinGroup = {}, acceptCall = acceptCall, scrollToItem = scrollToItem)
}
} else {
Box(Modifier.padding(start = 86.dp, end = 12.dp).then(swipeableModifier)) {
Box(Modifier.padding(start = 104.dp, end = 12.dp).then(swipeableModifier)) {
ChatItemView(user, chat.chatInfo, cItem, composeState, cxt, uriHandler, provider, chatModelIncognito = chatModelIncognito, useLinkPreviews = useLinkPreviews, deleteMessage = deleteMessage, receiveFile = receiveFile, joinGroup = {}, acceptCall = acceptCall, scrollToItem = scrollToItem)
}
}
@@ -578,6 +588,21 @@ fun BoxWithConstraintsScope.ChatItemsList(
FloatingButtons(chatItems, unreadCount, chat.chatStats.minUnreadItemId, searchValue, markRead, setFloatingButton, listState)
}
@Composable
private fun ScrollToBottom(chatId: ChatId, listState: LazyListState) {
val scope = rememberCoroutineScope()
// Helps to scroll to bottom after moving from Group to Direct chat
// and prevents scrolling to bottom on orientation change
var shouldAutoScroll by rememberSaveable { mutableStateOf(true to chatId) }
LaunchedEffect(chatId, shouldAutoScroll) {
if ((shouldAutoScroll.first || shouldAutoScroll.second != chatId) && listState.firstVisibleItemIndex != 0) {
scope.launch { listState.scrollToItem(0) }
}
// Don't autoscroll next time until it will be needed
shouldAutoScroll = false to chatId
}
}
@Composable
fun BoxWithConstraintsScope.FloatingButtons(
chatItems: List<ChatItem>,
@@ -784,6 +809,22 @@ private fun bottomEndFloatingButton(
}
}
private fun markUnreadChatAsRead(activeChat: MutableState<Chat?>, chatModel: ChatModel) {
val chat = activeChat.value
if (chat?.chatStats?.unreadChat != true) return
withApi {
val success = chatModel.controller.apiChatUnread(
chat.chatInfo.chatType,
chat.chatInfo.apiId,
false
)
if (success && chat.id == activeChat.value?.id) {
activeChat.value = chat.copy(chatStats = chat.chatStats.copy(unreadChat = false))
chatModel.replaceChat(chat.id, activeChat.value!!)
}
}
}
private fun providerForGallery(
listStateIndex: Int,
chatItems: List<ChatItem>,
@@ -10,7 +10,6 @@ import androidx.compose.material.icons.outlined.Close
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
@@ -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)))
}
}
@@ -194,13 +161,22 @@ fun ComposeView(
Toast.makeText(context, generalGetString(R.string.toast_permission_denied), Toast.LENGTH_SHORT).show()
}
}
val processPickedImage = { uris: List<Uri> ->
val processPickedImage = { uris: List<Uri>, text: String? ->
val content = ArrayList<UploadContent>()
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))
@@ -223,17 +199,17 @@ fun ComposeView(
if (imagesPreview.isNotEmpty()) {
chosenContent.value = content
composeState.value = composeState.value.copy(preview = ComposePreview.ImagePreview(imagesPreview))
composeState.value = composeState.value.copy(message = text ?: composeState.value.message, preview = ComposePreview.ImagePreview(imagesPreview))
}
}
val processPickedFile = { uri: Uri? ->
val processPickedFile = { uri: Uri?, text: String? ->
if (uri != null) {
val fileSize = getFileSize(context, uri)
if (fileSize != null && fileSize <= MAX_FILE_SIZE) {
val fileName = getFileName(SimplexApp.context, uri)
if (fileName != null) {
chosenFile.value = uri
composeState.value = composeState.value.copy(preview = ComposePreview.FilePreview(fileName))
composeState.value = composeState.value.copy(message = text ?: composeState.value.message, preview = ComposePreview.FilePreview(fileName))
}
} else {
AlertManager.shared.showAlertMsg(
@@ -243,9 +219,9 @@ fun ComposeView(
}
}
}
val galleryLauncher = rememberLauncherForActivityResult(contract = PickFromGallery(), processPickedImage)
val galleryLauncherFallback = rememberGetMultipleContentsLauncher(processPickedImage)
val filesLauncher = rememberGetContentLauncher(processPickedFile)
val galleryLauncher = rememberLauncherForActivityResult(contract = PickMultipleFromGallery()) { processPickedImage(it, null) }
val galleryLauncherFallback = rememberGetMultipleContentsLauncher { processPickedImage(it, null) }
val filesLauncher = rememberGetContentLauncher { processPickedFile(it, null) }
LaunchedEffect(attachmentOption.value) {
when (attachmentOption.value) {
@@ -294,6 +270,9 @@ fun ComposeView(
if (lp != null && pendingLinkUrl.value == url) {
composeState.value = composeState.value.copy(preview = ComposePreview.CLinkPreview(lp))
pendingLinkUrl.value = null
} else if (pendingLinkUrl.value == url) {
composeState.value = composeState.value.copy(preview = ComposePreview.NoPreview)
pendingLinkUrl.value = null
}
}
}
@@ -388,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) {
@@ -500,8 +479,8 @@ fun ComposeView(
LaunchedEffect(chatModel.sharedContent.value) {
when (val shared = chatModel.sharedContent.value) {
is SharedContent.Text -> onMessageChange(shared.text)
is SharedContent.Image -> processPickedImage(listOf(shared.uri))
is SharedContent.File -> processPickedFile(shared.uri)
is SharedContent.Images -> processPickedImage(shared.uris, shared.text)
is SharedContent.File -> processPickedFile(shared.uri, shared.text)
null -> {}
}
chatModel.sharedContent.value = null
@@ -547,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)
@@ -1,12 +1,15 @@
package chat.simplex.app.views.chat
import android.annotation.SuppressLint
import android.content.Context
import android.content.res.Configuration
import android.text.InputType
import android.view.ViewGroup
import android.view.inputmethod.*
import android.widget.EditText
import androidx.compose.foundation.*
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.BasicTextField
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Check
@@ -14,27 +17,25 @@ import androidx.compose.material.icons.outlined.ArrowUpward
import androidx.compose.runtime.*
import androidx.compose.ui.*
import androidx.compose.ui.draw.clip
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
import androidx.compose.ui.graphics.*
import androidx.compose.ui.platform.*
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.input.KeyboardCapitalization
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp
import androidx.core.text.BidiFormatter
import androidx.compose.ui.viewinterop.AndroidView
import androidx.core.graphics.drawable.DrawableCompat
import androidx.core.view.inputmethod.EditorInfoCompat
import androidx.core.view.inputmethod.InputConnectionCompat
import androidx.core.widget.doOnTextChanged
import chat.simplex.app.R
import chat.simplex.app.SimplexApp
import chat.simplex.app.model.ChatItem
import chat.simplex.app.ui.theme.HighOrLowlight
import chat.simplex.app.ui.theme.SimpleXTheme
import chat.simplex.app.views.helpers.SharedContent
import kotlinx.coroutines.delay
import kotlin.math.min
@OptIn(ExperimentalComposeUiApi::class)
@Composable
fun SendMsgView(
composeState: MutableState<ComposeState>,
@@ -43,82 +44,114 @@ fun SendMsgView(
textStyle: MutableState<TextStyle>
) {
val cs = composeState.value
val focusRequester = remember { FocusRequester() }
val keyboard = LocalSoftwareKeyboardController.current
var showKeyboard by remember { mutableStateOf(false) }
LaunchedEffect(cs.contextItem) {
if (cs.contextItem !is ComposeContextItem.QuotedItem) return@LaunchedEffect
// In replying state
focusRequester.requestFocus()
delay(50)
keyboard?.show()
when (cs.contextItem) {
is ComposeContextItem.QuotedItem -> {
delay(100)
showKeyboard = true
}
is ComposeContextItem.EditingItem -> {
// Keyboard will not show up if we try to show it too fast
delay(300)
showKeyboard = true
}
}
}
val isRtl = remember(cs.message) { BidiFormatter.getInstance().isRtl(cs.message.subSequence(0, min(50, cs.message.length))) }
BasicTextField(
value = cs.message,
onValueChange = onMessageChange,
textStyle = textStyle.value,
maxLines = 16,
keyboardOptions = KeyboardOptions.Default.copy(
capitalization = KeyboardCapitalization.Sentences,
autoCorrect = true
),
modifier = Modifier.padding(vertical = 8.dp).focusRequester(focusRequester),
cursorBrush = SolidColor(HighOrLowlight),
decorationBox = { innerTextField ->
Surface(
shape = RoundedCornerShape(18.dp),
border = BorderStroke(1.dp, MaterialTheme.colors.secondary)
) {
Row(
Modifier.background(MaterialTheme.colors.background),
verticalAlignment = Alignment.Bottom
) {
CompositionLocalProvider(
LocalLayoutDirection provides if (isRtl) LayoutDirection.Rtl else LocalLayoutDirection.current
val textColor = MaterialTheme.colors.onBackground
val tintColor = MaterialTheme.colors.secondary
val paddingStart = with(LocalDensity.current) { 12.dp.roundToPx() }
val paddingTop = with(LocalDensity.current) { 7.dp.roundToPx() }
val paddingEnd = with(LocalDensity.current) { 45.dp.roundToPx() }
val paddingBottom = with(LocalDensity.current) { 7.dp.roundToPx() }
Column(Modifier.padding(vertical = 8.dp)) {
Box {
AndroidView(modifier = Modifier, factory = {
val editText = @SuppressLint("AppCompatCustomView") object: EditText(it) {
override fun setOnReceiveContentListener(
mimeTypes: Array<out String>?,
listener: android.view.OnReceiveContentListener?
) {
Box(
Modifier
.weight(1f)
.padding(horizontal = 12.dp)
.padding(top = 5.dp)
.padding(bottom = 7.dp)
) {
innerTextField()
}
super.setOnReceiveContentListener(mimeTypes, listener)
}
val icon = if (cs.editing) Icons.Filled.Check else Icons.Outlined.ArrowUpward
val color = if (cs.sendEnabled()) MaterialTheme.colors.primary else HighOrLowlight
if (cs.inProgress
&& (cs.preview is ComposePreview.ImagePreview || cs.preview is ComposePreview.FilePreview)
) {
CircularProgressIndicator(
Modifier
.size(36.dp)
.padding(4.dp),
color = HighOrLowlight,
strokeWidth = 3.dp
)
} else {
Icon(
icon,
stringResource(R.string.icon_descr_send_message),
tint = Color.White,
modifier = Modifier
.size(36.dp)
.padding(4.dp)
.clip(CircleShape)
.background(color)
.clickable {
if (cs.sendEnabled()) {
sendMessage()
}
override fun onCreateInputConnection(editorInfo: EditorInfo): InputConnection {
val connection = super.onCreateInputConnection(editorInfo)
EditorInfoCompat.setContentMimeTypes(editorInfo, arrayOf("image/*"))
val onCommit = InputConnectionCompat.OnCommitContentListener { inputContentInfo, _, _ ->
try {
inputContentInfo.requestPermission()
} catch (e: Exception) {
return@OnCommitContentListener false
}
)
SimplexApp.context.chatModel.sharedContent.value = SharedContent.Images("", listOf(inputContentInfo.contentUri))
true
}
return InputConnectionCompat.createWrapper(connection, editorInfo, onCommit)
}
}
editText.layoutParams = ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)
editText.maxLines = 16
editText.inputType = InputType.TYPE_TEXT_FLAG_CAP_SENTENCES or editText.inputType
editText.setTextColor(textColor.toArgb())
editText.textSize = textStyle.value.fontSize.value
val drawable = it.getDrawable(R.drawable.send_msg_view_background)!!
DrawableCompat.setTint(drawable, tintColor.toArgb())
editText.background = drawable
editText.setPadding(paddingStart, paddingTop, paddingEnd, paddingBottom)
editText.setText(cs.message)
editText.textCursorDrawable?.let { DrawableCompat.setTint(it, HighOrLowlight.toArgb()) }
editText.doOnTextChanged { text, _, _, _ -> onMessageChange(text.toString()) }
editText
}) {
it.setTextColor(textColor.toArgb())
it.textSize = textStyle.value.fontSize.value
DrawableCompat.setTint(it.background, tintColor.toArgb())
if (cs.message != it.text.toString()) {
it.setText(cs.message)
// Set cursor to the end of the text
it.setSelection(it.text.length)
}
if (showKeyboard) {
it.requestFocus()
val imm: InputMethodManager = SimplexApp.context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
imm.showSoftInput(it, InputMethodManager.SHOW_IMPLICIT)
showKeyboard = false
}
}
Box(Modifier.align(Alignment.BottomEnd)) {
val icon = if (cs.editing) Icons.Filled.Check else Icons.Outlined.ArrowUpward
val color = if (cs.sendEnabled()) MaterialTheme.colors.primary else HighOrLowlight
if (cs.inProgress
&& (cs.preview is ComposePreview.ImagePreview || cs.preview is ComposePreview.FilePreview)
) {
CircularProgressIndicator(
Modifier
.size(36.dp)
.padding(4.dp),
color = HighOrLowlight,
strokeWidth = 3.dp
)
} else {
Icon(
icon,
stringResource(R.string.icon_descr_send_message),
tint = Color.White,
modifier = Modifier
.size(36.dp)
.padding(4.dp)
.clip(CircleShape)
.background(color)
.clickable {
if (cs.sendEnabled()) {
sendMessage()
}
}
)
}
}
}
)
}
}
@Preview(showBackground = true)
@@ -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 = {},
@@ -65,6 +65,12 @@ fun GroupChatInfoView(chatModel: ChatModel, close: () -> Unit) {
deleteGroup = { deleteGroupDialog(chat.chatInfo, groupInfo, chatModel, close) },
clearChat = { clearChatDialog(chat.chatInfo, chatModel, close) },
leaveGroup = { leaveGroupDialog(groupInfo, chatModel, close) },
manageGroupLink = {
withApi {
val groupLink = chatModel.controller.apiGetGroupLink(groupInfo.groupId)
ModalManager.shared.showModal { GroupLinkView(chatModel, groupInfo, groupLink) }
}
}
)
}
}
@@ -117,6 +123,7 @@ fun GroupChatInfoLayout(
deleteGroup: () -> Unit,
clearChat: () -> Unit,
leaveGroup: () -> Unit,
manageGroupLink: () -> Unit,
) {
Column(
Modifier
@@ -134,6 +141,8 @@ fun GroupChatInfoLayout(
SectionView(title = String.format(generalGetString(R.string.group_info_section_title_num_members), members.count() + 1)) {
if (groupInfo.canAddMembers) {
SectionItemView(manageGroupLink) { GroupLinkButton() }
SectionDivider()
val onAddMembersClick = if (chat.chatInfo.incognito) ::cantInviteIncognitoAlert else addMembers
SectionItemView(onAddMembersClick) {
val tint = if (chat.chatInfo.incognito) HighOrLowlight else MaterialTheme.colors.primary
@@ -150,7 +159,6 @@ fun GroupChatInfoLayout(
MembersList(members, showMemberInfo)
}
SectionSpacer()
SectionView {
if (groupInfo.canEdit) {
SectionItemView(editGroupProfile) { EditGroupProfileButton() }
@@ -268,6 +276,23 @@ fun MemberRow(member: GroupMember, user: Boolean = false) {
}
}
@Composable
fun GroupLinkButton() {
Row(
Modifier
.fillMaxSize(),
verticalAlignment = Alignment.CenterVertically
) {
Icon(
Icons.Outlined.Link,
stringResource(R.string.group_link),
tint = MaterialTheme.colors.primary
)
Spacer(Modifier.size(8.dp))
Text(stringResource(R.string.group_link), color = MaterialTheme.colors.primary)
}
}
@Composable
fun EditGroupProfileButton() {
Row(
@@ -330,7 +355,7 @@ fun PreviewGroupChatInfoLayout() {
groupInfo = GroupInfo.sampleData,
members = listOf(GroupMember.sampleData, GroupMember.sampleData, GroupMember.sampleData),
developerTools = false,
addMembers = {}, showMemberInfo = {}, editGroupProfile = {}, deleteGroup = {}, clearChat = {}, leaveGroup = {},
addMembers = {}, showMemberInfo = {}, editGroupProfile = {}, deleteGroup = {}, clearChat = {}, leaveGroup = {}, manageGroupLink = {},
)
}
}
@@ -0,0 +1,101 @@
package chat.simplex.app.views.chat.group
import androidx.compose.foundation.layout.*
import androidx.compose.material.Text
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import chat.simplex.app.R
import chat.simplex.app.model.ChatModel
import chat.simplex.app.model.GroupInfo
import chat.simplex.app.ui.theme.DEFAULT_PADDING
import chat.simplex.app.ui.theme.SimpleButton
import chat.simplex.app.views.helpers.*
import chat.simplex.app.views.newchat.QRCode
@Composable
fun GroupLinkView(chatModel: ChatModel, groupInfo: GroupInfo, connReqContact: String?) {
var groupLink by remember { mutableStateOf(connReqContact) }
val cxt = LocalContext.current
GroupLinkLayout(
groupLink = groupLink,
createLink = {
withApi {
groupLink = chatModel.controller.apiCreateGroupLink(groupInfo.groupId)
}
},
share = { shareText(cxt, groupLink ?: return@GroupLinkLayout) },
deleteLink = {
AlertManager.shared.showAlertMsg(
title = generalGetString(R.string.delete_link_question),
text = generalGetString(R.string.all_group_members_will_remain_connected),
confirmText = generalGetString(R.string.delete_verb),
onConfirm = {
withApi {
val r = chatModel.controller.apiDeleteGroupLink(groupInfo.groupId)
if (r) {
groupLink = null
}
}
}
)
}
)
}
@Composable
fun GroupLinkLayout(
groupLink: String?,
createLink: () -> Unit,
share: () -> Unit,
deleteLink: () -> Unit
) {
Column(
Modifier.padding(horizontal = DEFAULT_PADDING),
horizontalAlignment = Alignment.Start,
verticalArrangement = Arrangement.Top
) {
AppBarTitle(stringResource(R.string.group_link), false)
Text(
stringResource(R.string.you_can_share_group_link_anybody_will_be_able_to_connect),
Modifier.padding(bottom = 12.dp),
lineHeight = 22.sp
)
Column(
Modifier.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.SpaceEvenly
) {
if (groupLink == null) {
SimpleButton(stringResource(R.string.button_create_group_link), icon = Icons.Outlined.AddLink, click = createLink)
} else {
QRCode(groupLink, Modifier.weight(1f, fill = false).aspectRatio(1f))
Row(
horizontalArrangement = Arrangement.spacedBy(10.dp),
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.padding(vertical = 10.dp)
) {
SimpleButton(
stringResource(R.string.share_link),
icon = Icons.Outlined.Share,
click = share
)
SimpleButton(
stringResource(R.string.delete_link),
icon = Icons.Outlined.Delete,
color = Color.Red,
click = deleteLink
)
}
}
}
}
}
@@ -24,6 +24,7 @@ import chat.simplex.app.R
import chat.simplex.app.model.*
import chat.simplex.app.ui.theme.*
import chat.simplex.app.views.chat.SimplexServers
import chat.simplex.app.views.chat.SwitchAddressButton
import chat.simplex.app.views.chatlist.openChat
import chat.simplex.app.views.helpers.*
import chat.simplex.app.views.usersettings.SettingsActionItem
@@ -81,6 +82,9 @@ fun GroupMemberInfoView(
}
}
}
},
switchMemberAddress = {
switchMemberAddress(chatModel, groupInfo, member)
}
)
}
@@ -113,6 +117,7 @@ fun GroupMemberInfoLayout(
openDirectChat: () -> Unit,
removeMember: () -> Unit,
onRoleSelected: (GroupMemberRole) -> Unit,
switchMemberAddress: () -> Unit,
) {
Column(
Modifier
@@ -136,14 +141,14 @@ fun GroupMemberInfoLayout(
SectionView(title = stringResource(R.string.member_info_section_title_member)) {
InfoRow(stringResource(R.string.info_row_group), groupInfo.displayName)
SectionDivider()
/*val roles = remember { member.canChangeRoleTo(groupInfo) }
val roles = remember { member.canChangeRoleTo(groupInfo) }
if (roles != null) {
SectionItemView {
RoleSelectionRow(roles, newRole, onRoleSelected)
}
} else {*/
} else {
InfoRow(stringResource(R.string.role_in_group), member.memberRole.text)
//}
}
val conn = member.activeConn
if (conn != null) {
SectionDivider()
@@ -154,12 +159,15 @@ fun GroupMemberInfoLayout(
}
}
SectionSpacer()
if (connStats != null) {
val rcvServers = connStats.rcvServers
val sndServers = connStats.sndServers
if ((rcvServers != null && rcvServers.isNotEmpty()) || (sndServers != null && sndServers.isNotEmpty())) {
SectionView(title = stringResource(R.string.conn_stats_section_title_servers)) {
SectionView(title = stringResource(R.string.conn_stats_section_title_servers)) {
if (developerTools) {
SwitchAddressButton(switchMemberAddress)
SectionDivider()
}
if (connStats != null) {
val rcvServers = connStats.rcvServers
val sndServers = connStats.sndServers
if ((rcvServers != null && rcvServers.isNotEmpty()) || (sndServers != null && sndServers.isNotEmpty())) {
if (rcvServers != null && rcvServers.isNotEmpty()) {
SimplexServers(stringResource(R.string.receiving_via), rcvServers)
if (sndServers != null && sndServers.isNotEmpty()) {
@@ -170,9 +178,9 @@ fun GroupMemberInfoLayout(
SimplexServers(stringResource(R.string.sending_via), sndServers)
}
}
SectionSpacer()
}
}
SectionSpacer()
if (member.canBeRemoved(groupInfo)) {
SectionView {
@@ -280,6 +288,10 @@ private fun updateMemberRoleDialog(
)
}
private fun switchMemberAddress(m: ChatModel, groupInfo: GroupInfo, member: GroupMember) = withApi {
m.controller.apiSwitchGroupMember(groupInfo.apiId, member.groupMemberId)
}
@Preview
@Composable
fun PreviewGroupMemberInfoLayout() {
@@ -292,7 +304,8 @@ fun PreviewGroupMemberInfoLayout() {
developerTools = false,
openDirectChat = {},
removeMember = {},
onRoleSelected = {}
onRoleSelected = {},
switchMemberAddress = {},
)
}
}
@@ -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() }
@@ -17,7 +17,7 @@ import chat.simplex.app.ui.theme.HighOrLowlight
import chat.simplex.app.ui.theme.SimpleXTheme
@Composable
fun CIGroupEventView(ci: ChatItem) {
fun CIEventView(ci: ChatItem) {
fun withGroupEventStyle(builder: AnnotatedString.Builder, text: String) {
return builder.withStyle(SpanStyle(fontSize = 12.sp, fontWeight = FontWeight.Light, color = HighOrLowlight)) { append(text) }
}
@@ -50,9 +50,9 @@ fun CIGroupEventView(ci: ChatItem) {
name = "Dark Mode"
)
@Composable
fun CIGroupEventViewPreview() {
fun CIEventViewPreview() {
SimpleXTheme {
CIGroupEventView(
CIEventView(
ChatItem.getGroupEventSample()
)
}
@@ -1,3 +1,5 @@
package chat.simplex.app.views.chat.item
import android.graphics.Bitmap
import android.os.Build.VERSION.SDK_INT
import androidx.compose.foundation.Image
@@ -9,8 +11,7 @@ import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Check
import androidx.compose.material.icons.outlined.ArrowDownward
import androidx.compose.material.icons.outlined.MoreHoriz
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
@@ -18,16 +19,16 @@ import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.graphics.painter.BitmapPainter
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.layout.layoutId
import androidx.compose.ui.platform.*
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.core.content.FileProvider
import chat.simplex.app.BuildConfig
import chat.simplex.app.*
import chat.simplex.app.R
import chat.simplex.app.model.CIFile
import chat.simplex.app.model.CIFileStatus
import chat.simplex.app.views.chat.item.ImageFullScreenView
import chat.simplex.app.views.chat.item.ImageGalleryProvider
import chat.simplex.app.views.helpers.*
import coil.ImageLoader
import coil.compose.rememberAsyncImagePainter
@@ -93,6 +94,12 @@ fun CIImageView(
}
}
@Composable
fun imageViewFullWidth(): Dp {
val approximatePadding = 100.dp
return with(LocalDensity.current) { minOf(1000.dp, LocalView.current.width.toDp() - approximatePadding) }
}
@Composable
fun imageView(imageBitmap: Bitmap, onClick: () -> Unit) {
Image(
@@ -101,7 +108,7 @@ fun CIImageView(
// .width(1000.dp) is a hack for image to increase IntrinsicSize of FramedItemView
// if text is short and take all available width if text is long
modifier = Modifier
.width(1000.dp)
.width(if (imageBitmap.width * 0.97 <= imageBitmap.height) imageViewFullWidth() * 0.75f else 1000.dp)
.combinedClickable(
onLongClick = { showMenu.value = true },
onClick = onClick
@@ -118,7 +125,7 @@ fun CIImageView(
// .width(1000.dp) is a hack for image to increase IntrinsicSize of FramedItemView
// if text is short and take all available width if text is long
modifier = Modifier
.width(1000.dp)
.width(if (painter.intrinsicSize.width * 0.97 <= painter.intrinsicSize.height) imageViewFullWidth() * 0.75f else 1000.dp)
.combinedClickable(
onLongClick = { showMenu.value = true },
onClick = onClick
@@ -134,27 +141,28 @@ fun CIImageView(
return false
}
Box(contentAlignment = Alignment.TopEnd) {
fun imageAndFilePath(file: CIFile?): Pair<Bitmap?, String?> {
val imageBitmap: Bitmap? = getLoadedImage(SimplexApp.context, file)
val filePath = getLoadedFilePath(SimplexApp.context, file)
return imageBitmap to filePath
}
Box(
Modifier.layoutId(CHAT_IMAGE_LAYOUT_ID),
contentAlignment = Alignment.TopEnd
) {
val context = LocalContext.current
val imageBitmap: Bitmap? = getLoadedImage(context, file)
val filePath = getLoadedFilePath(context, file)
val (imageBitmap, filePath) = remember(file) { imageAndFilePath(file) }
if (imageBitmap != null && filePath != null) {
val uri = FileProvider.getUriForFile(context, "${BuildConfig.APPLICATION_ID}.provider", File(filePath))
val imageLoader = ImageLoader.Builder(context)
.components {
if (SDK_INT >= 28) {
add(ImageDecoderDecoder.Factory())
} else {
add(GifDecoder.Factory())
}
}
.build()
val uri = remember(filePath) { FileProvider.getUriForFile(context, "${BuildConfig.APPLICATION_ID}.provider", File(filePath)) }
val imagePainter = rememberAsyncImagePainter(
ImageRequest.Builder(context).data(data = uri).size(coil.size.Size.ORIGINAL).build(),
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)
@@ -190,3 +198,13 @@ fun CIImageView(
loadingIndicator()
}
}
private val imageLoader = ImageLoader.Builder(SimplexApp.context)
.components {
if (SDK_INT >= 28) {
add(ImageDecoderDecoder.Factory())
} else {
add(GifDecoder.Factory())
}
}
.build()
@@ -18,6 +18,7 @@ import androidx.compose.ui.platform.*
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import chat.simplex.app.*
import chat.simplex.app.R
import chat.simplex.app.model.*
import chat.simplex.app.ui.theme.SimpleXTheme
@@ -55,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)) {
@@ -81,7 +93,11 @@ fun ChatItemView(
showMenu.value = false
})
ItemAction(stringResource(R.string.share_verb), Icons.Outlined.Share, onClick = {
shareText(cxt, cItem.content.text)
val filePath = getLoadedFilePath(SimplexApp.context, cItem.file)
when {
filePath != null -> shareFile(cxt, cItem.text, filePath)
else -> shareText(cxt, cItem.content.text)
}
showMenu.value = false
})
ItemAction(stringResource(R.string.copy_verb), Icons.Outlined.ContentCopy, onClick = {
@@ -152,8 +168,10 @@ fun ChatItemView(
is CIContent.RcvIntegrityError -> IntegrityErrorItemView(cItem, showMember = showMember)
is CIContent.RcvGroupInvitation -> CIGroupInvitationView(cItem, c.groupInvitation, c.memberRole, joinGroup = joinGroup, chatIncognito = cInfo.incognito)
is CIContent.SndGroupInvitation -> CIGroupInvitationView(cItem, c.groupInvitation, c.memberRole, joinGroup = joinGroup, chatIncognito = cInfo.incognito)
is CIContent.RcvGroupEventContent -> CIGroupEventView(cItem)
is CIContent.SndGroupEventContent -> CIGroupEventView(cItem)
is CIContent.RcvGroupEventContent -> CIEventView(cItem)
is CIContent.SndGroupEventContent -> CIEventView(cItem)
is CIContent.RcvConnEventContent -> CIEventView(cItem)
is CIContent.SndConnEventContent -> CIEventView(cItem)
}
}
}
@@ -203,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() {
@@ -1,6 +1,5 @@
package chat.simplex.app.views.chat.item
import CIImageView
import androidx.compose.foundation.*
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
@@ -10,16 +9,17 @@ import androidx.compose.material.icons.filled.InsertDriveFile
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.clipToBounds
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.layout.*
import androidx.compose.ui.platform.UriHandler
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.tooling.preview.*
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.ui.unit.*
import androidx.compose.ui.util.fastMap
import chat.simplex.app.R
import chat.simplex.app.model.*
import chat.simplex.app.ui.theme.*
@@ -69,7 +69,10 @@ fun FramedItemView(
Modifier
.background(if (sent) SentQuoteColorLight else ReceivedQuoteColorLight)
.fillMaxWidth()
.clickable { scrollToItem(qi.itemId?: return@clickable) }
.combinedClickable(
onLongClick = { showMenu.value = true },
onClick = { scrollToItem(qi.itemId?: return@combinedClickable) }
)
) {
when (qi.content) {
is MsgContent.MCImage -> {
@@ -102,46 +105,58 @@ fun FramedItemView(
}
}
Surface(
shape = RoundedCornerShape(18.dp),
color = if (sent) SentColorLight else ReceivedColorLight
) {
@Composable
fun ciFileView(ci: ChatItem, text: String) {
CIFileView(ci.file, ci.meta.itemEdited, receiveFile)
if (text != "") {
CIMarkdownText(ci, showMember, uriHandler)
}
}
val transparentBackground = ci.content.msgContent is MsgContent.MCImage && ci.content.text.isEmpty() && ci.quotedItem == null
Box(Modifier
.clip(RoundedCornerShape(18.dp))
.background(
when {
transparentBackground -> Color.Transparent
sent -> SentColorLight
else -> ReceivedColorLight
}
)) {
var metaColor = HighOrLowlight
Box(contentAlignment = Alignment.BottomEnd) {
Column(Modifier.width(IntrinsicSize.Max)) {
val qi = ci.quotedItem
if (qi != null) {
ciQuoteView(qi)
}
if (ci.file == null && ci.formattedText == null && isShortEmoji(ci.content.text)) {
Box(Modifier.padding(vertical = 6.dp, horizontal = 12.dp)) {
Column(
Modifier
.padding(bottom = 2.dp)
.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally
) {
EmojiText(ci.content.text)
Text("")
PriorityLayout(Modifier, CHAT_IMAGE_LAYOUT_ID) {
ci.quotedItem?.let { ciQuoteView(it) }
if (ci.file == null && ci.formattedText == null && isShortEmoji(ci.content.text)) {
Box(Modifier.padding(vertical = 6.dp, horizontal = 12.dp)) {
Column(
Modifier
.padding(bottom = 2.dp)
.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally
) {
EmojiText(ci.content.text)
Text("")
}
}
}
} else {
Column(Modifier.fillMaxWidth()) {
} else {
when (val mc = ci.content.msgContent) {
is MsgContent.MCImage -> {
CIImageView(image = mc.image, file = ci.file, imageProvider ?: return@Box, showMenu, receiveFile)
CIImageView(image = mc.image, file = ci.file, imageProvider ?: return@PriorityLayout, showMenu, receiveFile)
if (mc.text == "") {
metaColor = Color.White
} else {
CIMarkdownText(ci, showMember, uriHandler)
}
}
is MsgContent.MCFile -> {
CIFileView(ci.file, ci.meta.itemEdited, receiveFile)
if (mc.text != "") {
CIMarkdownText(ci, showMember, uriHandler)
is MsgContent.MCFile -> ciFileView(ci, mc.text)
is MsgContent.MCUnknown ->
if (ci.file == null) {
CIMarkdownText(ci, showMember, uriHandler, onLinkLongClick)
} else {
ciFileView(ci, mc.text)
}
}
is MsgContent.MCLink -> {
ChatItemLinkView(mc.preview)
CIMarkdownText(ci, showMember, uriHandler, onLinkLongClick)
@@ -174,6 +189,37 @@ fun CIMarkdownText(
}
}
const val CHAT_IMAGE_LAYOUT_ID = "chatImage"
@Composable
fun PriorityLayout(
modifier: Modifier = Modifier,
priorityLayoutId: String,
content: @Composable () -> Unit
) {
Layout(
content = content,
modifier = modifier
) { measureable, constraints ->
// Find important element which should tell what max width other elements can use
// Expecting only one such element. Can be less than one but not more
val imagePlaceable = measureable.firstOrNull { it.layoutId == priorityLayoutId }?.measure(constraints)
val placeables: List<Placeable> = measureable.fastMap {
if (it.layoutId == priorityLayoutId)
imagePlaceable!!
else
it.measure(constraints.copy(maxWidth = imagePlaceable?.width ?: constraints.maxWidth)) }
// Limit width for every other element to width of important element and height for a sum of all elements
layout(imagePlaceable?.measuredWidth ?: placeables.maxOf { it.width }, placeables.sumOf { it.height }) {
var y = 0
placeables.forEach {
it.place(0, y)
y += it.measuredHeight
}
}
}
}
class EditedProvider: PreviewParameterProvider<Boolean> {
override val values = listOf(false, true).asSequence()
}
@@ -13,7 +13,9 @@ import androidx.compose.ui.graphics.*
import androidx.compose.ui.graphics.painter.BitmapPainter
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.*
@@ -25,6 +27,7 @@ import coil.request.ImageRequest
import coil.size.Size
import com.google.accompanist.pager.*
import kotlinx.coroutines.launch
import kotlin.math.absoluteValue
interface ImageGalleryProvider {
val initialIndex: Int
@@ -88,6 +91,8 @@ fun ImageFullScreenView(imageProvider: () -> ImageGalleryProvider, close: () ->
var scale by remember { mutableStateOf(1f) }
var translationX by remember { mutableStateOf(0f) }
var translationY by remember { mutableStateOf(0f) }
var viewWidth by remember { mutableStateOf(0) }
var allowTranslate by remember { mutableStateOf(true) }
LaunchedEffect(settledCurrentPage) {
scale = 1f
translationX = 0f
@@ -113,6 +118,9 @@ fun ImageFullScreenView(imageProvider: () -> ImageGalleryProvider, close: () ->
contentDescription = stringResource(R.string.image_descr),
contentScale = ContentScale.Fit,
modifier = Modifier
.onGloballyPositioned {
viewWidth = it.size.width
}
.graphicsLayer(
scaleX = scale,
scaleY = scale,
@@ -121,12 +129,14 @@ fun ImageFullScreenView(imageProvider: () -> ImageGalleryProvider, close: () ->
)
.pointerInput(Unit) {
detectTransformGestures(
{ allowTranslate },
onGesture = { _, pan, gestureZoom, _ ->
scale = (scale * gestureZoom).coerceIn(1f, 20f)
if (scale > 1) {
allowTranslate = viewWidth * (scale - 1f) - ((translationX + pan.x * scale).absoluteValue * 2) > 0
if (scale > 1 && allowTranslate) {
translationX += pan.x * scale
translationY += pan.y * scale
} else {
} else if (allowTranslate) {
translationX = 0f
translationY = 0f
}
@@ -60,8 +60,7 @@ fun MarkdownText (
else -> " "
}
CompositionLocalProvider(
// When we draw `sender` is has issues on LTR languages set globally with RTL text language
LocalLayoutDirection provides if (textLayoutDirection != LocalLayoutDirection.current && sender == null)
LocalLayoutDirection provides if (textLayoutDirection != LocalLayoutDirection.current)
if (LocalLayoutDirection.current == LayoutDirection.Ltr) LayoutDirection.Rtl else LayoutDirection.Ltr
else
LocalLayoutDirection.current
@@ -5,7 +5,7 @@ import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.*
import androidx.compose.material.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.TheaterComedy
import androidx.compose.material.icons.filled.*
import androidx.compose.material.icons.outlined.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
@@ -29,12 +29,13 @@ import kotlinx.datetime.Clock
@Composable
fun ChatListNavLinkView(chat: Chat, chatModel: ChatModel) {
val showMenu = remember { mutableStateOf(false) }
var showMarkRead by remember { mutableStateOf(false) }
val showMarkRead = remember(chat.chatStats.unreadCount, chat.chatStats.unreadChat) {
chat.chatStats.unreadCount > 0 || chat.chatStats.unreadChat
}
val stopped = chatModel.chatRunning.value == false
LaunchedEffect(chat.id, chat.chatStats.unreadCount > 0) {
LaunchedEffect(chat.id) {
showMenu.value = false
delay(500L)
showMarkRead = chat.chatStats.unreadCount > 0
}
when (chat.chatInfo) {
is ChatInfo.Direct ->
@@ -123,6 +124,8 @@ suspend fun setGroupMembers(groupInfo: GroupInfo, chatModel: ChatModel) {
fun ContactMenuItems(chat: Chat, chatModel: ChatModel, showMenu: MutableState<Boolean>, showMarkRead: Boolean) {
if (showMarkRead) {
MarkReadChatAction(chat, chatModel, showMenu)
} else {
MarkUnreadChatAction(chat, chatModel, showMenu)
}
ToggleNotificationsChatAction(chat, chatModel, chat.chatInfo.ntfsEnabled, showMenu)
ClearChatAction(chat, chatModel, showMenu)
@@ -141,6 +144,8 @@ fun GroupMenuItems(chat: Chat, groupInfo: GroupInfo, chatModel: ChatModel, showM
else -> {
if (showMarkRead) {
MarkReadChatAction(chat, chatModel, showMenu)
} else {
MarkUnreadChatAction(chat, chatModel, showMenu)
}
ToggleNotificationsChatAction(chat, chatModel, chat.chatInfo.ntfsEnabled, showMenu)
ClearChatAction(chat, chatModel, showMenu)
@@ -167,6 +172,30 @@ fun MarkReadChatAction(chat: Chat, chatModel: ChatModel, showMenu: MutableState<
)
}
@Composable
fun MarkUnreadChatAction(chat: Chat, chatModel: ChatModel, showMenu: MutableState<Boolean>) {
DropdownMenuItem({
markChatUnread(chat, chatModel)
showMenu.value = false
}) {
Row {
Text(
stringResource(R.string.mark_unread),
modifier = Modifier
.fillMaxWidth()
.weight(1F)
.padding(end = 15.dp),
color = MaterialTheme.colors.onBackground
)
Icon(
Icons.Outlined.MarkChatUnread,
stringResource(R.string.mark_unread),
tint = MaterialTheme.colors.onBackground
)
}
}
}
@Composable
fun ToggleNotificationsChatAction(chat: Chat, chatModel: ChatModel, ntfsEnabled: Boolean, showMenu: MutableState<Boolean>) {
ItemAction(
@@ -290,18 +319,45 @@ fun ContactConnectionMenuItems(chatInfo: ChatInfo.ContactConnection, chatModel:
)
}
fun markChatRead(chat: Chat, chatModel: ChatModel) {
// Just to be sure
if (chat.chatStats.unreadCount == 0) return
val minUnreadItemId = chat.chatStats.minUnreadItemId
chatModel.markChatItemsRead(chat.chatInfo)
fun markChatRead(c: Chat, chatModel: ChatModel) {
var chat = c
withApi {
chatModel.controller.apiChatRead(
if (chat.chatStats.unreadCount > 0) {
val minUnreadItemId = chat.chatStats.minUnreadItemId
chatModel.markChatItemsRead(chat.chatInfo)
chatModel.controller.apiChatRead(
chat.chatInfo.chatType,
chat.chatInfo.apiId,
CC.ItemRange(minUnreadItemId, chat.chatItems.last().id)
)
chat = chatModel.getChat(chat.id) ?: return@withApi
}
if (chat.chatStats.unreadChat) {
val success = chatModel.controller.apiChatUnread(
chat.chatInfo.chatType,
chat.chatInfo.apiId,
false
)
if (success) {
chatModel.replaceChat(chat.id, chat.copy(chatStats = chat.chatStats.copy(unreadChat = false)))
}
}
}
}
fun markChatUnread(chat: Chat, chatModel: ChatModel) {
// Just to be sure
if (chat.chatStats.unreadChat) return
withApi {
val success = chatModel.controller.apiChatUnread(
chat.chatInfo.chatType,
chat.chatInfo.apiId,
CC.ItemRange(minUnreadItemId, chat.chatItems.last().id)
true
)
if (success) {
chatModel.replaceChat(chat.id, chat.copy(chatStats = chat.chatStats.copy(unreadChat = true)))
}
}
}
@@ -478,7 +534,7 @@ fun ChatListNavLinkLayout(
showMenu: MutableState<Boolean>,
stopped: Boolean
) {
var modifier = Modifier.fillMaxWidth().heightIn(min = 88.dp)
var modifier = Modifier.fillMaxWidth()
if (!stopped) modifier = modifier.combinedClickable(onClick = click, onLongClick = { showMenu.value = true })
Surface(modifier) {
Row(
@@ -3,8 +3,7 @@ package chat.simplex.app.views.chatlist
import androidx.activity.compose.BackHandler
import androidx.compose.foundation.*
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.*
import androidx.compose.material.icons.Icons
@@ -203,14 +202,21 @@ private fun ChatListToolbar(chatModel: ChatModel, drawerState: DrawerState, stop
Divider(Modifier.padding(top = AppBarHeight))
}
private var lazyListState = 0 to 0
@Composable
private fun ChatList(chatModel: ChatModel, search: String) {
val filter: (Chat) -> Boolean = { chat: Chat ->
chat.chatInfo.chatViewName.lowercase().contains(search.lowercase())
}
val listState = rememberLazyListState(lazyListState.first, lazyListState.second)
DisposableEffect(Unit) {
onDispose { lazyListState = listState.firstVisibleItemIndex to listState.firstVisibleItemScrollOffset }
}
val chats by remember(search) { derivedStateOf { if (search.isEmpty()) chatModel.chats else chatModel.chats.filter(filter) } }
LazyColumn(
modifier = Modifier.fillMaxWidth()
modifier = Modifier.fillMaxWidth(),
listState
) {
items(chats) { chat ->
ChatListNavLinkView(chat, chatModel)
@@ -13,6 +13,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
@@ -84,7 +85,8 @@ fun ChatPreviewView(chat: Chat, chatModelIncognito: Boolean, currentUserProfileD
MarkdownText(
ci.text,
ci.formattedText,
sender = null,
sender = if (cInfo is ChatInfo.Group && !ci.chatDir.sent) ci.memberDisplayName else null,
senderBold = true,
metaText = null,
maxLines = 2,
overflow = TextOverflow.Ellipsis,
@@ -121,7 +123,10 @@ fun ChatPreviewView(chat: Chat, chatModelIncognito: Boolean, currentUserProfileD
.weight(1F)
) {
chatPreviewTitle()
chatPreviewText(chatModelIncognito)
val height = with(LocalDensity.current) { 46.sp.toDp() }
Row(Modifier.heightIn(min = height)) {
chatPreviewText(chatModelIncognito)
}
}
val ts = chat.chatItems.lastOrNull()?.timestampText ?: getTimestampText(chat.chatInfo.updatedAt)
@@ -136,13 +141,13 @@ fun ChatPreviewView(chat: Chat, chatModelIncognito: Boolean, currentUserProfileD
)
val n = chat.chatStats.unreadCount
val showNtfsIcon = !chat.chatInfo.ntfsEnabled && (chat.chatInfo is ChatInfo.Direct || chat.chatInfo is ChatInfo.Group)
if (n > 0) {
if (n > 0 || chat.chatStats.unreadChat) {
Box(
Modifier.padding(top = 24.dp),
contentAlignment = Alignment.Center
) {
Text(
unreadCountStr(n),
if (n > 0) unreadCountStr(n) else "",
color = MaterialTheme.colors.onPrimary,
fontSize = 11.sp,
modifier = Modifier
@@ -8,9 +8,11 @@ import androidx.compose.material.icons.outlined.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import chat.simplex.app.model.*
import chat.simplex.app.ui.theme.*
import chat.simplex.app.views.helpers.ProfileImage
@@ -34,7 +36,8 @@ fun ContactConnectionView(contactConnection: PendingContactConnection) {
fontWeight = FontWeight.Bold,
color = HighOrLowlight
)
Text(contactConnection.description, maxLines = 2, color = if (isInDarkTheme()) MessagePreviewDark else MessagePreviewLight)
val height = with(LocalDensity.current) { 46.sp.toDp() }
Text(contactConnection.description, Modifier.heightIn(min = height), maxLines = 2, color = if (isInDarkTheme()) MessagePreviewDark else MessagePreviewLight)
}
val ts = getTimestampText(contactConnection.updatedAt)
Column(
@@ -5,10 +5,12 @@ import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import chat.simplex.app.R
import chat.simplex.app.model.*
import chat.simplex.app.ui.theme.*
@@ -31,7 +33,8 @@ fun ContactRequestView(chatModelIncognito: Boolean, contactRequest: ChatInfo.Con
fontWeight = FontWeight.Bold,
color = if (chatModelIncognito) Indigo else MaterialTheme.colors.primary
)
Text(stringResource(R.string.contact_wants_to_connect_with_you), maxLines = 2, color = if (isInDarkTheme()) MessagePreviewDark else MessagePreviewLight)
val height = with(LocalDensity.current) { 46.sp.toDp() }
Text(stringResource(R.string.contact_wants_to_connect_with_you), Modifier.heightIn(min = height), maxLines = 2, color = if (isInDarkTheme()) MessagePreviewDark else MessagePreviewLight)
}
val ts = getTimestampText(contactRequest.contactRequest.updatedAt)
Column(
@@ -93,7 +93,7 @@ private fun ShareListToolbar(chatModel: ChatModel, stopped: Boolean, onSearchVal
Text(
when (chatModel.sharedContent.value) {
is SharedContent.Text -> stringResource(R.string.share_message)
is SharedContent.Image -> stringResource(R.string.share_image)
is SharedContent.Images -> stringResource(R.string.share_image)
is SharedContent.File -> stringResource(R.string.share_file)
else -> stringResource(R.string.share_message)
},
@@ -94,8 +94,7 @@ private fun rememberSaveArchiveLauncher(cxt: Context, chatArchivePath: String):
val contentResolver = cxt.contentResolver
contentResolver.openOutputStream(destination)?.let { stream ->
val outputStream = BufferedOutputStream(stream)
val file = File(chatArchivePath)
outputStream.write(file.readBytes())
File(chatArchivePath).inputStream().use { it.copyTo(outputStream) }
outputStream.close()
Toast.makeText(cxt, generalGetString(R.string.file_saved), Toast.LENGTH_SHORT).show()
}
@@ -58,13 +58,13 @@ fun DatabaseView(
val chatLastStart = remember { mutableStateOf(prefs.chatLastStart.get()) }
val chatArchiveFile = remember { mutableStateOf<String?>(null) }
val saveArchiveLauncher = rememberSaveArchiveLauncher(cxt = context, chatArchiveFile)
val appFilesCountAndSize = remember { mutableStateOf(directoryFileCountAndSize(getAppFilesDirectory(context))) }
val importArchiveLauncher = rememberGetContentLauncher { uri: Uri? ->
if (uri != null) {
importArchiveAlert(m, context, uri, progressIndicator)
importArchiveAlert(m, context, uri, appFilesCountAndSize, progressIndicator)
}
}
val chatDbDeleted = remember { m.chatDbDeleted }
val appFilesCountAndSize = remember { mutableStateOf(directoryFileCountAndSize(getAppFilesDirectory(context))) }
LaunchedEffect(m.chatRunning) {
runChat.value = m.chatRunning.value ?: true
}
@@ -489,8 +489,7 @@ private fun rememberSaveArchiveLauncher(cxt: Context, chatArchiveFile: MutableSt
val contentResolver = cxt.contentResolver
contentResolver.openOutputStream(destination)?.let { stream ->
val outputStream = BufferedOutputStream(stream)
val file = File(filePath)
outputStream.write(file.readBytes())
File(filePath).inputStream().use { it.copyTo(outputStream) }
outputStream.close()
Toast.makeText(cxt, generalGetString(R.string.file_saved), Toast.LENGTH_SHORT).show()
}
@@ -507,16 +506,28 @@ private fun rememberSaveArchiveLauncher(cxt: Context, chatArchiveFile: MutableSt
}
)
private fun importArchiveAlert(m: ChatModel, context: Context, importedArchiveUri: Uri, progressIndicator: MutableState<Boolean>) {
private fun importArchiveAlert(
m: ChatModel,
context: Context,
importedArchiveUri: Uri,
appFilesCountAndSize: MutableState<Pair<Int, Long>>,
progressIndicator: MutableState<Boolean>
) {
AlertManager.shared.showAlertDialog(
title = generalGetString(R.string.import_database_question),
text = generalGetString(R.string.your_current_chat_database_will_be_deleted_and_replaced_with_the_imported_one),
confirmText = generalGetString(R.string.import_database_confirmation),
onConfirm = { importArchive(m, context, importedArchiveUri, progressIndicator) }
onConfirm = { importArchive(m, context, importedArchiveUri, appFilesCountAndSize, progressIndicator) }
)
}
private fun importArchive(m: ChatModel, context: Context, importedArchiveUri: Uri, progressIndicator: MutableState<Boolean>) {
private fun importArchive(
m: ChatModel,
context: Context,
importedArchiveUri: Uri,
appFilesCountAndSize: MutableState<Pair<Int, Long>>,
progressIndicator: MutableState<Boolean>
) {
progressIndicator.value = true
val archivePath = saveArchiveFromUri(context, importedArchiveUri)
if (archivePath != null) {
@@ -527,6 +538,7 @@ private fun importArchive(m: ChatModel, context: Context, importedArchiveUri: Ur
val config = ArchiveConfig(archivePath, parentTempDirectory = context.cacheDir.toString())
m.controller.apiImportArchive(config)
DatabaseUtils.removeDatabaseKey()
appFilesCountAndSize.value = directoryFileCountAndSize(getAppFilesDirectory(context))
operationEnded(m, progressIndicator) {
AlertManager.shared.showAlertMsg(generalGetString(R.string.chat_database_imported), generalGetString(R.string.restart_the_app_to_use_imported_chat_database))
}
@@ -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 {
@@ -7,8 +7,8 @@ import kotlinx.coroutines.flow.MutableStateFlow
sealed class SharedContent {
data class Text(val text: String): SharedContent()
data class Image(val uri: Uri): SharedContent()
data class File(val uri: Uri): SharedContent()
data class Images(val text: String, val uris: List<Uri>): SharedContent()
data class File(val text: String, val uri: Uri): SharedContent()
}
enum class NewChatSheetState {
@@ -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()
}
@@ -214,13 +214,14 @@ fun interactionSourceWithDetection(onClick: () -> Unit, onLongClick: () -> Unit)
}
suspend fun PointerInputScope.detectTransformGestures(
allowIntercept: () -> Boolean,
panZoomLock: Boolean = false,
onGesture: (centroid: Offset, pan: Offset, zoom: Float, rotation: Float) -> Unit
) {
var zoom = 1f
forEachGesture {
awaitPointerEventScope {
var rotation = 0f
var zoom = 1f
var pan = Offset.Zero
var pastTouchSlop = false
val touchSlop = viewConfiguration.touchSlop
@@ -264,7 +265,7 @@ suspend fun PointerInputScope.detectTransformGestures(
onGesture(centroid, panChange, zoomChange, effectiveRotation)
}
event.changes.fastForEach {
if (it.positionChanged() && zoomChange != 1f) {
if (it.positionChanged() && zoom != 1f && allowIntercept()) {
it.consume()
}
}
@@ -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
@@ -65,26 +70,28 @@ fun resizeImageToStrSize(image: Bitmap, maxDataSize: Long): String {
}
private fun compressImageStr(bitmap: Bitmap): String {
return "data:image/jpg;base64," + Base64.encodeToString(compressImageData(bitmap).toByteArray(), Base64.NO_WRAP)
val usePng = bitmap.hasAlpha()
val ext = if (usePng) "png" else "jpg"
return "data:image/$ext;base64," + Base64.encodeToString(compressImageData(bitmap, usePng).toByteArray(), Base64.NO_WRAP)
}
fun resizeImageToDataSize(image: Bitmap, maxDataSize: Long): ByteArrayOutputStream {
fun resizeImageToDataSize(image: Bitmap, usePng: Boolean, maxDataSize: Long): ByteArrayOutputStream {
var img = image
var stream = compressImageData(img)
var stream = compressImageData(img, usePng)
while (stream.size() > maxDataSize) {
val ratio = sqrt(stream.size().toDouble() / maxDataSize.toDouble())
val clippedRatio = min(ratio, 2.0)
val width = (img.width.toDouble() / clippedRatio).toInt()
val height = img.height * width / img.width
img = Bitmap.createScaledBitmap(img, width, height, true)
stream = compressImageData(img)
stream = compressImageData(img, usePng)
}
return stream
}
private fun compressImageData(bitmap: Bitmap): ByteArrayOutputStream {
private fun compressImageData(bitmap: Bitmap, usePng: Boolean): ByteArrayOutputStream {
val stream = ByteArrayOutputStream()
bitmap.compress(Bitmap.CompressFormat.JPEG, 85, stream)
bitmap.compress(if (!usePng) Bitmap.CompressFormat.JPEG else Bitmap.CompressFormat.PNG, 85, stream)
return stream
}
@@ -104,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)
@@ -121,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 {
@@ -146,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> =
@@ -163,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)
}
}
@@ -217,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()
}
}
@@ -26,29 +26,44 @@ import chat.simplex.app.views.chat.item.SentColorLight
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.jsoup.Jsoup
import java.net.URL
private const val OG_SELECT_QUERY = "meta[property^=og:]"
private const val ICON_SELECT_QUERY = "link[rel^=icon],link[rel^=apple-touch-icon],link[rel^=shortcut icon]"
private val IMAGE_SUFFIXES = listOf(".jpg", ".png", ".ico", ".webp", ".gif")
suspend fun getLinkPreview(url: String): LinkPreview? {
return withContext(Dispatchers.IO) {
try {
val response = Jsoup.connect(url)
.ignoreContentType(true)
.timeout(10000)
.followRedirects(true)
.execute()
val doc = response.parse()
val ogTags = doc.select(OG_SELECT_QUERY)
val imageUri = ogTags.firstOrNull { it.attr("property") == "og:image" }?.attr("content")
val title: String?
val u = kotlin.runCatching { URL(url) }.getOrNull() ?: return@withContext null
var imageUri = when {
IMAGE_SUFFIXES.any { u.path.lowercase().endsWith(it) } -> {
title = u.path.substringAfterLast("/")
url
}
else -> {
val response = Jsoup.connect(url)
.ignoreContentType(true)
.timeout(10000)
.followRedirects(true)
.execute()
val doc = response.parse()
val ogTags = doc.select(OG_SELECT_QUERY)
title = ogTags.firstOrNull { it.attr("property") == "og:title" }?.attr("content") ?: doc.title()
ogTags.firstOrNull { it.attr("property") == "og:image" }?.attr("content")
?: doc.select(ICON_SELECT_QUERY).firstOrNull { it.attr("rel").contains("icon") }?.attr("href")
}
}
if (imageUri != null) {
imageUri = normalizeImageUri(u, imageUri)
try {
val stream = java.net.URL(imageUri).openStream()
val stream = URL(imageUri).openStream()
val image = resizeImageToStrSize(BitmapFactory.decodeStream(stream), maxDataSize = 14000)
// TODO add once supported in iOS
// val description = ogTags.firstOrNull {
// it.attr("property") == "og:description"
// }?.attr("content") ?: ""
val title = ogTags.firstOrNull { it.attr("property") == "og:title" }?.attr("content")
if (title != null) {
return@withContext LinkPreview(url, title, description = "", image)
}
@@ -63,8 +78,6 @@ suspend fun getLinkPreview(url: String): LinkPreview? {
}
}
@Composable
fun ComposeLinkView(linkPreview: LinkPreview?, cancelPreview: () -> Unit) {
Row(
@@ -127,6 +140,37 @@ fun ChatItemLinkView(linkPreview: LinkPreview) {
}
}
private fun normalizeImageUri(u: URL, imageUri: String) = when {
!imageUri.lowercase().startsWith("http") -> {
"${u.protocol}://${u.host}" +
if (imageUri.startsWith("/"))
imageUri
else
// When an icon is used as an image with relative href path like: site=site.com, <link rel="icon" href="icon.png">
if (u.path.endsWith("/")) u.path + imageUri
else u.path.substringBeforeLast("/") + "/$imageUri"
}
else -> imageUri
}
/*fun normalizeImageUriTest() {
val expect = mapOf<Pair<URL, String>, String>(
URL("https://example.com") to "icon.png" to "https://example.com/icon.png",
URL("https://example.com/") to "icon.png" to "https://example.com/icon.png",
URL("https://example.com/") to "/icon.png" to "https://example.com/icon.png",
URL("https://example.com") to "assets/images/favicon.png" to "https://example.com/assets/images/favicon.png",
URL("https://example.com/") to "assets/images/favicon.png" to "https://example.com/assets/images/favicon.png",
URL("https://example.com/dir") to "/favicon.png" to "https://example.com/favicon.png",
URL("https://example.com/dir/") to "favicon.png" to "https://example.com/dir/favicon.png",
URL("https://example.com/dir/") to "/favicon.png" to "https://example.com/favicon.png",
URL("https://example.com/dir/page") to "favicon.png" to "https://example.com/dir/favicon.png",
URL("https://example.com/abcde.gif") to "https://example.com/abcde.gif" to "https://example.com/abcde.gif",
URL("https://example.com/abcde.gif?a=b") to "https://example.com/abcde.gif?a=b" to "https://example.com/abcde.gif?a=b",
)
expect.forEach {
Log.d(TAG, "Image URI ${normalizeImageUri(it.key.first, it.key.second)} == ${normalizeImageUri(it.key.first, it.key.second) == it.value}")
}
}*/
@Preview(showBackground = true)
@Preview(
@@ -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)
@@ -60,6 +60,8 @@ class ModalManager {
modalCount.value = modalViews.size - toRemove.size
}
fun hasModalsOpen() = modalCount.value > 0
fun closeModal() {
if (modalViews.isNotEmpty()) {
if (modalViews.lastOrNull()?.first == false) modalViews.removeAt(modalViews.lastIndex)
@@ -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,
@@ -3,13 +3,15 @@ package chat.simplex.app.views.helpers
import android.content.*
import android.net.Uri
import android.provider.MediaStore
import android.webkit.MimeTypeMap
import android.widget.Toast
import androidx.activity.compose.ManagedActivityResultLauncher
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.runtime.Composable
import androidx.core.content.ContextCompat
import chat.simplex.app.R
import androidx.core.content.FileProvider
import chat.simplex.app.*
import chat.simplex.app.model.CIFile
import java.io.BufferedOutputStream
import java.io.File
@@ -24,6 +26,22 @@ fun shareText(cxt: Context, text: String) {
cxt.startActivity(shareIntent)
}
fun shareFile(cxt: Context, text: String, filePath: String) {
val uri = FileProvider.getUriForFile(SimplexApp.context, "${BuildConfig.APPLICATION_ID}.provider", File(filePath))
val ext = filePath.substringAfterLast(".")
val mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(ext) ?: return
val sendIntent: Intent = Intent().apply {
action = Intent.ACTION_SEND
/*if (text.isNotEmpty()) {
putExtra(Intent.EXTRA_TEXT, text)
}*/
putExtra(Intent.EXTRA_STREAM, uri)
type = mimeType
}
val shareIntent = Intent.createChooser(sendIntent, null)
cxt.startActivity(shareIntent)
}
fun copyText(cxt: Context, text: String) {
val clipboard = ContextCompat.getSystemService(cxt, ClipboardManager::class.java)
clipboard?.setPrimaryClip(ClipData.newPlainText("text", text))
@@ -40,8 +58,7 @@ fun rememberSaveFileLauncher(cxt: Context, ciFile: CIFile?): ManagedActivityResu
val contentResolver = cxt.contentResolver
contentResolver.openOutputStream(destination)?.let { stream ->
val outputStream = BufferedOutputStream(stream)
val file = File(filePath)
outputStream.write(file.readBytes())
File(filePath).inputStream().use { it.copyTo(outputStream) }
outputStream.close()
Toast.makeText(cxt, generalGetString(R.string.file_saved), Toast.LENGTH_SHORT).show()
}
@@ -52,30 +69,32 @@ fun rememberSaveFileLauncher(cxt: Context, ciFile: CIFile?): ManagedActivityResu
}
)
fun imageMimeType(fileName: String): String {
val lowercaseName = fileName.lowercase()
return when {
lowercaseName.endsWith(".png") -> "image/png"
lowercaseName.endsWith(".gif") -> "image/gif"
lowercaseName.endsWith(".webp") -> "image/webp"
lowercaseName.endsWith(".avif") -> "image/avif"
lowercaseName.endsWith(".svg") -> "image/svg+xml"
else -> "image/jpeg"
}
}
fun saveImage(cxt: Context, ciFile: CIFile?) {
val filePath = getLoadedFilePath(cxt, ciFile)
val fileName = ciFile?.fileName
if (filePath != null && fileName != null) {
val values = ContentValues()
val lowercaseName = fileName.lowercase()
val mimeType = when {
lowercaseName.endsWith(".png") -> "image/png"
lowercaseName.endsWith(".gif") -> "image/gif"
lowercaseName.endsWith(".webp") -> "image/webp"
lowercaseName.endsWith(".avif") -> "image/avif"
lowercaseName.endsWith(".svg") -> "image/svg+xml"
else -> "image/jpeg"
}
values.put(MediaStore.Images.Media.DATE_TAKEN, System.currentTimeMillis())
values.put(MediaStore.Images.Media.MIME_TYPE, mimeType)
values.put(MediaStore.Images.Media.MIME_TYPE, imageMimeType(fileName))
values.put(MediaStore.MediaColumns.DISPLAY_NAME, fileName)
values.put(MediaStore.MediaColumns.TITLE, fileName)
val uri = cxt.contentResolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values)
uri?.let {
cxt.contentResolver.openOutputStream(uri)?.let { stream ->
val outputStream = BufferedOutputStream(stream)
val file = File(filePath)
outputStream.write(file.readBytes())
File(filePath).inputStream().use { it.copyTo(outputStream) }
outputStream.close()
Toast.makeText(cxt, generalGetString(R.string.image_saved), Toast.LENGTH_SHORT).show()
}
@@ -28,6 +28,22 @@ fun SimpleButton(text: String, icon: ImageVector,
}
}
@Composable
fun SimpleButtonIconEnded(
text: String,
icon: ImageVector,
color: Color = MaterialTheme.colors.primary,
click: () -> Unit
) {
SimpleButtonFrame(click) {
Text(text, style = MaterialTheme.typography.caption, color = color)
Icon(
icon, text, tint = color,
modifier = Modifier.padding(start = 8.dp)
)
}
}
@Composable
fun SimpleButtonFrame(click: () -> Unit, disabled: Boolean = false, content: @Composable () -> Unit) {
Surface(shape = RoundedCornerShape(20.dp)) {
@@ -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,11 +311,18 @@ 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 dataResized = resizeImageToDataSize(image, maxDataSize = MAX_IMAGE_SIZE)
val ext = if (image.hasAlpha()) "png" else "jpg"
val dataResized = resizeImageToDataSize(image, ext == "png", maxDataSize = MAX_IMAGE_SIZE)
val timestamp = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.US).format(Date())
val fileToSave = uniqueCombine(context, "IMG_${timestamp}.jpg")
val fileToSave = uniqueCombine(context, "IMG_${timestamp}.$ext")
val file = File(getAppFilePath(context, fileToSave))
val output = FileOutputStream(file)
dataResized.writeTo(output)
@@ -24,16 +24,8 @@ import chat.simplex.app.ui.theme.*
import chat.simplex.app.views.helpers.*
@Composable
fun AddContactView(chatModel: ChatModel, connReqInvitation: String, connIncognito: Boolean) {
fun AddContactView(connReqInvitation: String, connIncognito: Boolean) {
val cxt = LocalContext.current
LaunchedEffect(connReqInvitation) {
if (connReqInvitation.isNotEmpty()) {
chatModel.connReqInv.value = connReqInvitation
}
}
DisposableEffect(Unit) {
onDispose { chatModel.connReqInv.value = null }
}
AddContactLayout(
connReq = connReqInvitation,
connIncognito = connIncognito,
@@ -83,7 +75,7 @@ fun AddContactLayout(connReq: String, connIncognito: Boolean, share: () -> Unit)
annotatedStringResource(R.string.if_you_cannot_meet_in_person_show_QR_in_video_call_or_via_another_channel),
lineHeight = 22.sp,
modifier = Modifier
.padding(bottom = if (screenHeight > 600.dp) 8.dp else 0.dp)
.padding(top = 16.dp, bottom = if (screenHeight > 600.dp) 16.dp else 0.dp)
)
Row(
Modifier.fillMaxWidth(),
@@ -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) {
@@ -30,31 +30,27 @@ fun ContactConnectionInfoView(
focusAlias: Boolean,
close: () -> Unit
) {
/** When [AddContactView] is open, we don't need to drop [chatModel.connReqInv]. It will be managed by [AddContactView] itself
* Otherwise it will be called here AFTER [AddContactView] is launched and will clear the value too soon */
val allowDispose = remember { mutableStateOf(true) }
LaunchedEffect(connReqInvitation) {
allowDispose.value = true
chatModel.connReqInv.value = connReqInvitation
}
/** When [AddContactView] is open, we don't need to drop [chatModel.connReqInv].
* Otherwise, it will be called here AFTER [AddContactView] is launched and will clear the value too soon.
* It will be dropped automatically when connection established or when user goes away from this screen.
**/
DisposableEffect(Unit) {
onDispose {
if (allowDispose.value) {
if (!ModalManager.shared.hasModalsOpen()) {
chatModel.connReqInv.value = null
}
}
}
ContactConnectionInfoLayout(
connReq = connReqInvitation,
contactConnection.localAlias,
contactConnection.initiated,
contactConnection.viaContactUri,
contactConnection.incognito,
contactConnection,
focusAlias,
deleteConnection = { deleteContactConnectionAlert(contactConnection, chatModel, close) },
onLocalAliasChanged = { setContactAlias(contactConnection, it, chatModel) },
showQr = {
allowDispose.value = false
ModalManager.shared.showModal {
Column(
Modifier
@@ -62,7 +58,7 @@ fun ContactConnectionInfoView(
.padding(horizontal = DEFAULT_PADDING),
verticalArrangement = Arrangement.SpaceBetween
) {
AddContactView(chatModel, connReqInvitation ?: return@showModal, contactConnection.incognito)
AddContactView(connReqInvitation ?: return@showModal, contactConnection.incognito)
}
}
}
@@ -72,10 +68,7 @@ fun ContactConnectionInfoView(
@Composable
private fun ContactConnectionInfoLayout(
connReq: String?,
localAlias: String,
connectionInitiated: Boolean,
connectionViaContactUri: Boolean,
connectionIncognito: Boolean,
contactConnection: PendingContactConnection,
focusAlias: Boolean,
deleteConnection: () -> Unit,
onLocalAliasChanged: (String) -> Unit,
@@ -87,23 +80,27 @@ private fun ContactConnectionInfoLayout(
) {
AppBarTitle(
stringResource(
if (connectionInitiated) R.string.you_invited_your_contact
if (contactConnection.initiated) R.string.you_invited_your_contact
else R.string.you_accepted_connection
)
)
Row(Modifier.padding(bottom = DEFAULT_PADDING)) {
LocalAliasEditor(localAlias, center = false, leadingIcon = true, focus = focusAlias, updateValue = onLocalAliasChanged)
if (contactConnection.groupLinkId == null) {
Row(Modifier.padding(bottom = DEFAULT_PADDING)) {
LocalAliasEditor(contactConnection.localAlias, center = false, leadingIcon = true, focus = focusAlias, updateValue = onLocalAliasChanged)
}
}
Text(
stringResource(
if (connectionViaContactUri) R.string.you_will_be_connected_when_your_connection_request_is_accepted
if (contactConnection.viaContactUri)
if (contactConnection.groupLinkId != null) R.string.you_will_be_connected_when_group_host_device_is_online
else R.string.you_will_be_connected_when_your_connection_request_is_accepted
else R.string.you_will_be_connected_when_your_contacts_device_is_online
),
Modifier.padding(start = DEFAULT_PADDING, end = DEFAULT_PADDING, bottom = DEFAULT_PADDING)
)
SectionView {
if (!connReq.isNullOrEmpty() && connectionInitiated) {
ShowQrButton(connectionIncognito, showQr)
if (!connReq.isNullOrEmpty() && contactConnection.initiated) {
ShowQrButton(contactConnection.incognito, showQr)
SectionDivider()
}
DeleteButton(deleteConnection)
@@ -149,11 +146,8 @@ private fun setContactAlias(contactConnection: PendingContactConnection, localAl
private fun PreviewContactConnectionInfoView() {
SimpleXTheme {
ContactConnectionInfoLayout(
localAlias = "",
connReq = "https://simplex.chat/contact#/?v=1&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2FK1rslx-m5bpXVIdMZg9NLUZ_8JBm8xTt%23MCowBQYDK2VuAyEALDeVe-sG8mRY22LsXlPgiwTNs9dbiLrNuA7f3ZMAJ2w%3D",
connectionInitiated = true,
connectionViaContactUri = true,
connectionIncognito = false,
PendingContactConnection.getSampleData(),
focusAlias = false,
deleteConnection = {},
onLocalAliasChanged = {},
@@ -5,6 +5,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.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.sp
@@ -12,6 +13,7 @@ import chat.simplex.app.R
import chat.simplex.app.model.ChatModel
import chat.simplex.app.ui.theme.DEFAULT_PADDING
import chat.simplex.app.ui.theme.HighOrLowlight
import chat.simplex.app.views.helpers.ModalManager
import chat.simplex.app.views.helpers.withApi
import chat.simplex.app.views.usersettings.UserAddressView
@@ -22,16 +24,27 @@ enum class CreateLinkTab {
@Composable
fun CreateLinkView(m: ChatModel, initialSelection: CreateLinkTab) {
val selection = remember { mutableStateOf(initialSelection) }
val connReqInvitation = remember { mutableStateOf("") }
val creatingConnReq = remember { mutableStateOf(false) }
val connReqInvitation = rememberSaveable { m.connReqInv }
val creatingConnReq = rememberSaveable { mutableStateOf(false) }
LaunchedEffect(selection.value) {
if (selection.value == CreateLinkTab.ONE_TIME && connReqInvitation.value.isEmpty() && !creatingConnReq.value) {
if (selection.value == CreateLinkTab.ONE_TIME && connReqInvitation.value.isNullOrEmpty() && !creatingConnReq.value) {
createInvitation(m, creatingConnReq, connReqInvitation)
}
}
/** When [AddContactView] is open, we don't need to drop [chatModel.connReqInv].
* Otherwise, it will be called here AFTER [AddContactView] is launched and will clear the value too soon.
* It will be dropped automatically when connection established or when user goes away from this screen.
**/
DisposableEffect(Unit) {
onDispose {
if (!ModalManager.shared.hasModalsOpen()) {
m.connReqInv.value = null
}
}
}
val tabTitles = CreateLinkTab.values().map {
when {
it == CreateLinkTab.ONE_TIME && connReqInvitation.value.isEmpty() -> stringResource(R.string.create_one_time_link)
it == CreateLinkTab.ONE_TIME && connReqInvitation.value.isNullOrEmpty() -> stringResource(R.string.create_one_time_link)
it == CreateLinkTab.ONE_TIME -> stringResource(R.string.one_time_link)
it == CreateLinkTab.LONG_TERM -> stringResource(R.string.your_contact_address)
else -> ""
@@ -46,7 +59,7 @@ fun CreateLinkView(m: ChatModel, initialSelection: CreateLinkTab) {
Column(Modifier.weight(1f)) {
when (selection.value) {
CreateLinkTab.ONE_TIME -> {
AddContactView(m, connReqInvitation.value, m.incognito.value)
AddContactView(connReqInvitation.value ?: "", m.incognito.value)
}
CreateLinkTab.LONG_TERM -> {
UserAddressView(m)
@@ -79,7 +92,7 @@ fun CreateLinkView(m: ChatModel, initialSelection: CreateLinkTab) {
}
}
private fun createInvitation(m: ChatModel, creatingConnReq: MutableState<Boolean>, connReqInvitation: MutableState<String>) {
private fun createInvitation(m: ChatModel, creatingConnReq: MutableState<Boolean>, connReqInvitation: MutableState<String?>) {
creatingConnReq.value = true
withApi {
val connReq = m.controller.apiAddContact()
@@ -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(
@@ -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) {
val action = uri.path?.drop(1)
if (action == "contact" || action == "invitation") {
withApi { run(action) }
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("/", "")
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
@@ -91,6 +135,7 @@ fun ConnectContactLayout(chatModelIncognito: Boolean, qrCodeScanner: @Composable
Modifier
.fillMaxWidth()
.aspectRatio(ratio = 1F)
.padding(bottom = 12.dp)
) { qrCodeScanner() }
Text(
annotatedStringResource(R.string.if_you_cannot_meet_in_person_scan_QR_in_video_call_or_ask_for_invitation_link),
@@ -0,0 +1,167 @@
package chat.simplex.app.views.usersettings
import SectionCustomFooter
import SectionDivider
import SectionItemView
import SectionView
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.TheaterComedy
import androidx.compose.material.icons.outlined.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import chat.simplex.app.R
import chat.simplex.app.model.*
import chat.simplex.app.ui.theme.*
import chat.simplex.app.views.helpers.*
@Composable
fun AcceptRequestsView(m: ChatModel, contactLink: UserContactLinkRec) {
var contactLink by remember { mutableStateOf(contactLink) }
AcceptRequestsLayout(
contactLink,
saveState = { new: MutableState<AutoAcceptState>, old: MutableState<AutoAcceptState> ->
withApi {
val link = m.controller.userAddressAutoAccept(new.value.autoAccept)
if (link != null) {
contactLink = link
m.userAddress.value = link
old.value = new.value
}
}
}
)
}
@Composable
private fun AcceptRequestsLayout(
contactLink: UserContactLinkRec,
saveState: (new: MutableState<AutoAcceptState>, old: MutableState<AutoAcceptState>) -> Unit,
) {
Column(
Modifier.fillMaxWidth().verticalScroll(rememberScrollState()),
) {
AppBarTitle(stringResource(R.string.contact_requests))
val autoAcceptState = remember { mutableStateOf(AutoAcceptState(contactLink)) }
val autoAcceptStateSaved = remember { mutableStateOf(autoAcceptState.value) }
SectionView(stringResource(R.string.accept_requests).uppercase()) {
SectionItemView {
PreferenceToggleWithIcon(stringResource(R.string.accept_automatically), Icons.Outlined.Check, checked = autoAcceptState.value.enable) {
autoAcceptState.value = if (!it)
AutoAcceptState()
else
AutoAcceptState(it, autoAcceptState.value.incognito, autoAcceptState.value.welcomeText)
}
}
if (autoAcceptState.value.enable) {
SectionDivider()
SectionItemView {
PreferenceToggleWithIcon(
stringResource(R.string.incognito),
if (autoAcceptState.value.incognito) Icons.Filled.TheaterComedy else Icons.Outlined.TheaterComedy,
if (autoAcceptState.value.incognito) Indigo else HighOrLowlight,
autoAcceptState.value.incognito,
) {
autoAcceptState.value = AutoAcceptState(autoAcceptState.value.enable, it, autoAcceptState.value.welcomeText)
}
}
}
}
val welcomeText = remember { mutableStateOf(autoAcceptState.value.welcomeText) }
SectionCustomFooter(PaddingValues(horizontal = DEFAULT_PADDING)) {
ButtonsFooter(
cancel = {
autoAcceptState.value = autoAcceptStateSaved.value
welcomeText.value = autoAcceptStateSaved.value.welcomeText
},
save = { saveState(autoAcceptState, autoAcceptStateSaved) },
disabled = autoAcceptState.value == autoAcceptStateSaved.value
)
}
Spacer(Modifier.height(DEFAULT_PADDING))
if (autoAcceptState.value.enable) {
Text(
stringResource(R.string.section_title_welcome_message), color = HighOrLowlight, style = MaterialTheme.typography.body2,
modifier = Modifier.padding(start = DEFAULT_PADDING, bottom = 5.dp), fontSize = 12.sp
)
TextEditor(Modifier.padding(horizontal = DEFAULT_PADDING).height(160.dp), text = welcomeText)
LaunchedEffect(welcomeText.value) {
if (welcomeText.value != autoAcceptState.value.welcomeText) {
autoAcceptState.value = AutoAcceptState(autoAcceptState.value.enable, autoAcceptState.value.incognito, welcomeText.value)
}
}
}
}
}
@Composable
private fun ButtonsFooter(cancel: () -> Unit, save: () -> Unit, disabled: Boolean) {
Row(
Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
FooterButton(Icons.Outlined.Replay, stringResource(R.string.cancel_verb), cancel, disabled)
FooterButton(Icons.Outlined.Check, stringResource(R.string.save_verb), save, disabled)
}
}
private class AutoAcceptState {
var enable: Boolean = false
private set
var incognito: Boolean = false
private set
var welcomeText: String = ""
private set
constructor(enable: Boolean = false, incognito: Boolean = false, welcomeText: String = "") {
this.enable = enable
this.incognito = incognito
this.welcomeText = welcomeText
}
constructor(contactLink: UserContactLinkRec) {
contactLink.autoAccept?.let { aa ->
enable = true
incognito = aa.acceptIncognito
aa.autoReply?.let { msg ->
welcomeText = msg.text
} ?: run {
welcomeText = ""
}
}
}
val autoAccept: AutoAccept?
get() {
if (enable) {
var autoReply: MsgContent? = null
val s = welcomeText.trim()
if (s != "") {
autoReply = MsgContent.MCText(s)
}
return AutoAccept(incognito, autoReply)
}
return null
}
override fun equals(other: Any?): Boolean {
if (other !is AutoAcceptState) return false
return this.enable == other.enable && this.incognito == other.incognito && this.welcomeText == other.welcomeText
}
override fun hashCode(): Int {
var result = enable.hashCode()
result = 31 * result + incognito.hashCode()
result = 31 * result + welcomeText.hashCode()
return result
}
}
@@ -3,6 +3,9 @@ package chat.simplex.app.views.usersettings
import android.annotation.SuppressLint
import android.security.keystore.KeyGenParameterSpec
import android.security.keystore.KeyProperties
import chat.simplex.app.R
import chat.simplex.app.views.helpers.AlertManager
import chat.simplex.app.views.helpers.generalGetString
import java.security.KeyStore
import javax.crypto.*
import javax.crypto.spec.GCMParameterSpec
@@ -10,11 +13,24 @@ import javax.crypto.spec.GCMParameterSpec
@SuppressLint("ObsoleteSdkInt")
internal class Cryptor {
private var keyStore: KeyStore = KeyStore.getInstance("AndroidKeyStore").apply { load(null) }
private var warningShown = false
fun decryptData(data: ByteArray, iv: ByteArray, alias: String): String {
fun decryptData(data: ByteArray, iv: ByteArray, alias: String): String? {
val secretKey = getSecretKey(alias)
if (secretKey == null) {
if (!warningShown) {
// Repeated calls will not show the alert again
warningShown = true
AlertManager.shared.showAlertMsg(
title = generalGetString(R.string.wrong_passphrase),
text = generalGetString(R.string.restore_passphrase_not_found_desc)
)
}
return null
}
val cipher: Cipher = Cipher.getInstance(TRANSFORMATION)
val spec = GCMParameterSpec(128, iv)
cipher.init(Cipher.DECRYPT_MODE, getSecretKey(alias), spec)
cipher.init(Cipher.DECRYPT_MODE, secretKey, spec)
return String(cipher.doFinal(data))
}
@@ -29,7 +45,7 @@ internal class Cryptor {
keyStore.deleteEntry(alias)
}
private fun createSecretKey(alias: String): SecretKey {
private fun createSecretKey(alias: String): SecretKey? {
if (keyStore.containsAlias(alias)) return getSecretKey(alias)
val keyGenerator: KeyGenerator = KeyGenerator.getInstance(KEY_ALGORITHM, "AndroidKeyStore")
keyGenerator.init(
@@ -41,8 +57,8 @@ internal class Cryptor {
return keyGenerator.generateKey()
}
private fun getSecretKey(alias: String): SecretKey {
return (keyStore.getEntry(alias, null) as KeyStore.SecretKeyEntry).secretKey
private fun getSecretKey(alias: String): SecretKey? {
return (keyStore.getEntry(alias, null) as? KeyStore.SecretKeyEntry)?.secretKey
}
companion object {
@@ -4,19 +4,15 @@ import SectionDivider
import SectionSpacer
import SectionView
import androidx.compose.foundation.layout.*
import androidx.compose.material.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Image
import androidx.compose.material.icons.outlined.TravelExplore
import androidx.compose.material.icons.outlined.*
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import chat.simplex.app.R
import chat.simplex.app.model.ChatModel
import chat.simplex.app.views.helpers.AppBarTitle
import chat.simplex.app.views.helpers.generalGetString
@Composable
fun PrivacySettingsView(chatModel: ChatModel, setPerformLA: (Boolean) -> Unit) {
@@ -33,6 +29,10 @@ fun PrivacySettingsView(chatModel: ChatModel, setPerformLA: (Boolean) -> Unit) {
SectionView(stringResource(R.string.settings_section_title_chats)) {
SettingsPreferenceItem(Icons.Outlined.Image, stringResource(R.string.auto_accept_images), chatModel.controller.appPrefs.privacyAcceptImages)
SectionDivider()
if (chatModel.controller.appPrefs.developerTools.get()) {
SettingsPreferenceItem(Icons.Outlined.ImageAspectRatio, stringResource(R.string.transfer_images_faster), chatModel.controller.appPrefs.privacyTransferImagesInline)
SectionDivider()
}
SettingsPreferenceItem(Icons.Outlined.TravelExplore, stringResource(R.string.send_link_previews), chatModel.controller.appPrefs.privacyLinkPreviews)
}
}
@@ -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(
@@ -342,6 +394,36 @@ fun SettingsPreferenceItemWithInfo(
}
}
@Composable
fun PreferenceToggleWithIcon(
text: String,
icon: ImageVector,
iconColor: Color = HighOrLowlight,
checked: Boolean,
onChange: (Boolean) -> Unit = {},
) {
Row(Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) {
Icon(
icon,
null,
tint = iconColor
)
Spacer(Modifier.padding(horizontal = 4.dp))
Text(text)
Spacer(Modifier.fillMaxWidth().weight(1f))
Switch(
checked = checked,
onCheckedChange = {
onChange(it)
},
colors = SwitchDefaults.colors(
checkedThumbColor = MaterialTheme.colors.primary,
uncheckedThumbColor = HighOrLowlight
)
)
}
}
@Preview(showBackground = true)
@Preview(
uiMode = Configuration.UI_MODE_NIGHT_YES,
@@ -2,10 +2,13 @@ package chat.simplex.app.views.usersettings
import android.content.res.Configuration
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.Text
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
@@ -16,8 +19,8 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import chat.simplex.app.R
import chat.simplex.app.model.ChatModel
import chat.simplex.app.ui.theme.SimpleButton
import chat.simplex.app.ui.theme.SimpleXTheme
import chat.simplex.app.model.UserContactLinkRec
import chat.simplex.app.ui.theme.*
import chat.simplex.app.views.helpers.*
import chat.simplex.app.views.newchat.QRCode
@@ -25,13 +28,21 @@ import chat.simplex.app.views.newchat.QRCode
fun UserAddressView(chatModel: ChatModel) {
val cxt = LocalContext.current
UserAddressLayout(
userAddress = chatModel.userAddress.value,
userAddress = remember { chatModel.userAddress }.value,
createAddress = {
withApi {
chatModel.userAddress.value = chatModel.controller.apiCreateUserAddress()
val connReqContact = chatModel.controller.apiCreateUserAddress()
if (connReqContact != null) {
chatModel.userAddress.value = UserContactLinkRec(connReqContact)
}
}
},
share = { userAddress: String -> shareText(cxt, userAddress) },
acceptRequests = {
chatModel.userAddress.value?.let { address ->
ModalManager.shared.showModal(settings = true) { AcceptRequestsView(chatModel, address) }
}
},
deleteAddress = {
AlertManager.shared.showAlertMsg(
title = generalGetString(R.string.delete_address__question),
@@ -50,12 +61,14 @@ fun UserAddressView(chatModel: ChatModel) {
@Composable
fun UserAddressLayout(
userAddress: String?,
userAddress: UserContactLinkRec?,
createAddress: () -> Unit,
share: (String) -> Unit,
acceptRequests: () -> Unit,
deleteAddress: () -> Unit
) {
Column(
Modifier.verticalScroll(rememberScrollState()),
horizontalAlignment = Alignment.Start,
verticalArrangement = Arrangement.Top
) {
@@ -66,40 +79,35 @@ fun UserAddressLayout(
lineHeight = 22.sp
)
Column(
Modifier.fillMaxWidth(),
Modifier.fillMaxWidth().padding(bottom = DEFAULT_PADDING_HALF),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.SpaceEvenly
) {
if (userAddress == null) {
Text(
stringResource(R.string.if_you_later_delete_address_you_wont_lose_contacts),
Modifier.padding(bottom = 12.dp),
lineHeight = 22.sp
)
SimpleButton(stringResource(R.string.create_address), icon = Icons.Outlined.QrCode, click = createAddress)
} else {
Text(
stringResource(R.string.if_you_delete_address_you_wont_lose_contacts),
Modifier.padding(bottom = 12.dp),
lineHeight = 22.sp
)
QRCode(userAddress, Modifier.weight(1f, fill = false).aspectRatio(1f))
QRCode(userAddress.connReqContact, Modifier.aspectRatio(1f))
Row(
horizontalArrangement = Arrangement.spacedBy(10.dp),
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.padding(vertical = 10.dp)
modifier = Modifier.padding(vertical = 16.dp)
) {
SimpleButton(
stringResource(R.string.share_link),
icon = Icons.Outlined.Share,
click = { share(userAddress) })
SimpleButton(
stringResource(R.string.delete_address),
icon = Icons.Outlined.Delete,
color = Color.Red,
click = deleteAddress
click = { share(userAddress.connReqContact) })
SimpleButtonIconEnded(
stringResource(R.string.contact_requests),
icon = Icons.Outlined.ChevronRight,
click = acceptRequests
)
}
SimpleButton(
stringResource(R.string.delete_address),
icon = Icons.Outlined.Delete,
color = Color.Red,
click = deleteAddress
)
}
}
}
@@ -118,6 +126,7 @@ fun PreviewUserAddressLayoutNoAddress() {
userAddress = null,
createAddress = {},
share = { _ -> },
acceptRequests = {},
deleteAddress = {},
)
}
@@ -133,9 +142,10 @@ fun PreviewUserAddressLayoutNoAddress() {
fun PreviewUserAddressLayoutAddressCreated() {
SimpleXTheme {
UserAddressLayout(
userAddress = "https://simplex.chat/contact#/?v=1&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2FK1rslx-m5bpXVIdMZg9NLUZ_8JBm8xTt%23MCowBQYDK2VuAyEALDeVe-sG8mRY22LsXlPgiwTNs9dbiLrNuA7f3ZMAJ2w%3D",
userAddress = UserContactLinkRec("https://simplex.chat/contact#/?v=1&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2FK1rslx-m5bpXVIdMZg9NLUZ_8JBm8xTt%23MCowBQYDK2VuAyEALDeVe-sG8mRY22LsXlPgiwTNs9dbiLrNuA7f3ZMAJ2w%3D"),
createAddress = {},
share = { _ -> },
acceptRequests = {},
deleteAddress = {},
)
}
@@ -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()
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item>
<shape>
<corners android:radius="18dp"/>
<solid android:color="@android:color/transparent"/>
<stroke
android:width="1dp"
android:color="#8b8786"/>
</shape>
</item>
</layer-list>
@@ -6,13 +6,15 @@
<!-- 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 -->
<string name="server_connected">Verbunden</string>
<string name="server_error">Fehler</string>
<string name="server_connecting">verbinde</string>
<string name="server_connecting">Verbinde</string>
<string name="connected_to_server_to_receive_messages_from_contact">Sie sind mit dem Server verbunden, der für den Empfang von Nachrichten mit diesem Kontakt genutzt wird.</string>
<string name="trying_to_connect_to_server_to_receive_messages_with_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: <xliff:g id="errorMsg">%1$s</xliff:g>).</string>
<string name="trying_to_connect_to_server_to_receive_messages">Versuche die Verbindung mit dem Server aufzunehmen, der für den Empfang von Nachrichten mit diesem Kontakt genutzt wird.</string>
@@ -26,14 +28,16 @@
<string name="invalid_message_format">Unzulässiges Nachrichtenformat</string>
<!-- PendingContactConnection - ChatModel.kt -->
<string name="connection_local_display_name">Verbindung <xliff:g id="connection ID" example="1">%1$d</xliff:g></string>
<string name="display_name_connection_established">Verbindung hergestellt</string>
<string name="display_name_invited_to_connect">Für eine Verbindung eingeladen</string>
<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_contact_address_link">über einen Kontaktadressen Link</string>
<string name="description_via_contact_address_link_incognito">Inkognito über einen Kontaktadressen Link</string>
<string name="connection_local_display_name">verbindung <xliff:g id="connection ID" example="1">%1$d</xliff:g></string>
<string name="display_name_connection_established">verbindung hergestellt</string>
<string name="display_name_invited_to_connect">für eine Verbindung eingeladen</string>
<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">ü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>
@@ -54,19 +58,18 @@
<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>
<string name="connection_error_auth_desc">Entweder hat Ihr Kontakt die Verbindung gelöscht, oder dieser Link wurde bereits verwendet, es könnte sich um einen Fehler handeln - Bitte melden Sie es uns.\nBitten Sie Ihren Kontakt darum einen weiteren Verbindungs-Link zu erzeugen, um sich neu verbinden zu können und stellen Sie sicher, dass Sie eine stabile Netzwerk-Verbindung haben.</string>
<string name="error_accepting_contact_request">Fehler beim Akzeptieren der Kontaktanfrage</string>
<string name="sender_may_have_deleted_the_connection_request">Der Absender hat möglicherweise die Verbindungsanfrage gelöscht.</string>
<string name="cannot_delete_contact">Der Kontakt kann nicht gelöscht werden!</string>
<string name="contact_cannot_be_deleted_as_they_are_in_groups">Der Kontakt mit <xliff:g id="contactName" example="Jane Doe">%1$s!</xliff:g> kann nicht gelöscht werden, da er Mitglied einer oder mehrerer dieser Gruppen ist <xliff:g id="groups" example="[team, chess club]">%2$s</xliff:g>.</string>
<string name="error_deleting_contact">Fehler beim Löschen des Kontakts</string>
<string name="error_deleting_group">Fehler beim Löschen der Gruppe</string>
<string name="error_deleting_contact_request">Fehler beim Löschen der Kontakt-Anfrage</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">Fehler beim Wechseln der Adresse</string>
<!-- background service notice - SimpleXAPI.kt -->
<string name="icon_descr_instant_notifications">Sofortige Benachrichtigungen</string>
@@ -111,9 +114,9 @@
<string name="notification_preview_mode_contact_desc">Nur Kontakt anzeigen</string>
<string name="notification_display_mode_hidden_desc">Kontakt und Nachricht verbergen</string>
<string name="notification_preview_somebody">Kontakt verbergen:</string>
<string name="notification_preview_new_message">neue Nachricht</string>
<string name="notification_new_contact_request">New contact request</string>
<string name="notification_contact_connected">Сonnected</string>
<string name="notification_preview_new_message">Neue Nachricht</string>
<string name="notification_new_contact_request">Neue Kontaktanfrage</string>
<string name="notification_contact_connected">Verbunden</string>
<!-- local authentication notice - SimpleXAPI.kt -->
<string name="la_notice_title_simplex_lock">SimpleX Sperre</string>
@@ -128,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>
@@ -151,11 +154,11 @@
<string name="for_everybody">Für alle</string>
<!-- CIMetaView.kt -->
<string name="icon_descr_edited">bearbeitet</string>
<string name="icon_descr_sent_msg_status_sent">gesendet</string>
<string name="icon_descr_sent_msg_status_unauthorized_send">nicht autorisiertes Senden</string>
<string name="icon_descr_edited">Bearbeitet</string>
<string name="icon_descr_sent_msg_status_sent">Gesendet</string>
<string name="icon_descr_sent_msg_status_unauthorized_send">Nicht autorisiertes Senden</string>
<string name="icon_descr_sent_msg_status_send_failed">Senden fehlgeschlagen</string>
<string name="icon_descr_received_msg_status_unread">ungelesen</string>
<string name="icon_descr_received_msg_status_unread">Ungelesen</string>
<!-- ChatListView.kt -->
<string name="personal_welcome">Willkommen <xliff:g>%1$s</xliff:g>!</string>
@@ -164,9 +167,9 @@
<string name="your_chats">Meine Chats</string>
<string name="contact_connection_pending">verbinde …</string>
<string name="group_preview_you_are_invited">Sie sind zu der Gruppe eingeladen</string>
<string name="group_preview_join_as">beitreten als %s</string>
<string name="group_preview_join_as">Beitreten als %s</string>
<string name="group_connection_pending">verbinde …</string>
<string name="tap_to_start_new_chat">Tippen Sie um einen neuen Chat zu starten</string>
<string name="tap_to_start_new_chat">Tippen Sie, um einen neuen Chat zu starten</string>
<string name="chat_with_developers">Chatten Sie mit den Entwicklern</string>
<string name="you_have_no_chats">Sie haben keine Chats</string>
@@ -182,14 +185,16 @@
<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 - CIImageView.kt -->
<!-- Images - chat.simplex.app.views.chat.item.CIImageView.kt -->
<string name="image_descr">Bild</string>
<string name="icon_descr_waiting_for_image">Warten auf ein Bild</string>
<string name="icon_descr_asked_to_receive">Um Empfang des Bildes gebeten</string>
<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 -->
@@ -198,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>
@@ -215,6 +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">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>
@@ -224,7 +231,7 @@
<string name="cancel_verb">Abbrechen</string>
<string name="confirm_verb">Bestätigen</string>
<string name="ok">OK</string>
<string name="no_details">keine Details</string>
<string name="no_details">Keine Details</string>
<string name="add_contact">Kontakt hinzufügen</string>
<string name="copied">In die Zwischenablage kopiert</string>
@@ -236,7 +243,7 @@
<string name="create_group">Geheime Gruppe erstellen</string>
<string name="to_share_with_your_contact">(zum Teilen mit Ihrem Kontakt)</string>
<string name="connect_via_link_or_qr_from_clipboard_or_in_person">(Scannen oder Einfügen aus der Zwischenablage)</string>
<string name="only_stored_on_members_devices">(wird nur von Gruppenmitgliedern gespeichert)</string>
<string name="only_stored_on_members_devices">(Wird nur von Gruppenmitgliedern gespeichert)</string>
<!-- GetImageView -->
<string name="toast_permission_denied">Berechtigung verweigert!</string>
@@ -249,7 +256,7 @@
<string name="you_can_connect_to_simplex_chat_founder">Sie können sich <font color="#0088ff">mit <xliff:g id="appNameFull">SimpleX Chat</xliff:g> Entwicklern verbinden, um Fragen zu stellen und Updates zu erhalten</font>.</string>
<string name="to_start_a_new_chat_help_header">Um einen neuen Chat zu starten</string>
<string name="chat_help_tap_button">Schaltfläche antippen</string>
<string name="above_then_preposition_continuation">über, dann:</string>
<string name="above_then_preposition_continuation">Danach die gewünschte Aktion auswählen:</string>
<string name="add_new_contact_to_create_one_time_QR_code"><b>Neuen Kontakt hinzufügen</b>: Um Ihren Einmal-QR-Code für Ihren Kontakt zu erstellen.</string>
<string name="scan_QR_code_to_connect_to_contact_who_shows_QR_code"><b>QR-Code scannen</b>: Um sich mit Ihrem Kontakt zu verbinden, der Ihnen seinen QR-Code zeigt.</string>
<string name="to_connect_via_link_title">Über Link verbinden</string>
@@ -273,6 +280,7 @@
<string name="delete_contact_menu_action">Kontakt löschen</string>
<string name="delete_group_menu_action">Gruppe löschen</string>
<string name="mark_read">Als gelesen markieren</string>
<string name="mark_unread">Als ungelesen markieren</string>
<string name="set_contact_name">Kontaktname festlegen</string>
<!-- Actions - ChatListNavLinkView.kt -->
@@ -298,7 +306,7 @@
<string name="image_descr_profile_image">Profilbild</string>
<!-- Content Descriptions -->
<string name="icon_descr_close_button">Schließen Schaltfläche</string>
<string name="icon_descr_close_button">Schließen der Schaltfläche</string>
<string name="image_descr_link_preview">Vorschaubild verlinken</string>
<string name="icon_descr_cancel_link_preview">Link Vorschau abbrechen</string>
<string name="icon_descr_settings">Einstellungen</string>
@@ -319,8 +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_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>
@@ -349,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>
@@ -394,12 +406,16 @@
<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 mit Ihnen verbinden.</string>
<string name="if_you_delete_address_you_wont_lose_contacts">Wenn Sie diese Adresse löschen, werden Sie Ihre Kontakte nicht verlieren.</string>
<string name="if_you_later_delete_address_you_wont_lose_contacts">Wenn Sie diese Adresse später löschen, werden Sie Ihre mit dieser Adresse verbundenen Kontakte nicht verlieren.</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>
<!-- AcceptRequestsView.kt -->
<string name="contact_requests">Kontaktanfragen</string>
<string name="accept_requests">Anfragen annehmen</string>
<string name="accept_automatically">Automatisch</string>
<string name="section_title_welcome_message">Begrüßungsmeldung</string>
<!-- User profile details - UserProfileView.kt -->
<string name="display_name__field">Angezeigter Name:</string>
<string name="full_name__field">"Vollständiger Name:</string>
@@ -449,8 +465,8 @@
<string name="callstate_received_answer">Antwort erhalten…</string>
<string name="callstate_received_confirmation">Bestätigung erhalten…</string>
<string name="callstate_connecting">Verbindung wird hergestellt…</string>
<string name="callstate_connected">verbunden</string>
<string name="callstate_ended">beendet</string>
<string name="callstate_connected">Verbunden</string>
<string name="callstate_ended">Beendet</string>
<!-- SimpleXInfo -->
<string name="next_generation_of_private_messaging">Die nächste Generation von privatem Messaging</string>
@@ -459,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>
@@ -544,12 +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">Bilder schneller übertragen (BETA)</string>
<string name="send_link_previews">Link-Vorschau senden</string>
<!-- Settings sections -->
<string name="settings_section_title_you">DU</string>
<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>
@@ -565,8 +583,8 @@
<!-- DatabaseView.kt -->
<string name="your_chat_database">Meine Chat-Datenbank</string>
<string name="run_chat_section">CHAT STARTEN</string>
<string name="chat_is_running">Chat läuft</string>
<string name="chat_is_stopped">Chat ist beendet</string>
<string name="chat_is_running">Der Chat läuft</string>
<string name="chat_is_stopped">Der Chat ist beendet</string>
<string name="chat_database_section">CHAT-DATENBANK</string>
<string name="database_passphrase">Datenbank-Passwort</string>
<string name="export_database">Datenbank exportieren</string>
@@ -576,7 +594,7 @@
<string name="delete_database">Datenbank löschen</string>
<string name="error_starting_chat">Fehler beim Starten des Chats</string>
<string name="stop_chat_question">Chat beenden?</string>
<string name="stop_chat_to_export_import_or_delete_chat_database">Beenden Sie den Chat, um die Chat-Datenbank zu exportieren, zu importieren oder zu löschen. Sie können keine Nachrichten empfangen oder senden, solange der Chat angehalten ist.</string>
<string name="stop_chat_to_export_import_or_delete_chat_database">Beenden Sie den Chat, um die Chat-Datenbank zu exportieren, zu importieren oder zu löschen. Solange der Chat angehalten ist, können Sie keine Nachrichten empfangen oder senden.</string>
<string name="stop_chat_confirmation">Beenden</string>
<string name="set_password_to_export">Passwort zum Exportieren festlegen</string>
<string name="set_password_to_export_desc">Die Datenbank ist mit einem zufälligen Passwort verschlüsselt. Bitte ändern Sie es vor dem Exportieren.</string>
@@ -598,17 +616,17 @@
<string name="data_section">DATEN</string>
<string name="delete_files_and_media">Dateien \&amp; Medien löschen</string>
<string name="delete_files_and_media_question">Dateien und Medien löschen?</string>
<string name="delete_files_and_media_desc">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.</string>
<string name="delete_files_and_media_desc">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.</string>
<string name="no_received_app_files">Keine empfangenen oder gesendeten Dateien</string>
<string name="total_files_count_and_size">%d Datei(en) mit einem Gesamtspeicherverbrauch von %s</string>
<string name="chat_item_ttl_none">nie</string>
<string name="chat_item_ttl_day">1 Tag</string>
<string name="chat_item_ttl_week">1 Woche</string>
<string name="chat_item_ttl_month">1 Monat</string>
<string name="chat_item_ttl_day">täglich</string>
<string name="chat_item_ttl_week">wöchentlich</string>
<string name="chat_item_ttl_month">monatlich</string>
<string name="chat_item_ttl_seconds">%s Sekunde(n)</string>
<string name="delete_messages_after">Löschen der Nachrichten nach</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 - Die gesendeten und empfangenen Nachrichten, welche älter als die Ausgewählten sind, 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>
@@ -664,6 +682,7 @@
<string name="restore_database_alert_desc">Bitte geben Sie das vorherige Passwort ein, nachdem Sie die Datenbanksicherung wiederhergestellt haben. Diese Aktion kann nicht rückgängig gemacht werden.</string>
<string name="restore_database_alert_confirm">Wiederherstellen</string>
<string name="database_restore_error">Fehler bei der Wiederherstellung der Datenbank</string>
<string name="restore_passphrase_not_found_desc">***Passphrase not found in Keystore, please enter it manually. This may have happened if you restored the app\'s data using a backup tool. If it\'s not the case, please, contact developers.</string>
<!-- ChatModel.chatRunning interactions -->
<string name="chat_is_stopped_indication">Chat wurde beendet</string>
@@ -683,10 +702,10 @@
<string name="you_are_invited_to_group_join_to_connect_with_group_members">Sie sind zu einer Gruppe eingeladen worden. Treten Sie bei, um sich mit Gruppenmitgliedern zu verbinden.</string>
<string name="join_group_button">Beitreten</string>
<string name="join_group_incognito_button">Inkognito beitreten</string>
<string name="joining_group">Gruppe beitreten</string>
<string name="joining_group">Der Gruppe beitreten</string>
<string name="youve_accepted_group_invitation_connecting_to_inviting_group_member">Sie sind dieser Gruppe beigetreten. Sie werden mit dem einladenden Gruppenmitglied verbunden.</string>
<string name="leave_group_button">Verlassen</string>
<string name="leave_group_question">Gruppe verlassen?</string>
<string name="leave_group_question">Die Gruppe verlassen?</string>
<string name="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.</string>
<string name="icon_descr_add_members">Mitglieder einladen</string>
<string name="icon_descr_group_inactive">Gruppe inaktiv</string>
@@ -707,33 +726,46 @@
<string name="group_invitation_expired">Die Gruppeneinladung ist abgelaufen</string>
<!-- Group event chat items -->
<string name="rcv_group_event_member_added">Sie haben <xliff:g id="member profile" example="alice (Alice)">%1$s</xliff:g> eingeladen.</string>
<string name="rcv_group_event_member_connected">verbunden</string>
<string name="rcv_group_event_member_left">verlassen</string>
<string name="rcv_group_event_member_deleted">entfernte <xliff:g id="member profile" example="alice (Alice)">%1$s</xliff:g>.</string>
<string name="rcv_group_event_user_deleted">hat Sie entfernt</string>
<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">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>
<string name="rcv_group_event_user_deleted">hat Sie aus der Gruppe entfernt</string>
<string name="rcv_group_event_group_deleted">Gruppe gelöscht</string>
<string name="rcv_group_event_updated_group_profile">aktualisiertes Gruppenprofil</string>
<string name="snd_group_event_member_deleted">Sie haben <xliff:g id="member profile" example="alice (Alice)">%1$s</xliff:g> entfernt.</string>
<string name="snd_group_event_user_left">Sie haben verlassen</string>
<string name="rcv_group_event_updated_group_profile">Aktualisiertes Gruppenprofil</string>
<string name="rcv_group_event_invited_via_your_group_link">wurde über Ihren Gruppen-Link eingeladen</string>
<string name="snd_group_event_changed_member_role">Sie haben die Rolle von %s auf %s geändert</string>
<string name="snd_group_event_changed_role_for_yourself">Sie haben Ihre eigene Rolle auf %s geändert</string>
<string name="snd_group_event_member_deleted">entfernt <xliff:g id="member profile" example="alice (Alice)">%1$s</xliff:g> aus der Gruppe.</string>
<string name="snd_group_event_user_left">hat die Gruppe verlassen</string>
<string name="snd_group_event_group_profile_updated">Gruppenprofil aktualisiert</string>
<!-- Conn event chat items -->
<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>
<string name="group_member_role_admin">Admin</string>
<string name="group_member_role_owner">Eigentümer</string>
<!-- GroupMemberStatus -->
<string name="group_member_status_removed">entfernt</string>
<string name="group_member_status_left">verlassen</string>
<string name="group_member_status_removed">Entfernt</string>
<string name="group_member_status_left">Verlassen</string>
<string name="group_member_status_group_deleted">Gruppe gelöscht</string>
<string name="group_member_status_invited">eingeladen</string>
<string name="group_member_status_invited">Eingeladen</string>
<string name="group_member_status_introduced">Verbindung (erstellt)</string>
<string name="group_member_status_intro_invitation">Verbindung (eingeladen)</string>
<string name="group_member_status_accepted">Verbindung (akzeptiert)</string>
<string name="group_member_status_announced">Verbindung (angekündigt)</string>
<string name="group_member_status_connected">verbunden</string>
<string name="group_member_status_complete">vollständig</string>
<string name="group_member_status_connected">Verbunden</string>
<string name="group_member_status_complete">Vollständig</string>
<string name="group_member_status_creator">Ersteller</string>
<string name="group_member_status_connecting">verbinden</string>
@@ -760,6 +792,14 @@
<string name="delete_group_for_self_cannot_undo_warning">Die Gruppe wird für Sie gelöscht - dies kann nicht rückgängig gemacht werden!</string>
<string name="button_leave_group">Gruppe verlassen</string>
<string name="button_edit_group_profile">Gruppenprofil bearbeiten</string>
<string name="group_link">Gruppen-Link</string>
<string name="button_create_group_link">Link erzeugen</string>
<string name="delete_link_question">Link löschen?</string>
<string name="delete_link">Link löschen</string>
<string name="you_can_share_group_link_anybody_will_be_able_to_connect">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.</string>
<string name="all_group_members_will_remain_connected">Alle Gruppenmitglieder bleiben verbunden.</string>
<string name="error_creating_link_for_group">Fehler beim Erzeugen des Gruppen-Links</string>
<string name="error_deleting_link_for_group">Fehler beim Löschen des Gruppen-Links</string>
<!-- For Console chat info section -->
<string name="section_title_for_console">FÜR KONSOLE</string>
@@ -775,13 +815,12 @@
<string name="role_in_group">Rolle</string>
<string name="change_role">Rolle ändern</string>
<string name="change_verb">Ändern</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>
<string name="error_removing_member">Fehler beim Entfernen des Mitglieds</string>
<string name="error_changing_role">Fehler beim Ändern der Rolle</string>
<string name="member_role">Mitglieder %s Rolle: %s</string>
<string name="your_member_role">Meine Rolle: %s</string>
<string name="info_row_group">Gruppe</string>
<string name="info_row_connection">Verbindung</string>
<string name="conn_level_desc_direct">direkt</string>
@@ -792,6 +831,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">Wechseln der Empfängeradresse (BETA)</string>
<!-- AddGroupView.kt -->
<string name="create_secret_group_title">Geheime Gruppe erstellen</string>
@@ -799,7 +839,7 @@
<string name="group_display_name_field">Anzeigename der Gruppe:</string>
<string name="group_full_name_field">Vollständiger Gruppenname:</string>
<string name="group_unsupported_incognito_main_profile_sent">Der Inkognito-Modus wird hier nicht unterstützt - Ihr Hauptprofil wird an die Gruppenmitglieder gesendet</string>
<string name="group_main_profile_sent">Ihr Chat-Profil wird an Gruppenmitglieder gesendet</string>
<string name="group_main_profile_sent">Ihr Chat-Profil wird an die Gruppenmitglieder gesendet</string>
<!-- GroupProfileView.kt -->
@@ -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 -->
@@ -32,6 +34,8 @@
<string name="display_name_connecting">соединяется…</string>
<string name="description_you_shared_one_time_link">вы создали одноразовую ссылку</string>
<string name="description_you_shared_one_time_link_incognito">вы создали одноразовую ссылку инкогнито</string>
<string name="description_via_group_link">через ссылку группы</string>
<string name="description_via_group_link_incognito">инкогнито через ссылку группы</string>
<string name="description_via_contact_address_link">через ссылку-контакт</string>
<string name="description_via_contact_address_link_incognito">инкогнито через ссылку-контакт</string>
<string name="description_via_one_time_link">через одноразовую ссылку</string>
@@ -54,19 +58,18 @@
<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>
<string name="connection_error_auth_desc">Возможно, ваш контакт удалил ссылку, или она уже была использована. Если это не так, то это может быть ошибкой - пожалуйста, сообщите нам об этом.\nЧтобы установить соединение, попросите ваш контакт создать еще одну ссылку и проверьте ваше соединение с сетью.</string>
<string name="error_accepting_contact_request">Ошибка при принятии запроса на соединение</string>
<string name="sender_may_have_deleted_the_connection_request">Отправитель мог удалить запрос на соединение.</string>
<string name="cannot_delete_contact">Невозможно удалить контакт!</string>
<string name="contact_cannot_be_deleted_as_they_are_in_groups">Контакт <xliff:g id="contactName" example="Jane Doe">%1$s!</xliff:g> не может быть удален, так как является членом групп(ы) <xliff:g id="groups" example="[team, chess club]">%2$s</xliff:g>.</string>
<string name="error_deleting_contact">Ошибка удаления контакта</string>
<string name="error_deleting_contact">Ошибка при удалении контакта</string>
<string name="error_deleting_group">Ошибка удаления группы</string>
<string name="error_deleting_contact_request">Ошибка удаления запроса</string>
<string name="error_deleting_pending_contact_connection">Ошибка удаления ожидаемого соединения</string>
<string name="error_changing_address">Ошибка при изменении адреса</string>
<!-- background service notice - SimpleXAPI.kt -->
<string name="icon_descr_instant_notifications">Мгновенные уведомления</string>
@@ -128,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>
@@ -182,8 +185,10 @@
<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 - CIImageView.kt -->
<!-- Images - chat.simplex.app.views.chat.item.CIImageView.kt -->
<string name="image_descr">Изображение</string>
<string name="icon_descr_waiting_for_image">Ожидается прием изображения</string>
<string name="icon_descr_asked_to_receive">Предложено получить изображение</string>
@@ -215,6 +220,8 @@
<string name="icon_descr_server_status_disconnected">Соединение с сервером не установлено</string>
<string name="icon_descr_server_status_error">Ошибка соединения с сервером</string>
<string name="icon_descr_server_status_pending">Ожидается соединение с сервером</string>
<string name="switch_receiving_address_question">Переключить адрес получения?</string>
<string name="switch_receiving_address_desc">Это экспериментальная функция! Она будет работать, только если на другом клиенте установлена версия 4.2. После завершения смены адреса вы увидите сообщение — убедитесь, что вы все еще можете получать сообщения от этого контакта (или члена группы).</string>
<!-- Message Actions - SendMsgView.kt -->
<string name="icon_descr_send_message">Отправить сообщение</string>
@@ -273,6 +280,7 @@
<string name="delete_contact_menu_action">Удалить</string>
<string name="delete_group_menu_action">Удалить</string>
<string name="mark_read">Прочитано</string>
<string name="mark_unread">Не прочитано</string>
<string name="set_contact_name">Имя контакта</string>
<!-- Actions - ChatListNavLinkView.kt -->
@@ -319,8 +327,9 @@
<string name="invalid_contact_link">Неверная ссылка!</string>
<string name="this_link_is_not_a_valid_connection_link">Эта ссылка не является ссылкой-приглашением!</string>
<string name="connection_request_sent">Запрос на соединение послан!</string>
<string name="you_will_be_connected_when_your_connection_request_is_accepted">Соединение будет установлено когда ваш запрос будет принят. Пожалуйста, подождите или проверьте позже!</string>
<string name="you_will_be_connected_when_your_contacts_device_is_online">Соединение будет установлено когда ваш контакт будет онлайн. Пожалуйста, подождите или проверьте позже!</string>
<string name="you_will_be_connected_when_group_host_device_is_online">Соединение с группой будет установлено, когда хост группы будет онлайн. Пожалуйста, подождите или проверьте позже!</string>
<string name="you_will_be_connected_when_your_connection_request_is_accepted">Соединение будет установлено, когда ваш запрос будет принят. Пожалуйста, подождите или проверьте позже!</string>
<string name="you_will_be_connected_when_your_contacts_device_is_online">Соединение будет установлено, когда ваш контакт будет онлайн. Пожалуйста, подождите или проверьте позже!</string>
<string name="show_QR_code_for_your_contact_to_scan_from_the_app__multiline">Ваш контакт может сосканировать QR код в приложении.</string>
<string name="if_you_cannot_meet_in_person_show_QR_in_video_call_or_via_another_channel">Если вы не можете встретиться лично, вы можете <b>показать QR код во время видеозвонка</b> или поделиться ссылкой.</string>
<string name="your_chat_profile_will_be_sent_to_your_contact">Ваш профиль будет отправлен\nвашему контакту</string>
@@ -346,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>
@@ -391,12 +403,16 @@
<string name="create_address">Создать адрес</string>
<string name="delete_address__question">Удалить адрес?</string>
<string name="all_your_contacts_will_remain_connected">Все контакты, которые соединились через этот адрес, сохранятся.</string>
<string name="you_can_share_your_address_anybody_will_be_able_to_connect">Вы можете использовать адрес как ссылку или как QR код - через него можно с вами соединиться.</string>
<string name="if_you_later_delete_address_you_wont_lose_contacts">Вы сможете удалить адрес, сохранив контакты, которые через него соединились.</string>
<string name="if_you_delete_address_you_wont_lose_contacts">Вы можете удалить адрес, сохранив контакты, которые через него соединились.</string>
<string name="you_can_share_your_address_anybody_will_be_able_to_connect">Вы можете использовать ваш адрес как ссылку или как QR код - кто угодно сможет соединиться с вами. Вы сможете удалить адрес, сохранив контакты, которые через него соединились.</string>
<string name="share_link">Поделиться\nссылкой</string>
<string name="delete_address">Удалить\nадрес</string>
<!-- AcceptRequestsView.kt -->
<string name="contact_requests">Запросы контактов</string>
<string name="accept_requests">Принимать запросы</string>
<string name="accept_automatically">Автоматически</string>
<string name="section_title_welcome_message">ПРИВЕТСТВЕННОЕ СООБЩЕНИЕ</string>
<!-- User profile details - UserProfileView.kt -->
<string name="display_name__field">Имя профиля:</string>
<string name="full_name__field">"Полное имя:</string>
@@ -544,12 +560,14 @@
<string name="privacy_and_security">Конфиденциальность</string>
<string name="your_privacy">Конфиденциальность</string>
<string name="auto_accept_images">Автоприем изображений</string>
<string name="transfer_images_faster">Передавать изображения быстрее (BETA)</string>
<string name="send_link_previews">Отправлять картинки ссылок</string>
<!-- Settings sections -->
<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>
@@ -664,6 +682,7 @@
<string name="restore_database_alert_desc">Введите предыдущий пароль после восстановления резервной копии. Это действие нельзя отменить.</string>
<string name="restore_database_alert_confirm">Восстановить</string>
<string name="database_restore_error">Ошибка при восстановлении базы данных</string>
<string name="restore_passphrase_not_found_desc">Пароль не найден в Keystore, пожалуйста, введите его вручную. Это могло произойти, если вы восстановили данные приложения с помощью инструмента резервного копирования. Если это не так, пожалуйста, свяжитесь с разработчиками.</string>
<!-- ChatModel.chatRunning interactions -->
<string name="chat_is_stopped_indication">Чат остановлен</string>
@@ -710,14 +729,27 @@
<string name="rcv_group_event_member_added">пригласил(а) <xliff:g id="member profile" example="alice (Alice)">%1$s</xliff:g></string>
<string name="rcv_group_event_member_connected">соединен(а)</string>
<string name="rcv_group_event_member_left">покинул(а) группу</string>
<string name="rcv_group_event_changed_member_role">поменял(а) роль члена %s на: %s</string>
<string name="rcv_group_event_changed_your_role">поменял(а) вашу роль на: %s</string>
<string name="rcv_group_event_member_deleted">удалил(а) <xliff:g id="member profile" example="alice (Alice)">%1$s</xliff:g></string>
<string name="rcv_group_event_user_deleted">удалил(а) вас из группы</string>
<string name="rcv_group_event_group_deleted">удалил(а) группу</string>
<string name="rcv_group_event_updated_group_profile">обновил(а) профиль группы</string>
<string name="rcv_group_event_invited_via_your_group_link">приглашен(а) через вашу ссылку группы</string>
<string name="snd_group_event_changed_member_role">вы поменяли роль члена %s на: %s</string>
<string name="snd_group_event_changed_role_for_yourself">вы поменяли роль себе на: %s</string>
<string name="snd_group_event_member_deleted">вы удалили <xliff:g id="member profile" example="alice (Alice)">%1$s</xliff:g></string>
<string name="snd_group_event_user_left">вы покинули группу</string>
<string name="snd_group_event_group_profile_updated">профиль группы обновлен</string>
<!-- Conn event chat items -->
<string name="rcv_conn_event_switch_queue_phase_completed">поменял(а) адрес для вас</string>
<string name="rcv_conn_event_switch_queue_phase_changing">смена адреса…</string>
<string name="snd_conn_event_switch_queue_phase_completed_for_member">вы поменяли адрес для %s</string>
<string name="snd_conn_event_switch_queue_phase_changing_for_member">смена адреса для %s…</string>
<string name="snd_conn_event_switch_queue_phase_completed">вы поменяли адрес</string>
<string name="snd_conn_event_switch_queue_phase_changing">смена адреса…</string>
<!-- GroupMemberRole -->
<string name="group_member_role_member">член группы</string>
<string name="group_member_role_admin">админ</string>
@@ -760,6 +792,14 @@
<string name="delete_group_for_self_cannot_undo_warning">Группа будет удалена для вас - это действие нельзя отменить!</string>
<string name="button_leave_group">Выйти из группы</string>
<string name="button_edit_group_profile">Редактировать профиль группы</string>
<string name="group_link">Ссылка группы</string>
<string name="button_create_group_link">Создать ссылку</string>
<string name="delete_link_question">Удалить ссылку?</string>
<string name="delete_link">Удалить ссылку</string>
<string name="you_can_share_group_link_anybody_will_be_able_to_connect">Вы можете поделиться ссылкой или QR кодом - через них можно присоединиться к группе. Вы сможете удалить ссылку, сохранив членов группы, которые через нее соединились.</string>
<string name="all_group_members_will_remain_connected">Все члены группы, которые соединились через эту ссылку, останутся в группе.</string>
<string name="error_creating_link_for_group">Ошибка при создании ссылки группы</string>
<string name="error_deleting_link_for_group">Ошибка при удалении ссылки группы</string>
<!-- For Console chat info section -->
<string name="section_title_for_console">ДЛЯ КОНСОЛИ</string>
@@ -775,13 +815,12 @@
<string name="role_in_group">Роль</string>
<string name="change_role">Поменять роль</string>
<string name="change_verb">Поменять</string>
<string name="switch_verb">Переключить</string>
<string name="change_member_role_question">Поменять роль в группе?</string>
<string name="member_role_will_be_changed_with_notification">Роль будет изменена на \"%s\". Все в группе получат сообщение.</string>
<string name="member_role_will_be_changed_with_invitation">Роль будет изменена на \"%s\". Будет отправлено новое приглашение.</string>
<string name="error_removing_member">Ошибка при удалении члена группы</string>
<string name="error_changing_role">Ошибка при изменении роли</string>
<string name="member_role">роль %s: %s</string>
<string name="your_member_role">ваша роль: %s</string>
<string name="info_row_group">Группа</string>
<string name="info_row_connection">Соединение</string>
<string name="conn_level_desc_direct">прямое</string>
@@ -792,6 +831,7 @@
<string name="receiving_via">Получение через</string>
<string name="sending_via">Отправка через</string>
<string name="network_status">Состояние сети</string>
<string name="switch_receiving_address">Переключить адрес получения (BETA)</string>
<!-- AddGroupView.kt -->
<string name="create_secret_group_title">Создать скрытую группу</string>
@@ -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 -->
@@ -32,6 +34,8 @@
<string name="display_name_connecting">connecting…</string>
<string name="description_you_shared_one_time_link">you shared one-time link</string>
<string name="description_you_shared_one_time_link_incognito">you shared one-time link incognito</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">via contact address link</string>
<string name="description_via_contact_address_link_incognito">incognito via contact address link</string>
<string name="description_via_one_time_link">via one-time link</string>
@@ -54,19 +58,18 @@
<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>
<string name="connection_error_auth_desc">Unless your contact deleted the connection or this link was already used, it might be a bug - please report it.\nTo connect, please ask your contact to create another connection link and check that you have a stable network connection.</string>
<string name="error_accepting_contact_request">Error accepting contact request</string>
<string name="sender_may_have_deleted_the_connection_request">Sender may have deleted the connection request.</string>
<string name="cannot_delete_contact">Can\'t delete contact!</string>
<string name="contact_cannot_be_deleted_as_they_are_in_groups">Contact <xliff:g id="contactName" example="Jane Doe">%1$s!</xliff:g> cannot be deleted, they are a member of the group(s) <xliff:g id="groups" example="[team, chess club]">%2$s</xliff:g>.</string>
<string name="error_deleting_contact">Error deleting contact</string>
<string name="error_deleting_group">Error deleting group</string>
<string name="error_deleting_contact_request">Error deleting contact request</string>
<string name="error_deleting_pending_contact_connection">Error deleting pending contact connection</string>
<string name="error_changing_address">Error changing address</string>
<!-- background service notice - SimpleXAPI.kt -->
<string name="icon_descr_instant_notifications">Instant notifications</string>
@@ -128,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>
@@ -182,8 +185,10 @@
<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 - CIImageView.kt -->
<!-- Images - chat.simplex.app.views.chat.item.CIImageView.kt -->
<string name="image_descr">Image</string>
<string name="icon_descr_waiting_for_image">Waiting for image</string>
<string name="icon_descr_asked_to_receive">Asked to receive the image</string>
@@ -215,6 +220,8 @@
<string name="icon_descr_server_status_disconnected">Disconnected</string>
<string name="icon_descr_server_status_error">Error</string>
<string name="icon_descr_server_status_pending">Pending</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>
<!-- Message Actions - SendMsgView.kt -->
<string name="icon_descr_send_message">Send Message</string>
@@ -273,6 +280,7 @@
<string name="delete_contact_menu_action">Delete</string>
<string name="delete_group_menu_action">Delete</string>
<string name="mark_read">Mark read</string>
<string name="mark_unread">Mark unread</string>
<string name="set_contact_name">Set contact name</string>
<!-- Actions - ChatListNavLinkView.kt -->
@@ -319,6 +327,7 @@
<string name="invalid_contact_link">Invalid link!</string>
<string name="this_link_is_not_a_valid_connection_link">This link is not a valid connection link!</string>
<string name="connection_request_sent">Connection request sent!</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">You will be connected when your connection request is accepted, please wait or check later!</string>
<string name="you_will_be_connected_when_your_contacts_device_is_online">You will be connected when your contact\'s device is online, please wait or check later!</string>
<string name="show_QR_code_for_your_contact_to_scan_from_the_app__multiline">Your contact can scan QR code from the app.</string>
@@ -349,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>
@@ -394,12 +406,16 @@
<string name="create_address">Create address</string>
<string name="delete_address__question">Delete address?</string>
<string name="all_your_contacts_will_remain_connected">All your contacts will remain connected.</string>
<string name="you_can_share_your_address_anybody_will_be_able_to_connect">You can share your address as a link or as a QR code - anybody will be able to connect to you.</string>
<string name="if_you_later_delete_address_you_wont_lose_contacts">If you later delete it - you won\'t lose your contacts made via the address.</string>
<string name="if_you_delete_address_you_wont_lose_contacts">If you delete it - you won\'t lose your contacts made via this address.</string>
<string name="you_can_share_your_address_anybody_will_be_able_to_connect">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.</string>
<string name="share_link">Share link</string>
<string name="delete_address">Delete address</string>
<!-- AcceptRequestsView.kt -->
<string name="contact_requests">Contact requests</string>
<string name="accept_requests">Accept requests</string>
<string name="accept_automatically">Automatically</string>
<string name="section_title_welcome_message">WELCOME MESSAGE</string>
<!-- User profile details - UserProfileView.kt -->
<string name="display_name__field">Display name:</string>
<string name="full_name__field">"Full name:</string>
@@ -544,12 +560,14 @@
<string name="privacy_and_security">Privacy &amp; security</string>
<string name="your_privacy">Your privacy</string>
<string name="auto_accept_images">Auto-accept images</string>
<string name="transfer_images_faster">Transfer images faster (BETA)</string>
<string name="send_link_previews">Send link previews</string>
<!-- Settings sections -->
<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>
@@ -664,6 +682,7 @@
<string name="restore_database_alert_desc">Please enter the previous password after restoring database backup. This action can not be undone.</string>
<string name="restore_database_alert_confirm">Restore</string>
<string name="database_restore_error">Restore database error</string>
<string name="restore_passphrase_not_found_desc">Passphrase not found in Keystore, please enter it manually. This may have happened if you restored the app\'s data using a backup tool. If it\'s not the case, please, contact developers.</string>
<!-- ChatModel.chatRunning interactions -->
<string name="chat_is_stopped_indication">Chat is stopped</string>
@@ -710,14 +729,27 @@
<string name="rcv_group_event_member_added">invited <xliff:g id="member profile" example="alice (Alice)">%1$s</xliff:g></string>
<string name="rcv_group_event_member_connected">connected</string>
<string name="rcv_group_event_member_left">left</string>
<string name="rcv_group_event_changed_member_role">changed role of %s to %s</string>
<string name="rcv_group_event_changed_your_role">changed your role to %s</string>
<string name="rcv_group_event_member_deleted">removed <xliff:g id="member profile" example="alice (Alice)">%1$s</xliff:g></string>
<string name="rcv_group_event_user_deleted">removed you</string>
<string name="rcv_group_event_group_deleted">deleted group</string>
<string name="rcv_group_event_updated_group_profile">updated group profile</string>
<string name="rcv_group_event_invited_via_your_group_link">invited via your group link</string>
<string name="snd_group_event_changed_member_role">you changed role of %s to %s</string>
<string name="snd_group_event_changed_role_for_yourself">you changed role for yourself to %s</string>
<string name="snd_group_event_member_deleted">you removed <xliff:g id="member profile" example="alice (Alice)">%1$s</xliff:g></string>
<string name="snd_group_event_user_left">you left</string>
<string name="snd_group_event_group_profile_updated">group profile updated</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>
<!-- GroupMemberRole -->
<string name="group_member_role_member">member</string>
<string name="group_member_role_admin">admin</string>
@@ -760,6 +792,14 @@
<string name="delete_group_for_self_cannot_undo_warning">Group will be deleted for you - this cannot be undone!</string>
<string name="button_leave_group">Leave group</string>
<string name="button_edit_group_profile">Edit group profile</string>
<string name="group_link">Group link</string>
<string name="button_create_group_link">Create link</string>
<string name="delete_link_question">Delete link?</string>
<string name="delete_link">Delete link</string>
<string name="you_can_share_group_link_anybody_will_be_able_to_connect">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.</string>
<string name="all_group_members_will_remain_connected">All group members will remain connected.</string>
<string name="error_creating_link_for_group">Error creating group link</string>
<string name="error_deleting_link_for_group">Error deleting group link</string>
<!-- For Console chat info section -->
<string name="section_title_for_console">FOR CONSOLE</string>
@@ -775,13 +815,12 @@
<string name="role_in_group">Role</string>
<string name="change_role">Change role</string>
<string name="change_verb">Change</string>
<string name="switch_verb">Switch</string>
<string name="change_member_role_question">Change group role?</string>
<string name="member_role_will_be_changed_with_notification">The role will be changed to \"%s\". Everyone in the group will be notified.</string>
<string name="member_role_will_be_changed_with_invitation">The role will be changed to \"%s\". The member will receive a new invitation.</string>
<string name="error_removing_member">Error removing member</string>
<string name="error_changing_role">Error changing role</string>
<string name="member_role">member %s role: %s</string>
<string name="your_member_role">your role: %s</string>
<string name="info_row_group">Group</string>
<string name="info_row_connection">Connection</string>
<string name="conn_level_desc_direct">direct</string>
@@ -792,6 +831,7 @@
<string name="receiving_via">Receiving via</string>
<string name="sending_via">Sending via</string>
<string name="network_status">Network status</string>
<string name="switch_receiving_address">Switch receiving address (BETA)</string>
<!-- AddGroupView.kt -->
<string name="create_secret_group_title">Create secret group</string>
+2
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()
+1
View File
@@ -70,6 +70,7 @@ struct ContentView: View {
dismissAllSheets(animated: false) {
justAuthenticate()
}
chatModel.chatId = nil
}
}
+27 -5
View File
@@ -30,7 +30,7 @@ final class ChatModel: ObservableObject {
@Published var groupMembers: [GroupMember] = []
// items in the terminal view
@Published var terminalItems: [TerminalItem] = []
@Published var userAddress: String?
@Published var userAddress: UserContactLink?
@Published var userSMPServers: [String]?
@Published var chatItemTTL: ChatItemTTL = .none
@Published var appOpenUrl: URL?
@@ -98,7 +98,7 @@ final class ChatModel: ObservableObject {
}
func updateContact(_ contact: Contact) {
updateChat(.direct(contact: contact), addMissing: !contact.isIndirectContact)
updateChat(.direct(contact: contact), addMissing: !contact.isIndirectContact && !contact.viaGroupLink)
}
func updateGroup(_ groupInfo: GroupInfo) {
@@ -113,6 +113,17 @@ final class ChatModel: ObservableObject {
}
}
private func _updateChat(_ id: ChatId, _ update: @escaping (Chat) -> Void) {
if let i = getChatIndex(id) {
// we need to separately update the chat object, as it is ObservedObject,
// and chat in the list so the list view is updated...
// simply updating chats[i] replaces the object without updating the current object in the list
let chat = chats[i]
update(chat)
chats[i] = chat
}
}
func updateNetworkStatus(_ id: ChatId, _ status: Chat.NetworkStatus) {
if let i = getChatIndex(id) {
chats[i].serverInfo.networkStatus = status
@@ -257,7 +268,7 @@ final class ChatModel: ObservableObject {
func markChatItemsRead(_ cInfo: ChatInfo) {
// update preview
if let chat = getChat(cInfo.id) {
_updateChat(cInfo.id) { chat in
NtfManager.shared.decNtfBadgeCount(by: chat.chatStats.unreadCount)
chat.chatStats = ChatStats()
}
@@ -282,11 +293,11 @@ final class ChatModel: ObservableObject {
if let cItem = aboveItem {
if chatId == cInfo.id, let i = reversedChatItems.firstIndex(where: { $0.id == cItem.id }) {
markCurrentChatRead(fromIndex: i)
if let chat = getChat(cInfo.id) {
_updateChat(cInfo.id) { chat in
var unreadBelow = 0
var j = i - 1
while j >= 0 {
if case .rcvNew = reversedChatItems[j].meta.itemStatus {
if case .rcvNew = self.reversedChatItems[j].meta.itemStatus {
unreadBelow += 1
}
j -= 1
@@ -303,6 +314,12 @@ final class ChatModel: ObservableObject {
markChatItemsRead(cInfo)
}
}
func markChatUnread(_ cInfo: ChatInfo, unreadChat: Bool = true) {
_updateChat(cInfo.id) { chat in
chat.chatStats.unreadChat = unreadChat
}
}
func clearChat(_ cInfo: ChatInfo) {
// clear preview
@@ -369,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 }) {
+110 -42
View File
@@ -357,6 +357,14 @@ func apiGroupMemberInfo(_ groupId: Int64, _ groupMemberId: Int64) async throws -
throw r
}
func apiSwitchContact(contactId: Int64) async throws {
try await sendCommandOkResp(.apiSwitchContact(contactId: contactId))
}
func apiSwitchGroupMember(_ groupId: Int64, _ groupMemberId: Int64) async throws {
try await sendCommandOkResp(.apiSwitchGroupMember(groupId: groupId, groupMemberId: groupMemberId))
}
func apiAddContact() async -> String? {
let r = await chatSendCmd(.addContact, bgTask: false)
if case let .invitation(connReqInvitation) = r { return connReqInvitation }
@@ -371,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)):
@@ -426,18 +438,10 @@ func deleteChat(_ chat: Chat) async {
DispatchQueue.main.async { ChatModel.shared.removeChat(cInfo.id) }
} catch let error {
logger.error("deleteChat apiDeleteChat error: \(responseError(error))")
switch error as? ChatResponse {
case let .chatCmdError(.error(.contactGroups(contact, groupNames))):
AlertManager.shared.showAlertMsg(
title: "Can't delete contact!",
message: "Contact \(contact.displayName) cannot be deleted, they are a member of the group(s) \(groupNames.joined(separator: ", "))."
)
default:
AlertManager.shared.showAlertMsg(
title: "Error deleting chat!",
message: "Error: \(responseError(error))"
)
}
AlertManager.shared.showAlertMsg(
title: "Error deleting chat!",
message: "Error: \(responseError(error))"
)
}
}
@@ -496,13 +500,20 @@ func apiDeleteUserAddress() async throws {
throw r
}
func apiGetUserAddress() throws -> String? {
func apiGetUserAddress() throws -> UserContactLink? {
let r = chatSendCmdSync(.showMyAddress)
switch r {
case let .userContactLink(connReq):
return connReq
case .chatCmdError(chatError: .errorStore(storeError: .userContactLinkNotFound)):
return nil
case let .userContactLink(contactLink): return contactLink
case .chatCmdError(chatError: .errorStore(storeError: .userContactLinkNotFound)): return nil
default: throw r
}
}
func userAddressAutoAccept(_ autoAccept: AutoAccept?) async throws -> UserContactLink? {
let r = await chatSendCmd(.addressAutoAccept(autoAccept: autoAccept))
switch r {
case let .userContactLinkUpdated(contactLink): return contactLink
case .chatCmdError(chatError: .errorStore(storeError: .userContactLinkNotFound)): return nil
default: throw r
}
}
@@ -537,14 +548,19 @@ func apiChatRead(type: ChatType, id: Int64, itemRange: (Int64, Int64)) async thr
try await sendCommandOkResp(.apiChatRead(type: type, id: id, itemRange: itemRange))
}
func apiChatUnread(type: ChatType, id: Int64, unreadChat: Bool) async throws {
try await sendCommandOkResp(.apiChatUnread(type: type, id: id, unreadChat: unreadChat))
}
func receiveFile(fileId: Int64) async {
if let chatItem = await apiReceiveFile(fileId: fileId) {
let inline = privacyTransferImagesInlineGroupDefault.get()
if let chatItem = await apiReceiveFile(fileId: fileId, inline: inline) {
DispatchQueue.main.async { chatItemSimpleUpdate(chatItem) }
}
}
func apiReceiveFile(fileId: Int64) async -> AChatItem? {
let r = await chatSendCmd(.receiveFile(fileId: fileId))
func apiReceiveFile(fileId: Int64, inline: Bool) async -> AChatItem? {
let r = await chatSendCmd(.receiveFile(fileId: fileId, inline: inline))
let am = AlertManager.shared
if case let .rcvFileAccepted(chatItem) = r { return chatItem }
if case .rcvFileAcceptedSndCancelled = r {
@@ -642,16 +658,31 @@ func apiCallStatus(_ contact: Contact, _ status: String) async throws {
func markChatRead(_ chat: Chat, aboveItem: ChatItem? = nil) async {
do {
let minItemId = chat.chatStats.minUnreadItemId
let itemRange = (minItemId, aboveItem?.id ?? chat.chatItems.last?.id ?? minItemId)
let cInfo = chat.chatInfo
try await apiChatRead(type: cInfo.chatType, id: cInfo.apiId, itemRange: itemRange)
DispatchQueue.main.async { ChatModel.shared.markChatItemsRead(cInfo, aboveItem: aboveItem) }
if chat.chatStats.unreadCount > 0 {
let minItemId = chat.chatStats.minUnreadItemId
let itemRange = (minItemId, aboveItem?.id ?? chat.chatItems.last?.id ?? minItemId)
let cInfo = chat.chatInfo
try await apiChatRead(type: cInfo.chatType, id: cInfo.apiId, itemRange: itemRange)
await MainActor.run { ChatModel.shared.markChatItemsRead(cInfo, aboveItem: aboveItem) }
}
if chat.chatStats.unreadChat {
await markChatUnread(chat, unreadChat: false)
}
} catch {
logger.error("markChatRead apiChatRead error: \(responseError(error))")
}
}
func markChatUnread(_ chat: Chat, unreadChat: Bool = true) async {
do {
let cInfo = chat.chatInfo
try await apiChatUnread(type: cInfo.chatType, id: cInfo.apiId, unreadChat: unreadChat)
await MainActor.run { ChatModel.shared.markChatUnread(cInfo, unreadChat: unreadChat) }
} catch {
logger.error("markChatUnread apiChatUnread error: \(responseError(error))")
}
}
func apiMarkChatItemRead(_ cInfo: ChatInfo, _ cItem: ChatItem) async {
do {
logger.debug("apiMarkChatItemRead: \(cItem.id)")
@@ -689,7 +720,7 @@ enum JoinGroupResult {
func apiJoinGroup(_ groupId: Int64) async throws -> JoinGroupResult {
let r = await chatSendCmd(.apiJoinGroup(groupId: groupId))
switch r {
case let .userAcceptedGroupSent(groupInfo): return .joined(groupInfo: groupInfo)
case let .userAcceptedGroupSent(groupInfo, _): return .joined(groupInfo: groupInfo)
case .chatCmdError(.errorAgent(.SMP(.AUTH))): return .invitationRemoved
case .chatCmdError(.errorStore(.groupNotFound)): return .groupNotFound
default: throw r
@@ -743,6 +774,29 @@ func apiUpdateGroup(_ groupId: Int64, _ groupProfile: GroupProfile) async throws
throw r
}
func apiCreateGroupLink(_ groupId: Int64) async throws -> String {
let r = await chatSendCmd(.apiCreateGroupLink(groupId: groupId))
if case let .groupLinkCreated(_, connReq) = r { return connReq }
throw r
}
func apiDeleteGroupLink(_ groupId: Int64) async throws {
let r = await chatSendCmd(.apiDeleteGroupLink(groupId: groupId))
if case .groupLinkDeleted = r { return }
throw r
}
func apiGetGroupLink(_ groupId: Int64) throws -> String? {
let r = chatSendCmdSync(.apiGetGroupLink(groupId: groupId))
switch r {
case let .groupLink(_, connReq):
return connReq
case .chatCmdError(chatError: .errorStore(storeError: .groupLinkNotFound)):
return nil
default: throw r
}
}
func initializeChat(start: Bool, dbKey: String? = nil) throws {
logger.debug("initializeChat")
let m = ChatModel.shared
@@ -840,16 +894,20 @@ func processReceivedMsg(_ res: ChatResponse) async {
m.updateContactConnection(connection)
case let .contactConnectionDeleted(connection):
m.removeChat(connection.id)
case let .contactConnected(contact):
m.updateContact(contact)
m.dismissConnReqView(contact.activeConn.id)
m.removeChat(contact.activeConn.id)
m.updateNetworkStatus(contact.id, .connected)
NtfManager.shared.notifyContactConnected(contact)
case let .contactConnected(contact, _):
if !contact.viaGroupLink {
m.updateContact(contact)
m.dismissConnReqView(contact.activeConn.id)
m.removeChat(contact.activeConn.id)
m.updateNetworkStatus(contact.id, .connected)
NtfManager.shared.notifyContactConnected(contact)
}
case let .contactConnecting(contact):
m.updateContact(contact)
m.dismissConnReqView(contact.activeConn.id)
m.removeChat(contact.activeConn.id)
if !contact.viaGroupLink {
m.updateContact(contact)
m.dismissConnReqView(contact.activeConn.id)
m.removeChat(contact.activeConn.id)
}
case let .receivedContactRequest(contactRequest):
let cInfo = ChatInfo.contactRequest(contactRequest: contactRequest)
if m.hasChat(contactRequest.id) {
@@ -866,6 +924,13 @@ func processReceivedMsg(_ res: ChatResponse) async {
if m.hasChat(toContact.id) {
m.updateChatInfo(cInfo)
}
case let .contactsMerged(intoContact, mergedContact):
if m.hasChat(mergedContact.id) {
if m.chatId == mergedContact.id {
m.chatId = intoContact.id
}
m.removeChat(mergedContact.id)
}
case let .contactsSubscribed(_, contactRefs):
updateContactsStatus(contactRefs, status: .connected)
case let .contactsDisconnected(_, contactRefs):
@@ -925,11 +990,14 @@ 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)
if let hostContact = hostContact {
m.dismissConnReqView(hostContact.activeConn.id)
m.removeChat(hostContact.activeConn.id)
}
case let .joinedGroupMemberConnecting(groupInfo, _, member):
_ = m.upsertGroupMember(groupInfo, member)
case let .deletedMemberUser(groupInfo, _): // TODO update user member
@@ -957,9 +1025,9 @@ func processReceivedMsg(_ res: ChatResponse) async {
case let .sndFileComplete(aChatItem, _):
chatItemSimpleUpdate(aChatItem)
let cItem = aChatItem.chatItem
let mc = cItem.content.msgContent
if aChatItem.chatInfo.chatType == .direct,
let mc = cItem.content.msgContent,
mc.isFile(),
case .file = mc,
let fileName = cItem.file?.filePath {
removeFile(fileName)
}
+59 -22
View File
@@ -30,7 +30,14 @@ func localizedInfoRow(_ title: LocalizedStringKey, _ value: LocalizedStringKey)
@ViewBuilder func smpServers(_ title: LocalizedStringKey, _ servers: [String]?) -> some View {
if let servers = servers,
servers.count > 0 {
infoRow(title, serverHost(servers[0]))
HStack {
Text(title).frame(width: 120, alignment: .leading)
Button(serverHost(servers[0])) {
UIPasteboard.general.string = servers.joined(separator: ";")
}
.foregroundColor(.secondary)
.lineLimit(1)
}
}
}
@@ -47,7 +54,7 @@ struct ChatInfoView: View {
@Environment(\.dismiss) var dismiss: DismissAction
@ObservedObject var chat: Chat
var contact: Contact
var connectionStats: ConnectionStats?
@Binding var connectionStats: ConnectionStats?
var customUserProfile: Profile?
@State var localAlias: String
@FocusState private var aliasTextFieldFocused: Bool
@@ -56,16 +63,18 @@ struct ChatInfoView: View {
enum ChatInfoViewAlert: Identifiable {
case deleteContactAlert
case contactGroupsAlert(groupNames: [GroupName])
case clearChatAlert
case networkStatusAlert
case switchAddressAlert
case error(title: LocalizedStringKey, error: LocalizedStringKey = "")
var id: String {
switch self {
case .deleteContactAlert: return "deleteContactAlert"
case .contactGroupsAlert: return "contactGroupsAlert"
case .clearChatAlert: return "clearChatAlert"
case .networkStatusAlert: return "networkStatusAlert"
case .switchAddressAlert: return "switchAddressAlert"
case let .error(title, _): return "error \(title)"
}
}
}
@@ -90,12 +99,17 @@ struct ChatInfoView: View {
}
}
if let connStats = connectionStats {
Section("Servers") {
networkStatusRow()
.onTapGesture {
alert = .networkStatusAlert
}
Section("Servers") {
networkStatusRow()
.onTapGesture {
alert = .networkStatusAlert
}
if developerTools {
Button("Change receiving address (BETA)") {
alert = .switchAddressAlert
}
}
if let connStats = connectionStats {
smpServers("Receiving via", connStats.rcvServers)
smpServers("Sending via", connStats.sndServers)
}
@@ -119,9 +133,10 @@ struct ChatInfoView: View {
.alert(item: $alert) { alertItem in
switch(alertItem) {
case .deleteContactAlert: return deleteContactAlert()
case let .contactGroupsAlert(groupNames): return contactGroupsAlert(groupNames)
case .clearChatAlert: return clearChatAlert()
case .networkStatusAlert: return networkStatusAlert()
case .switchAddressAlert: return switchAddressAlert(switchContactAddress)
case let .error(title, error): return mkAlert(title: title, message: error)
}
}
}
@@ -229,9 +244,10 @@ struct ChatInfoView: View {
dismiss()
}
} catch let error {
logger.error("deleteContactAlert apiDeleteChat error: \(error.localizedDescription)")
if case let .chatCmdError(.error(.contactGroups(_, groupNames))) = error as? ChatResponse {
alert = .contactGroupsAlert(groupNames: groupNames)
logger.error("deleteContactAlert apiDeleteChat error: \(responseError(error))")
let a = getErrorAlert(error, "Error deleting contact")
await MainActor.run {
alert = .error(title: a.title, error: a.message)
}
}
}
@@ -240,13 +256,6 @@ struct ChatInfoView: View {
)
}
private func contactGroupsAlert(_ groupNames: [GroupName]) -> Alert {
Alert(
title: Text("Can't delete contact!"),
message: Text("Contact \(contact.displayName) cannot be deleted, they are a member of the group(s) \(groupNames.joined(separator: ", ")).")
)
}
private func clearChatAlert() -> Alert {
Alert(
title: Text("Clear conversation?"),
@@ -267,10 +276,38 @@ struct ChatInfoView: View {
message: Text(chat.serverInfo.networkStatus.statusExplanation)
)
}
private func switchContactAddress() {
Task {
do {
try await apiSwitchContact(contactId: contact.apiId)
} catch let error {
logger.error("switchContactAddress apiSwitchContact error: \(responseError(error))")
let a = getErrorAlert(error, "Error changing address")
await MainActor.run {
alert = .error(title: a.title, error: a.message)
}
}
}
}
}
func switchAddressAlert(_ switchAddress: @escaping () -> Void) -> Alert {
Alert(
title: Text("Change receiving address?"),
message: Text("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)."),
primaryButton: .destructive(Text("Change"), action: switchAddress),
secondaryButton: .cancel()
)
}
struct ChatInfoView_Previews: PreviewProvider {
static var previews: some View {
ChatInfoView(chat: Chat(chatInfo: ChatInfo.sampleData.direct, chatItems: []), contact: Contact.sampleData, localAlias: "")
ChatInfoView(
chat: Chat(chatInfo: ChatInfo.sampleData.direct, chatItems: []),
contact: Contact.sampleData,
connectionStats: Binding.constant(nil),
localAlias: ""
)
}
}
@@ -1,5 +1,5 @@
//
// CIGroupEventView.swift
// CIEventView.swift
// SimpleX (iOS)
//
// Created by JRoberts on 20.07.2022.
@@ -9,7 +9,7 @@
import SwiftUI
import SimpleXChat
struct CIGroupEventView: View {
struct CIEventView: View {
var chatItem: ChatItem
var body: some View {
@@ -43,8 +43,8 @@ struct CIGroupEventView: View {
}
}
struct CIGroupEventView_Previews: PreviewProvider {
struct CIEventView_Previews: PreviewProvider {
static var previews: some View {
CIGroupEventView(chatItem: ChatItem.getGroupEventSample())
CIEventView(chatItem: ChatItem.getGroupEventSample())
}
}
@@ -68,14 +68,16 @@ struct FramedItemView: View {
ciMsgContentView (chatItem, showMember)
}
case let .file(text):
CIFileView(file: chatItem.file, edited: chatItem.meta.itemEdited)
.overlay(DetermineWidth())
if text != "" {
ciMsgContentView (chatItem, showMember)
}
ciFileView(chatItem, text)
case let .link(_, preview):
CILinkView(linkPreview: preview)
ciMsgContentView (chatItem, showMember)
case let .unknown(_, text: text):
if chatItem.file == nil {
ciMsgContentView (chatItem, showMember)
} else {
ciFileView(chatItem, text)
}
default:
ciMsgContentView (chatItem, showMember)
}
@@ -191,6 +193,14 @@ struct FramedItemView: View {
v
}
}
@ViewBuilder private func ciFileView(_ ci: ChatItem, _ text: String) -> some View {
CIFileView(file: chatItem.file, edited: chatItem.meta.itemEdited)
.overlay(DetermineWidth())
if text != "" {
ciMsgContentView (chatItem, showMember)
}
}
}
func isRightToLeft(_ s: String) -> Bool {
@@ -207,8 +217,17 @@ private struct MetaColorPreferenceKey: PreferenceKey {
}
}
func onlyImage(_ ci: ChatItem) -> Bool {
if case let .image(text, _) = ci.content.msgContent {
return ci.quotedItem == nil && text == ""
}
return false
}
func chatItemFrameColor(_ ci: ChatItem, _ colorScheme: ColorScheme) -> Color {
ci.chatDir.sent
onlyImage(ci)
? Color.clear
: ci.chatDir.sent
? (colorScheme == .light ? sentColorLight : sentColorDark)
: Color(uiColor: .tertiarySystemGroupedBackground)
}
@@ -27,8 +27,10 @@ struct ChatItemView: View {
case .rcvIntegrityError: IntegrityErrorItemView(chatItem: chatItem, showMember: showMember)
case let .rcvGroupInvitation(groupInvitation, memberRole): groupInvitationItemView(groupInvitation, memberRole)
case let .sndGroupInvitation(groupInvitation, memberRole): groupInvitationItemView(groupInvitation, memberRole)
case .rcvGroupEvent: groupEventItemView()
case .sndGroupEvent: groupEventItemView()
case .rcvGroupEvent: eventItemView()
case .sndGroupEvent: eventItemView()
case .rcvConnEvent: eventItemView()
case .sndConnEvent: eventItemView()
}
}
@@ -52,8 +54,8 @@ struct ChatItemView: View {
CIGroupInvitationView(chatItem: chatItem, groupInvitation: groupInvitation, memberRole: memberRole, chatIncognito: chatInfo.incognito)
}
private func groupEventItemView() -> some View {
CIGroupEventView(chatItem: chatItem)
private func eventItemView() -> some View {
CIEventView(chatItem: chatItem)
}
}
+13 -3
View File
@@ -62,6 +62,13 @@ struct ChatView: View {
.navigationTitle(cInfo.chatViewName)
.navigationBarTitleDisplayMode(.inline)
.navigationBarBackButtonHidden(true)
.onAppear {
if chat.chatStats.unreadChat {
Task {
await markChatUnread(chat, unreadChat: false)
}
}
}
.toolbar {
ToolbarItem(placement: .navigationBarLeading) {
Button {
@@ -100,7 +107,7 @@ struct ChatView: View {
connectionStats = nil
customUserProfile = nil
}) {
ChatInfoView(chat: chat, contact: contact, connectionStats: connectionStats, customUserProfile: customUserProfile, localAlias: chat.chatInfo.localAlias)
ChatInfoView(chat: chat, contact: contact, connectionStats: $connectionStats, customUserProfile: customUserProfile, localAlias: chat.chatInfo.localAlias)
}
} else if case let .group(groupInfo) = cInfo {
Button {
@@ -385,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)
@@ -16,6 +16,7 @@ struct GroupChatInfoView: View {
var groupInfo: GroupInfo
@ObservedObject private var alertManager = AlertManager.shared
@State private var alert: GroupChatInfoViewAlert? = nil
@State private var groupLink: String?
@State private var showAddMembersSheet: Bool = false
@State private var selectedMember: GroupMember? = nil
@State private var showGroupProfile: Bool = false
@@ -43,6 +44,7 @@ struct GroupChatInfoView: View {
Section("\(members.count + 1) members") {
if groupInfo.canAddMembers {
groupLinkButton()
if (chat.chatInfo.incognito) {
Label("Invite members", systemImage: "plus")
.foregroundColor(Color(uiColor: .tertiaryLabel))
@@ -69,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)
@@ -105,7 +110,13 @@ struct GroupChatInfoView: View {
case .clearChatAlert: return clearChatAlert()
case .leaveGroupAlert: return leaveGroupAlert()
case .cantInviteIncognitoAlert: return cantInviteIncognitoAlert()
}
}
.onAppear {
do {
groupLink = try apiGetGroupLink(groupInfo.groupId)
} catch let error {
logger.error("GroupChatInfoView apiGetGroupLink: \(responseError(error))")
}
}
}
@@ -175,6 +186,16 @@ struct GroupChatInfoView: View {
}
}
private func groupLinkButton() -> some View {
NavigationLink {
GroupLinkView(groupId: groupInfo.groupId, groupLink: $groupLink)
.navigationBarTitleDisplayMode(.inline)
} label: {
Label("Group link", systemImage: "link")
.foregroundColor(.accentColor)
}
}
func editGroupButton() -> some View {
Button {
showGroupProfile = true
@@ -0,0 +1,109 @@
//
// GroupLinkView.swift
// SimpleX (iOS)
//
// Created by JRoberts on 15.10.2022.
// Copyright © 2022 SimpleX Chat. All rights reserved.
//
import SwiftUI
import SimpleXChat
struct GroupLinkView: View {
var groupId: Int64
@Binding var groupLink: String?
@State private var alert: GroupLinkAlert?
private enum GroupLinkAlert: Identifiable {
case deleteLink
case error(title: LocalizedStringKey, error: String = "")
var id: String {
switch self {
case .deleteLink: return "deleteLink"
case let .error(title, _): return "error \(title)"
}
}
}
var body: some View {
ScrollView {
VStack (alignment: .leading) {
Text("Group link")
.font(.largeTitle)
.bold()
.padding(.bottom)
Text("You can share a link or a QR code - anybody will be able to join the group. You won't lose members of the group if you later delete it.")
.padding(.bottom)
if let groupLink = groupLink {
QRCode(uri: groupLink)
HStack {
Button {
showShareSheet(items: [groupLink])
} label: {
Label("Share link", systemImage: "square.and.arrow.up")
}
.padding()
Button(role: .destructive) { alert = .deleteLink } label: {
Label("Delete link", systemImage: "trash")
}
.padding()
}
.frame(maxWidth: .infinity)
} else {
Button {
Task {
do {
groupLink = try await apiCreateGroupLink(groupId)
} catch let error {
logger.error("GroupLinkView apiCreateGroupLink: \(responseError(error))")
let a = getErrorAlert(error, "Error creating group link")
alert = .error(title: a.title, error: "\(a.message)")
}
}
} label: { Label("Create link", systemImage: "link.badge.plus") }
.frame(maxWidth: .infinity)
}
}
.padding()
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top)
.alert(item: $alert) { alert in
switch alert {
case .deleteLink:
return Alert(
title: Text("Delete link?"),
message: Text("All group members will remain connected."),
primaryButton: .destructive(Text("Delete")) {
Task {
do {
try await apiDeleteGroupLink(groupId)
await MainActor.run {
groupLink = nil
}
} catch let error {
logger.error("GroupLinkView apiDeleteGroupLink: \(responseError(error))")
}
}
}, secondaryButton: .cancel()
)
case let .error(title, error):
return Alert(title: Text(title), message: Text("\(error)"))
}
}
}
}
}
struct GroupLinkView_Previews: PreviewProvider {
static var previews: some View {
@State var groupLink: String? = "https://simplex.chat/contact#/?v=1&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2FK1rslx-m5bpXVIdMZg9NLUZ_8JBm8xTt%23MCowBQYDK2VuAyEALDeVe-sG8mRY22LsXlPgiwTNs9dbiLrNuA7f3ZMAJ2w%3D"
@State var noGroupLink: String? = nil
return Group {
GroupLinkView(groupId: 1, groupLink: $groupLink)
GroupLinkView(groupId: 1, groupLink: $noGroupLink)
}
}
}
@@ -13,21 +13,23 @@ struct GroupMemberInfoView: View {
@EnvironmentObject var chatModel: ChatModel
@Environment(\.dismiss) var dismiss: DismissAction
var groupInfo: GroupInfo
@State var member: GroupMember
var connectionStats: ConnectionStats?
@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)"
}
}
@@ -35,76 +37,78 @@ 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)
// }
// }
// .onAppear { newRole = member.memberRole }
// .onChange(of: newRole) { _ in
// if newRole != member.memberRole {
// alert = .changeMemberRoleAlert(role: newRole)
// }
// }
if let roles = member.canChangeRoleTo(groupInfo: groupInfo) {
Picker("Change role", selection: $newRole) {
ForEach(roles) { role in
Text(role.text)
}
}
} else {
infoRow("Role", member.memberRole.text)
}
// TODO invited by - need to get contact by contact id
if let conn = member.activeConn {
let connLevelDesc = conn.connLevel == 0 ? NSLocalizedString("direct", comment: "connection level description") : String.localizedStringWithFormat(NSLocalizedString("indirect (%d)", comment: "connection level description"), conn.connLevel)
infoRow("Connection", connLevelDesc)
// TODO invited by - need to get contact by contact id
if let conn = member.activeConn {
let connLevelDesc = conn.connLevel == 0 ? NSLocalizedString("direct", comment: "connection level description") : String.localizedStringWithFormat(NSLocalizedString("indirect (%d)", comment: "connection level description"), conn.connLevel)
infoRow("Connection", connLevelDesc)
}
}
}
if let connStats = connectionStats {
Section("Servers") {
// TODO network connection status
smpServers("Receiving via", connStats.rcvServers)
smpServers("Sending via", connStats.sndServers)
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)
}
}
if developerTools {
Section("For console") {
infoRow("Local name", member.localDisplayName)
infoRow("Database ID", "\(member.groupMemberId)")
}
}
}
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))
}
}
@@ -134,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)
}
@@ -153,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 {
@@ -185,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)
@@ -206,14 +211,34 @@ struct GroupMemberInfoView: View {
}
},
secondaryButton: .cancel {
newRole = member.memberRole
newRole = mem.memberRole
}
)
}
private func switchMemberAddress() {
Task {
do {
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")
await MainActor.run {
alert = .error(title: a.title, error: a.message)
}
}
}
}
}
struct GroupMemberInfoView_Previews: PreviewProvider {
static var previews: some View {
GroupMemberInfoView(groupInfo: GroupInfo.sampleData, member: GroupMember.sampleData)
GroupMemberInfoView(
groupInfo: GroupInfo.sampleData,
member: Binding.constant(GroupMember.sampleData),
connectionStats: Binding.constant(nil)
)
}
}
@@ -53,9 +53,7 @@ struct ChatListNavLink: View {
disabled: !contact.ready
)
.swipeActions(edge: .leading, allowsFullSwipe: true) {
if chat.chatStats.unreadCount > 0 {
markReadButton()
}
markReadButton()
}
.swipeActions(edge: .trailing, allowsFullSwipe: true) {
if !chat.chatItems.isEmpty {
@@ -89,7 +87,7 @@ struct ChatListNavLink: View {
ChatPreviewView(chat: chat)
.frame(height: rowHeights[dynamicTypeSize])
.swipeActions(edge: .trailing, allowsFullSwipe: true) {
joinGroupButton(groupInfo.hostConnCustomUserProfileId)
joinGroupButton()
if groupInfo.canDelete {
deleteGroupChatButton(groupInfo)
}
@@ -116,9 +114,7 @@ struct ChatListNavLink: View {
)
.frame(height: rowHeights[dynamicTypeSize])
.swipeActions(edge: .leading, allowsFullSwipe: true) {
if chat.chatStats.unreadCount > 0 {
markReadButton()
}
markReadButton()
}
.swipeActions(edge: .trailing, allowsFullSwipe: true) {
if !chat.chatItems.isEmpty {
@@ -141,7 +137,7 @@ struct ChatListNavLink: View {
}
}
private func joinGroupButton(_ hostConnCustomUserProfileId: Int64?) -> some View {
private func joinGroupButton() -> some View {
Button {
joinGroup(chat.chatInfo.apiId)
} label: {
@@ -150,13 +146,23 @@ struct ChatListNavLink: View {
.tint(chat.chatInfo.incognito ? .indigo : .accentColor)
}
private func markReadButton() -> some View {
Button {
Task { await markChatRead(chat) }
} label: {
Label("Read", systemImage: "checkmark")
@ViewBuilder private func markReadButton() -> some View {
if chat.chatStats.unreadCount > 0 || chat.chatStats.unreadChat {
Button {
Task { await markChatRead(chat) }
} label: {
Label("Read", systemImage: "checkmark")
}
.tint(Color.accentColor)
} else {
Button {
Task { await markChatUnread(chat) }
} label: {
Label("Unread", systemImage: "circlebadge.fill")
}
.tint(Color.accentColor)
}
.tint(Color.accentColor)
}
private func clearChatButton() -> some View {
@@ -17,7 +17,6 @@ struct ChatPreviewView: View {
var body: some View {
let cItem = chat.chatItems.last
let unread = chat.chatStats.unreadCount
return HStack(spacing: 8) {
ZStack(alignment: .bottomTrailing) {
ChatInfoImage(chat: chat)
@@ -41,7 +40,7 @@ struct ChatPreviewView: View {
.padding(.horizontal, 8)
ZStack(alignment: .topTrailing) {
chatPreviewText(cItem, unread)
chatPreviewText(cItem)
if case .direct = chat.chatInfo {
chatStatusImage()
.padding(.top, 24)
@@ -100,7 +99,7 @@ struct ChatPreviewView: View {
}
}
@ViewBuilder private func chatPreviewText(_ cItem: ChatItem?, _ unread: Int) -> some View {
@ViewBuilder private func chatPreviewText(_ cItem: ChatItem?) -> some View {
if let cItem = cItem {
ZStack(alignment: .topTrailing) {
(itemStatusMark(cItem) + messageText(cItem.text, cItem.formattedText, cItem.memberDisplayName, preview: true))
@@ -109,8 +108,9 @@ struct ChatPreviewView: View {
.frame(maxWidth: .infinity, alignment: .topLeading)
.padding(.leading, 8)
.padding(.trailing, 36)
if unread > 0 {
unreadCountText(unread)
let s = chat.chatStats
if s.unreadCount > 0 || s.unreadChat {
unreadCountText(s.unreadCount)
.font(.caption)
.foregroundColor(.white)
.padding(.horizontal, 4)
@@ -185,7 +185,7 @@ struct ChatPreviewView: View {
}
func unreadCountText(_ n: Int) -> Text {
Text(n > 999 ? "\(n / 1000)k" : "\(n)")
Text(n > 999 ? "\(n / 1000)k" : n > 0 ? "\(n)" : "")
}
struct ChatPreviewView_Previews: PreviewProvider {
@@ -14,7 +14,6 @@ struct ContactConnectionInfo: View {
@Environment(\.dismiss) var dismiss: DismissAction
@State var contactConnection: PendingContactConnection
@State private var alert: CCInfoAlert?
@State private var showQRCodeView = false
@State private var localAlias = ""
@FocusState private var aliasTextFieldFocused: Bool
@@ -47,22 +46,27 @@ struct ContactConnectionInfo: View {
.onTapGesture { aliasTextFieldFocused = false }
Section {
HStack(spacing: 20) {
Image(systemName: "pencil")
.foregroundColor(.secondary)
.padding(.leading, 6)
.onTapGesture { aliasTextFieldFocused = true }
TextField("Set contact name…", text: $localAlias)
.autocapitalization(.none)
.autocorrectionDisabled(true)
.focused($aliasTextFieldFocused)
.submitLabel(.done)
.onSubmit(setConnectionAlias)
if contactConnection.groupLinkId == nil {
HStack(spacing: 20) {
Image(systemName: "pencil")
.foregroundColor(.secondary)
.padding(.leading, 6)
.onTapGesture { aliasTextFieldFocused = true }
TextField("Set contact name…", text: $localAlias)
.autocapitalization(.none)
.autocorrectionDisabled(true)
.focused($aliasTextFieldFocused)
.submitLabel(.done)
.onSubmit(setConnectionAlias)
}
}
if contactConnection.initiated && contactConnection.connReqInv != nil {
Button {
showQRCodeView = true
if contactConnection.initiated,
let connReqInv = contactConnection.connReqInv {
NavigationLink {
AddContactView(contactConnection: contactConnection, connReqInvitation: connReqInv)
.navigationTitle(CreateLinkTab.oneTime.title)
.navigationBarTitleDisplayMode(.large)
} label: {
Label("Show QR code", systemImage: "qrcode")
.foregroundColor(contactConnection.incognito ? .indigo : .accentColor)
@@ -79,11 +83,6 @@ struct ContactConnectionInfo: View {
}
.listStyle(.insetGrouped)
}
.sheet(isPresented: $showQRCodeView) {
if let connReqInv = contactConnection.connReqInv {
AddContactView(contactConnection: contactConnection, connReqInvitation: connReqInv)
}
}
.alert(item: $alert) { _alert in
switch _alert {
case .deleteInvitationAlert:
@@ -126,7 +125,10 @@ struct ContactConnectionInfo: View {
private func contactConnectionText(_ contactConnection: PendingContactConnection) -> LocalizedStringKey {
contactConnection.viaContactUri
? "You will be connected when your connection request is accepted, please wait or check later!"
? (contactConnection.groupLinkId != nil
? "You will be connected to group when the group host's device is online, please wait or check later!"
: "You will be connected when your connection request is accepted, please wait or check later!"
)
: "You will be connected when your contact's device is online, please wait or check later!"
}
}
@@ -14,15 +14,10 @@ struct AddContactView: View {
@EnvironmentObject private var chatModel: ChatModel
var contactConnection: PendingContactConnection? = nil
var connReqInvitation: String
var viaSettings = false
var body: some View {
ScrollView {
VStack(alignment: .leading) {
Text("One-time invitation link")
.font(.largeTitle)
.bold()
.padding(viaSettings ? .bottom : .vertical)
Text("Your contact can scan it from the app.")
.padding(.bottom, 4)
if (contactConnection?.incognito ?? chatModel.incognito) {
@@ -61,6 +56,7 @@ struct AddContactView: View {
.padding()
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top)
}
.onAppear { chatModel.connReqInv = connReqInvitation }
}
}
@@ -11,6 +11,13 @@ import SwiftUI
enum CreateLinkTab {
case oneTime
case longTerm
var title: LocalizedStringKey {
switch self {
case .oneTime: return "One-time invitation link"
case .longTerm: return "Your contact address"
}
}
}
struct CreateLinkView: View {
@@ -18,11 +25,21 @@ struct CreateLinkView: View {
@State var selection: CreateLinkTab
@State var connReqInvitation: String = ""
@State private var creatingConnReq = false
var viaSettings = false
var viaNavLink = false
var body: some View {
if viaNavLink {
createLinkView()
} else {
NavigationView {
createLinkView()
}
}
}
private func createLinkView() -> some View {
TabView(selection: $selection) {
AddContactView(connReqInvitation: connReqInvitation, viaSettings: viaSettings)
AddContactView(connReqInvitation: connReqInvitation)
.tabItem {
Label(
connReqInvitation == ""
@@ -32,7 +49,7 @@ struct CreateLinkView: View {
)
}
.tag(CreateLinkTab.oneTime)
UserAddress(viaSettings: viaSettings)
UserAddress()
.tabItem {
Label("Your contact address", systemImage: "infinity.circle")
}
@@ -45,6 +62,8 @@ struct CreateLinkView: View {
}
.onAppear { m.connReqInv = connReqInvitation }
.onDisappear { m.connReqInv = nil }
.navigationTitle(selection.title)
.navigationBarTitleDisplayMode(.large)
}
private func createInvitation() {
@@ -78,6 +78,39 @@ func connectViaLink(_ connectionLink: String, _ dismiss: DismissAction? = nil) {
}
}
struct CReqClientData: Decodable {
var type: String
var groupLinkId: String?
}
func parseLinkQueryData(_ connectionLink: String) -> CReqClientData? {
if let hashIndex = connectionLink.firstIndex(of: "#"),
let urlQuery = URL(string: String(connectionLink[connectionLink.index(after: hashIndex)...])),
let components = URLComponents(url: urlQuery, resolvingAgainstBaseURL: false),
let data = components.queryItems?.first(where: { $0.name == "data" })?.value,
let d = data.data(using: .utf8),
let crData = try? getJSONDecoder().decode(CReqClientData.self, from: d) {
return crData
} else {
return nil
}
}
func checkCRDataGroup(_ crData: CReqClientData) -> Bool {
return crData.type == "group" && crData.groupLinkId != nil
}
func groupLinkAlert(_ connectionLink: String) -> Alert {
return Alert(
title: Text("Connect via group link?"),
message: Text("You will join a group this link refers to and connect to its group members."),
primaryButton: .default(Text("Connect")) {
connectViaLink(connectionLink)
},
secondaryButton: .cancel()
)
}
func connectionReqSentAlert(_ type: ConnReqType) {
AlertManager.shared.showAlertMsg(
title: "Connection request sent!",
@@ -81,7 +81,14 @@ struct PasteToConnectView: View {
}
private func connect() {
connectViaLink(connectionLink.trimmingCharacters(in: .whitespaces), dismiss)
let link = connectionLink.trimmingCharacters(in: .whitespaces)
if let crData = parseLinkQueryData(link),
checkCRDataGroup(crData) {
dismiss()
AlertManager.shared.showAlert(groupLinkAlert(link))
} else {
connectViaLink(link, dismiss)
}
}
}
@@ -52,7 +52,13 @@ struct ScanToConnectView: View {
func processQRCode(_ resp: Result<ScanResult, ScanError>) {
switch resp {
case let .success(r):
Task { connectViaLink(r.string, dismiss) }
if let crData = parseLinkQueryData(r.string),
checkCRDataGroup(crData) {
dismiss()
AlertManager.shared.showAlert(groupLinkAlert(r.string))
} else {
Task { connectViaLink(r.string, dismiss) }
}
case let .failure(e):
logger.error("ConnectContactView.processQRCode QR code error: \(e.localizedDescription)")
dismiss()
@@ -0,0 +1,131 @@
//
// AcceptRequestsView.swift
// SimpleX (iOS)
//
// Created by Evgeny on 23/10/2022.
// Copyright © 2022 SimpleX Chat. All rights reserved.
//
import SwiftUI
import SimpleXChat
struct AcceptRequestsView: View {
@EnvironmentObject private var m: ChatModel
@State var contactLink: UserContactLink
@State private var a = AutoAcceptState()
@State private var saved = AutoAcceptState()
@FocusState private var keyboardVisible: Bool
var body: some View {
List {
Section {
settingsRow("checkmark") {
Toggle("Automatically", isOn: $a.enable)
}
if a.enable {
settingsRow(
a.incognito ? "theatermasks.fill" : "theatermasks",
color: a.incognito ? .indigo : .secondary
) {
Toggle("Incognito", isOn: $a.incognito)
}
}
} header: {
Text("Accept requests")
} footer: {
saveButtons()
}
if a.enable {
Section {
TextEditor(text: $a.welcomeText)
.focused($keyboardVisible)
.padding(.horizontal, -5)
.padding(.top, -8)
.frame(height: 90, alignment: .topLeading)
.frame(maxWidth: .infinity, alignment: .leading)
} header: {
Text("Welcome message")
}
}
}
.onAppear {
a = AutoAcceptState(contactLink: contactLink)
saved = a
}
.onChange(of: a.enable) { _ in
if !a.enable { a = AutoAcceptState() }
}
}
@ViewBuilder private func saveButtons() -> some View {
HStack {
Button {
a = saved
} label: {
Label("Cancel", systemImage: "arrow.counterclockwise")
}
Spacer()
Button {
Task {
do {
if let link = try await userAddressAutoAccept(a.autoAccept) {
contactLink = link
m.userAddress = link
saved = a
}
} catch let error {
logger.error("userAddressAutoAccept error: \(responseError(error))")
}
}
} label: {
Label("Save", systemImage: "checkmark")
}
}
.font(.body)
.disabled(a == saved)
}
private struct AutoAcceptState: Equatable {
var enable = false
var incognito = false
var welcomeText = ""
init(enable: Bool = false, incognito: Bool = false, welcomeText: String = "") {
self.enable = enable
self.incognito = incognito
self.welcomeText = welcomeText
}
init(contactLink: UserContactLink) {
if let aa = contactLink.autoAccept {
enable = true
incognito = aa.acceptIncognito
if let msg = aa.autoReply {
welcomeText = msg.text
} else {
welcomeText = ""
}
} else {
enable = false
incognito = false
welcomeText = ""
}
}
var autoAccept: AutoAccept? {
if enable {
var autoReply: MsgContent? = nil
let s = welcomeText.trimmingCharacters(in: .whitespacesAndNewlines)
if s != "" { autoReply = .text(s) }
return AutoAccept(acceptIncognito: incognito, autoReply: autoReply)
}
return nil
}
}
}
struct AcceptRequestsView_Previews: PreviewProvider {
static var previews: some View {
AcceptRequestsView(contactLink: UserContactLink(connReqContact: ""))
}
}
@@ -12,6 +12,8 @@ import SimpleXChat
struct PrivacySettings: View {
@AppStorage(DEFAULT_PRIVACY_ACCEPT_IMAGES) private var autoAcceptImages = true
@AppStorage(DEFAULT_PRIVACY_LINK_PREVIEWS) private var useLinkPreviews = true
@AppStorage(DEFAULT_DEVELOPER_TOOLS) private var developerTools = false
@AppStorage(GROUP_DEFAULT_PRIVACY_TRANSFER_IMAGES_INLINE, store: groupDefaults) private var transferImagesInline = false
var body: some View {
VStack {
@@ -22,7 +24,14 @@ struct PrivacySettings: View {
Section("Chats") {
settingsRow("photo") {
Toggle("Auto-accept images", isOn: $autoAcceptImages)
.onChange(of: autoAcceptImages) { privacyAcceptImagesGroupDefault.set($0) }
.onChange(of: autoAcceptImages) {
privacyAcceptImagesGroupDefault.set($0)
}
}
if developerTools {
settingsRow("photo.on.rectangle") {
Toggle("Transfer images faster (BETA)", isOn: $transferImagesInline)
}
}
settingsRow("network") {
Toggle("Send link previews", isOn: $useLinkPreviews)
@@ -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?
@@ -93,7 +95,7 @@ struct SettingsView: View {
.disabled(chatModel.chatRunning != true)
NavigationLink {
CreateLinkView(selection: .longTerm, viaSettings: true)
CreateLinkView(selection: .longTerm, viaNavLink: true)
.navigationBarTitleDisplayMode(.inline)
} label: {
settingsRow("qrcode") { Text("Your SimpleX contact address") }
@@ -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")
@@ -12,7 +12,7 @@ import SimpleXChat
struct UserAddress: View {
@EnvironmentObject private var chatModel: ChatModel
@State private var alert: UserAddressAlert?
var viaSettings = false
@State private var showAcceptRequests = false
private enum UserAddressAlert: Identifiable {
case deleteAddress
@@ -29,38 +29,49 @@ struct UserAddress: View {
var body: some View {
ScrollView {
VStack (alignment: .leading) {
Text("Your contact address")
.font(.largeTitle)
.bold()
.padding(viaSettings ? .bottom : .vertical)
Text("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.")
.padding(.bottom)
if let userAdress = chatModel.userAddress {
QRCode(uri: userAdress)
QRCode(uri: userAdress.connReqContact)
HStack {
Button {
showShareSheet(items: [userAdress])
showShareSheet(items: [userAdress.connReqContact])
} label: {
Label("Share link", systemImage: "square.and.arrow.up")
HStack {
Image(systemName: "square.and.arrow.up")
Text("Share link")
}
}
.padding()
Button(role: .destructive) { alert = .deleteAddress } label: {
Label("Delete address", systemImage: "trash")
NavigationLink {
if let contactLink = chatModel.userAddress {
AcceptRequestsView(contactLink: contactLink)
.navigationTitle("Contact requests")
.navigationBarTitleDisplayMode(.large)
}
} label: {
HStack {
Text("Contact requests")
Image(systemName: "chevron.right")
}
}
.padding()
}
.frame(maxWidth: .infinity)
Button(role: .destructive) { alert = .deleteAddress } label: {
Label("Delete address", systemImage: "trash")
}
.frame(maxWidth: .infinity)
} else {
Button {
Task {
do {
let userAddress = try await apiCreateUserAddress()
let connReqContact = try await apiCreateUserAddress()
DispatchQueue.main.async {
chatModel.userAddress = userAddress
chatModel.userAddress = UserContactLink(connReqContact: connReqContact)
}
} catch let error {
logger.error("UserAddress apiCreateUserAddress: \(error.localizedDescription)")
logger.error("UserAddress apiCreateUserAddress: \(responseError(error))")
let a = getErrorAlert(error, "Error creating address")
alert = .error(title: a.title, error: "\(a.message)")
}
@@ -71,6 +82,11 @@ struct UserAddress: View {
}
.padding()
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top)
.sheet(isPresented: $showAcceptRequests) {
if let contactLink = chatModel.userAddress {
AcceptRequestsView(contactLink: contactLink)
}
}
.alert(item: $alert) { alert in
switch alert {
case .deleteAddress:
@@ -85,7 +101,7 @@ struct UserAddress: View {
chatModel.userAddress = nil
}
} catch let error {
logger.error("UserAddress apiDeleteUserAddress: \(error.localizedDescription)")
logger.error("UserAddress apiDeleteUserAddress: \(responseError(error))")
}
}
}, secondaryButton: .cancel()
@@ -101,7 +117,7 @@ struct UserAddress: View {
struct UserAddress_Previews: PreviewProvider {
static var previews: some View {
let chatModel = ChatModel()
chatModel.userAddress = "https://simplex.chat/contact#/?v=1&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2FK1rslx-m5bpXVIdMZg9NLUZ_8JBm8xTt%23MCowBQYDK2VuAyEALDeVe-sG8mRY22LsXlPgiwTNs9dbiLrNuA7f3ZMAJ2w%3D"
chatModel.userAddress = UserContactLink(connReqContact: "https://simplex.chat/contact#/?v=1&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2FK1rslx-m5bpXVIdMZg9NLUZ_8JBm8xTt%23MCowBQYDK2VuAyEALDeVe-sG8mRY22LsXlPgiwTNs9dbiLrNuA7f3ZMAJ2w%3D")
return Group {
UserAddress()
.environmentObject(chatModel)
@@ -179,17 +179,17 @@
</trans-unit>
<trans-unit id="1 day" xml:space="preserve">
<source>1 day</source>
<target>1 Tag</target>
<target>täglich</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="1 month" xml:space="preserve">
<source>1 month</source>
<target>1 Monat</target>
<target>monatlich</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="1 week" xml:space="preserve">
<source>1 week</source>
<target>1 Woche</target>
<target>wöchentlich</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="6" xml:space="preserve">
@@ -253,11 +253,21 @@
<target>Inkognito akzeptieren</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Accept requests" xml:space="preserve">
<source>Accept requests</source>
<target>Anfragen annehmen</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Advanced network settings" xml:space="preserve">
<source>Advanced network settings</source>
<target>Erweiterte Netzwerkeinstellungen</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="All group members will remain connected." xml:space="preserve">
<source>All group members will remain connected.</source>
<target>Alle Gruppenmitglieder bleiben verbunden.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="All messages will be deleted - this cannot be undone! The messages will be deleted ONLY for you." xml:space="preserve">
<source>All messages will be deleted - this cannot be undone! The messages will be deleted ONLY for you.</source>
<target>Alle Nachrichten werden gelöscht - dies kann nicht rückgängig gemacht werden! Die Nachrichten werden NUR bei Ihnen gelöscht.</target>
@@ -313,6 +323,11 @@
<target>Bilder automatisch akzeptieren</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Automatically" xml:space="preserve">
<source>Automatically</source>
<target>Automatisch</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Call already ended!" xml:space="preserve">
<source>Call already ended!</source>
<target>Anruf ist bereits beendet!</target>
@@ -323,11 +338,6 @@
<target>Anrufe</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Can't delete contact!" xml:space="preserve">
<source>Can't delete contact!</source>
<target>Der Kontakt kann nicht gelöscht werden!</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Can't invite contact!" xml:space="preserve">
<source>Can't invite contact!</source>
<target>Kontakt kann nicht eingeladen werden!</target>
@@ -368,6 +378,16 @@
<target>Die Mitgliederrolle ändern?</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Change receiving address (BETA)" xml:space="preserve">
<source>Change receiving address (BETA)</source>
<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>Empfängeradresse wechseln?</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Change role" xml:space="preserve">
<source>Change role</source>
<target>Rolle ändern</target>
@@ -408,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>
@@ -478,6 +493,11 @@
<target>Über den Kontakt-Link verbinden?</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Connect via group link?" xml:space="preserve">
<source>Connect via group link?</source>
<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">
<source>Connect via link</source>
<target>Über einen Link verbinden</target>
@@ -538,11 +558,6 @@
<target>Verbindungszeitüberschreitung</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Contact %@ cannot be deleted, they are a member of the group(s) %@." xml:space="preserve">
<source>Contact %@ cannot be deleted, they are a member of the group(s) %@.</source>
<target>Der Kontakt mit %@ kann nicht gelöscht werden, da er Mitglied einer oder mehrerer dieser Gruppen ist %@.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Contact already exists" xml:space="preserve">
<source>Contact already exists</source>
<target>Der Kontakt ist bereits vorhanden</target>
@@ -573,6 +588,11 @@
<target>Kontaktname</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Contact requests" xml:space="preserve">
<source>Contact requests</source>
<target>Kontaktanfragen</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Copy" xml:space="preserve">
<source>Copy</source>
<target>Kopieren</target>
@@ -588,6 +608,11 @@
<target>Adresse erstellen</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Create link" xml:space="preserve">
<source>Create link</source>
<target>Link erzeugen</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Create one-time invitation link" xml:space="preserve">
<source>Create one-time invitation link</source>
<target>Erstellen Sie einen einmaligen Einladungslink</target>
@@ -801,6 +826,16 @@
<target>Einladung löschen</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Delete link" xml:space="preserve">
<source>Delete link</source>
<target>Link löschen</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Delete link?" xml:space="preserve">
<source>Delete link?</source>
<target>Link löschen?</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Delete message?" xml:space="preserve">
<source>Delete message?</source>
<target>Die Nachricht löschen?</target>
@@ -813,7 +848,7 @@
</trans-unit>
<trans-unit id="Delete messages after" xml:space="preserve">
<source>Delete messages after</source>
<target>Löschen der Nachrichten nach</target>
<target>Löschen der Nachrichten</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Delete old database" xml:space="preserve">
@@ -996,6 +1031,11 @@
<target>Fehler beim Hinzufügen von Mitgliedern</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error changing address" xml:space="preserve">
<source>Error changing address</source>
<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">
<source>Error changing role</source>
<target>Fehler beim Ändern der Rolle</target>
@@ -1016,6 +1056,11 @@
<target>Fehler beim Erzeugen der Gruppe</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error creating group link" xml:space="preserve">
<source>Error creating group link</source>
<target>Fehler beim Erzeugen des Gruppen-Links</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error deleting chat database" xml:space="preserve">
<source>Error deleting chat database</source>
<target>Fehler beim Löschen der Chat-Datenbank</target>
@@ -1031,6 +1076,11 @@
<target>Fehler beim Löschen der Verbindung</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error deleting contact" xml:space="preserve">
<source>Error deleting contact</source>
<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">
<source>Error deleting database</source>
<target>Fehler beim Löschen der Datenbank</target>
@@ -1158,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">
@@ -1166,11 +1216,6 @@
<target>Datei: %@</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Files" xml:space="preserve">
<source>Files</source>
<target>Dateien</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="For console" xml:space="preserve">
<source>For console</source>
<target>Für Konsole</target>
@@ -1221,6 +1266,11 @@
<target>Die Gruppeneinladung ist nicht mehr gültig, sie wurde vom Absender entfernt.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Group link" xml:space="preserve">
<source>Group link</source>
<target>Gruppen-Link</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Group message:" xml:space="preserve">
<source>Group message:</source>
<target>Grppennachricht:</target>
@@ -1308,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">
@@ -1735,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">
@@ -1790,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">
@@ -1833,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>
@@ -2048,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>
@@ -2113,11 +2173,6 @@ Wir werden Serverredundanzen hinzufügen, um verloren gegangene Nachrichten zu v
<target>Einmal-Einladungslink teilen</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Shared one-time link" xml:space="preserve">
<source>Shared one-time link</source>
<target>Geteilter Einmal-Link</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Show QR code" xml:space="preserve">
<source>Show QR code</source>
<target>QR-Code anzeigen</target>
@@ -2198,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>
@@ -2320,17 +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 - Die gesendeten und empfangenen Nachrichten, welche älter als die Ausgewählten sind, 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>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">
@@ -2375,6 +2440,11 @@ Sie werden aufgefordert, die Authentifizierung abzuschließen, bevor diese Funkt
<target>Um sofortige Push-Benachrichtigungen zu unterstützen, muss die Chat-Datenbank migriert werden.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Transfer images faster (BETA)" xml:space="preserve">
<source>Transfer images faster (BETA)</source>
<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">
<source>Trying to connect to the server used to receive messages from this contact (error: %@).</source>
<target>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: %@).</target>
@@ -2437,6 +2507,11 @@ Bitten Sie Ihren Kontakt darum einen weiteren Verbindungs-Link zu erzeugen, um s
<target>Stummschaltung aufheben</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Unread" xml:space="preserve">
<source>Unread</source>
<target>Ungelesen</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Update" xml:space="preserve">
<source>Update</source>
<target>Aktualisieren</target>
@@ -2517,6 +2592,11 @@ Bitten Sie Ihren Kontakt darum einen weiteren Verbindungs-Link zu erzeugen, um s
<target>Willkommen %@!</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Welcome message" xml:space="preserve">
<source>Welcome message</source>
<target>Begrüßungsmeldung</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="When available" xml:space="preserve">
<source>When available</source>
<target>Wenn verfügbar</target>
@@ -2547,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">
@@ -2577,9 +2657,14 @@ Bitten Sie Ihren Kontakt darum einen weiteren Verbindungs-Link zu erzeugen, um s
<target>Über die Geräte-Einstellungen können Sie die Benachrichtigungsvorschau im Sperrbildschirm erlauben.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="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." xml:space="preserve">
<source>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.</source>
<target>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.</target>
<note>No comment provided by engineer.</note>
</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">
@@ -2642,14 +2727,19 @@ Bitten Sie Ihren Kontakt darum einen weiteren Verbindungs-Link zu erzeugen, um s
<target>Sie haben eine Gruppeneinladung gesendet</target>
<note>No comment provided by engineer.</note>
</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>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">
@@ -2657,6 +2747,11 @@ Bitten Sie Ihren Kontakt darum einen weiteren Verbindungs-Link zu erzeugen, um s
<target>Sie müssen sich authentifizieren, wenn Sie die im Hintergrund befindliche App nach 30 Sekunden starten oder fortsetzen.</target>
<note>No comment provided by engineer.</note>
</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>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">
<source>You will stop receiving messages from this group. Chat history will be preserved.</source>
<target>Sie werden von dieser Gruppe keine Nachrichten mehr erhalten. Der Chatverlauf wird beibehalten.</target>
@@ -2781,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>
@@ -2846,6 +2951,31 @@ SimpleX-Server können Ihr Profil nicht einsehen.</target>
<target>Anrufen…</target>
<note>call status</note>
</trans-unit>
<trans-unit id="changed address for you" xml:space="preserve">
<source>changed address for you</source>
<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">
<source>changed role of %1$@ to %2$@</source>
<target>änderte die Rolle von %1$@ auf %2$@</target>
<note>rcv group event chat item</note>
</trans-unit>
<trans-unit id="changed your role to %@" xml:space="preserve">
<source>changed your role to %@</source>
<target>änderte Ihre Rolle auf %@</target>
<note>rcv group event chat item</note>
</trans-unit>
<trans-unit id="changing address for %@..." xml:space="preserve">
<source>changing address for %@...</source>
<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>Wechseln der Adresse ...</target>
<note>chat item text</note>
</trans-unit>
<trans-unit id="colored" xml:space="preserve">
<source>colored</source>
<target>farbig</target>
@@ -2988,7 +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>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">
@@ -2998,7 +3133,7 @@ SimpleX-Server können Ihr Profil nicht einsehen.</target>
</trans-unit>
<trans-unit id="indirect (%d)" xml:space="preserve">
<source>indirect (%d)</source>
<target>Indirekt (%d)</target>
<target>indirekt (%d)</target>
<note>connection level description</note>
</trans-unit>
<trans-unit id="invitation to group %@" xml:space="preserve">
@@ -3013,7 +3148,7 @@ SimpleX-Server können Ihr Profil nicht einsehen.</target>
</trans-unit>
<trans-unit id="invited %@" xml:space="preserve">
<source>invited %@</source>
<target>Sie haben %@ eingeladen</target>
<target>hat %@ eingeladen.</target>
<note>rcv group event chat item</note>
</trans-unit>
<trans-unit id="invited to connect" xml:space="preserve">
@@ -3021,6 +3156,11 @@ SimpleX-Server können Ihr Profil nicht einsehen.</target>
<target>Für eine Verbindung eingeladen</target>
<note>chat list item title</note>
</trans-unit>
<trans-unit id="invited via your group link" xml:space="preserve">
<source>invited via your group link</source>
<target>wurde über Ihren Gruppen-Link eingeladen</target>
<note>rcv group event chat item</note>
</trans-unit>
<trans-unit id="italic" xml:space="preserve">
<source>italic</source>
<target>kursiv</target>
@@ -3033,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">
@@ -3041,15 +3181,9 @@ SimpleX-Server können Ihr Profil nicht einsehen.</target>
<target>Mitglied</target>
<note>member role</note>
</trans-unit>
<trans-unit id="member %@ role: %@" xml:space="preserve">
<source>member %1$@ role: %2$@</source>
<target>Mitglieder %1$@ Rolle: %2$@</target>
<note>rcv group event chat item
snd group event chat item</note>
</trans-unit>
<trans-unit id="member connected" xml:space="preserve">
<source>connected</source>
<target>verbunden</target>
<target>ist der Gruppe beigetreten</target>
<note>rcv group event chat item</note>
</trans-unit>
<trans-unit id="message received" xml:space="preserve">
@@ -3114,12 +3248,12 @@ SimpleX-Server können Ihr Profil nicht einsehen.</target>
</trans-unit>
<trans-unit id="removed %@" xml:space="preserve">
<source>removed %@</source>
<target>Entfernt %@</target>
<target>hat %@ aus der Gruppe entfernt</target>
<note>rcv group event chat item</note>
</trans-unit>
<trans-unit id="removed you" xml:space="preserve">
<source>removed you</source>
<target>hat Sie entfernt</target>
<target>hat Sie aus der Gruppe entfernt</target>
<note>rcv group event chat item</note>
</trans-unit>
<trans-unit id="sec" xml:space="preserve">
@@ -3164,7 +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>über einen Gruppen-Link</target>
<note>chat list item description</note>
</trans-unit>
<trans-unit id="via one-time link" xml:space="preserve">
@@ -3202,6 +3341,26 @@ SimpleX-Server können Ihr Profil nicht einsehen.</target>
<target>Sie sind zur Gruppe eingeladen</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="you changed address" xml:space="preserve">
<source>you changed address</source>
<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>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">
<source>you changed role for yourself to %@</source>
<target>Sie haben Ihre eigene Rolle auf %@ geändert</target>
<note>snd group event chat item</note>
</trans-unit>
<trans-unit id="you changed role of %@ to %@" xml:space="preserve">
<source>you changed role of %1$@ to %2$@</source>
<target>Sie haben die Rolle von %1$@ auf %2$@ geändert</target>
<note>snd group event chat item</note>
</trans-unit>
<trans-unit id="you left" xml:space="preserve">
<source>you left</source>
<target>Sie haben verlassen</target>
@@ -3209,7 +3368,7 @@ SimpleX-Server können Ihr Profil nicht einsehen.</target>
</trans-unit>
<trans-unit id="you removed %@" xml:space="preserve">
<source>you removed %@</source>
<target>Sie haben %@ entfernt</target>
<target>entfernt %@ aus der Gruppe</target>
<note>snd group event chat item</note>
</trans-unit>
<trans-unit id="you shared one-time link" xml:space="preserve">
@@ -3227,12 +3386,6 @@ SimpleX-Server können Ihr Profil nicht einsehen.</target>
<target>Sie: </target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="your role: %@" xml:space="preserve">
<source>your role: %@</source>
<target>Meine Rolle: %@</target>
<note>rcv group event chat item
snd group event chat item</note>
</trans-unit>
<trans-unit id="~strike~" xml:space="preserve">
<source>\~strike~</source>
<target>\~durchstreichen~</target>
@@ -253,11 +253,21 @@
<target>Accept incognito</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Accept requests" xml:space="preserve">
<source>Accept requests</source>
<target>Accept requests</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Advanced network settings" xml:space="preserve">
<source>Advanced network settings</source>
<target>Advanced network settings</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="All group members will remain connected." xml:space="preserve">
<source>All group members will remain connected.</source>
<target>All group members will remain connected.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="All messages will be deleted - this cannot be undone! The messages will be deleted ONLY for you." xml:space="preserve">
<source>All messages will be deleted - this cannot be undone! The messages will be deleted ONLY for you.</source>
<target>All messages will be deleted - this cannot be undone! The messages will be deleted ONLY for you.</target>
@@ -313,6 +323,11 @@
<target>Auto-accept images</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Automatically" xml:space="preserve">
<source>Automatically</source>
<target>Automatically</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Call already ended!" xml:space="preserve">
<source>Call already ended!</source>
<target>Call already ended!</target>
@@ -323,11 +338,6 @@
<target>Calls</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Can't delete contact!" xml:space="preserve">
<source>Can't delete contact!</source>
<target>Can't delete contact!</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Can't invite contact!" xml:space="preserve">
<source>Can't invite contact!</source>
<target>Can't invite contact!</target>
@@ -368,6 +378,16 @@
<target>Change member role?</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Change receiving address (BETA)" xml:space="preserve">
<source>Change receiving address (BETA)</source>
<target>Change receiving address (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>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Change role" xml:space="preserve">
<source>Change role</source>
<target>Change role</target>
@@ -408,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>
@@ -478,6 +493,11 @@
<target>Connect via contact link?</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Connect via group link?" xml:space="preserve">
<source>Connect via group link?</source>
<target>Connect via group link?</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Connect via link" xml:space="preserve">
<source>Connect via link</source>
<target>Connect via link</target>
@@ -538,11 +558,6 @@
<target>Connection timeout</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Contact %@ cannot be deleted, they are a member of the group(s) %@." xml:space="preserve">
<source>Contact %@ cannot be deleted, they are a member of the group(s) %@.</source>
<target>Contact %@ cannot be deleted, they are a member of the group(s) %@.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Contact already exists" xml:space="preserve">
<source>Contact already exists</source>
<target>Contact already exists</target>
@@ -573,6 +588,11 @@
<target>Contact name</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Contact requests" xml:space="preserve">
<source>Contact requests</source>
<target>Contact requests</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Copy" xml:space="preserve">
<source>Copy</source>
<target>Copy</target>
@@ -588,6 +608,11 @@
<target>Create address</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Create link" xml:space="preserve">
<source>Create link</source>
<target>Create link</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Create one-time invitation link" xml:space="preserve">
<source>Create one-time invitation link</source>
<target>Create one-time invitation link</target>
@@ -801,6 +826,16 @@
<target>Delete invitation</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Delete link" xml:space="preserve">
<source>Delete link</source>
<target>Delete link</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Delete link?" xml:space="preserve">
<source>Delete link?</source>
<target>Delete link?</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Delete message?" xml:space="preserve">
<source>Delete message?</source>
<target>Delete message?</target>
@@ -996,6 +1031,11 @@
<target>Error adding member(s)</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error changing address" xml:space="preserve">
<source>Error changing address</source>
<target>Error changing address</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error changing role" xml:space="preserve">
<source>Error changing role</source>
<target>Error changing role</target>
@@ -1016,6 +1056,11 @@
<target>Error creating group</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error creating group link" xml:space="preserve">
<source>Error creating group link</source>
<target>Error creating group link</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error deleting chat database" xml:space="preserve">
<source>Error deleting chat database</source>
<target>Error deleting chat database</target>
@@ -1031,6 +1076,11 @@
<target>Error deleting connection</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error deleting contact" xml:space="preserve">
<source>Error deleting contact</source>
<target>Error deleting contact</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error deleting database" xml:space="preserve">
<source>Error deleting database</source>
<target>Error deleting database</target>
@@ -1166,11 +1216,6 @@
<target>File: %@</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Files" xml:space="preserve">
<source>Files</source>
<target>Files</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="For console" xml:space="preserve">
<source>For console</source>
<target>For console</target>
@@ -1221,6 +1266,11 @@
<target>Group invitation is no longer valid, it was removed by sender.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Group link" xml:space="preserve">
<source>Group link</source>
<target>Group link</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Group message:" xml:space="preserve">
<source>Group message:</source>
<target>Group message:</target>
@@ -1833,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>
@@ -2048,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>
@@ -2113,11 +2173,6 @@ We will be adding server redundancy to prevent lost messages.</target>
<target>Share one-time invitation link</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Shared one-time link" xml:space="preserve">
<source>Shared one-time link</source>
<target>Shared one-time link</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Show QR code" xml:space="preserve">
<source>Show QR code</source>
<target>Show QR code</target>
@@ -2198,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>
@@ -2333,6 +2393,11 @@ We will be adding server redundancy to prevent lost messages.</target>
<target>This action cannot be undone - your profile, contacts, messages and files will be irreversibly lost.</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>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="This group no longer exists." xml:space="preserve">
<source>This group no longer exists.</source>
<target>This group no longer exists.</target>
@@ -2375,6 +2440,11 @@ You will be prompted to complete authentication before this feature is enabled.<
<target>To support instant push notifications the chat database has to be migrated.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Transfer images faster (BETA)" xml:space="preserve">
<source>Transfer images faster (BETA)</source>
<target>Transfer images faster (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">
<source>Trying to connect to the server used to receive messages from this contact (error: %@).</source>
<target>Trying to connect to the server used to receive messages from this contact (error: %@).</target>
@@ -2437,6 +2507,11 @@ To connect, please ask your contact to create another connection link and check
<target>Unmute</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Unread" xml:space="preserve">
<source>Unread</source>
<target>Unread</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Update" xml:space="preserve">
<source>Update</source>
<target>Update</target>
@@ -2517,6 +2592,11 @@ To connect, please ask your contact to create another connection link and check
<target>Welcome %@!</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Welcome message" xml:space="preserve">
<source>Welcome message</source>
<target>Welcome message</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="When available" xml:space="preserve">
<source>When available</source>
<target>When available</target>
@@ -2547,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">
@@ -2577,6 +2657,11 @@ To connect, please ask your contact to create another connection link and check
<target>You can set lock screen notification preview via settings.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="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." xml:space="preserve">
<source>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.</source>
<target>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.</target>
<note>No comment provided by engineer.</note>
</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>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.</target>
@@ -2642,6 +2727,11 @@ To connect, please ask your contact to create another connection link and check
<target>You sent group invitation</target>
<note>No comment provided by engineer.</note>
</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>
<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>You will be connected when your connection request is accepted, please wait or check later!</target>
@@ -2657,6 +2747,11 @@ To connect, please ask your contact to create another connection link and check
<target>You will be required to authenticate when you start or resume the app after 30 seconds in background.</target>
<note>No comment provided by engineer.</note>
</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>
<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">
<source>You will stop receiving messages from this group. Chat history will be preserved.</source>
<target>You will stop receiving messages from this group. Chat history will be preserved.</target>
@@ -2781,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>
@@ -2846,6 +2951,31 @@ SimpleX servers cannot see your profile.</target>
<target>calling…</target>
<note>call status</note>
</trans-unit>
<trans-unit id="changed address for you" xml:space="preserve">
<source>changed address for you</source>
<target>changed address for you</target>
<note>chat item text</note>
</trans-unit>
<trans-unit id="changed role of %@ to %@" xml:space="preserve">
<source>changed role of %1$@ to %2$@</source>
<target>changed role of %1$@ to %2$@</target>
<note>rcv group event chat item</note>
</trans-unit>
<trans-unit id="changed your role to %@" xml:space="preserve">
<source>changed your role to %@</source>
<target>changed your role to %@</target>
<note>rcv group event chat item</note>
</trans-unit>
<trans-unit id="changing address for %@..." xml:space="preserve">
<source>changing address for %@...</source>
<target>changing address for %@...</target>
<note>chat item text</note>
</trans-unit>
<trans-unit id="changing address..." xml:space="preserve">
<source>changing address...</source>
<target>changing address...</target>
<note>chat item text</note>
</trans-unit>
<trans-unit id="colored" xml:space="preserve">
<source>colored</source>
<target>colored</target>
@@ -2991,6 +3121,11 @@ SimpleX servers cannot see your profile.</target>
<target>incognito via contact address 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>
<note>chat list item description</note>
</trans-unit>
<trans-unit id="incognito via one-time link" xml:space="preserve">
<source>incognito via one-time link</source>
<target>incognito via one-time link</target>
@@ -3021,6 +3156,11 @@ SimpleX servers cannot see your profile.</target>
<target>invited to connect</target>
<note>chat list item title</note>
</trans-unit>
<trans-unit id="invited via your group link" xml:space="preserve">
<source>invited via your group link</source>
<target>invited via your group link</target>
<note>rcv group event chat item</note>
</trans-unit>
<trans-unit id="italic" xml:space="preserve">
<source>italic</source>
<target>italic</target>
@@ -3041,12 +3181,6 @@ SimpleX servers cannot see your profile.</target>
<target>member</target>
<note>member role</note>
</trans-unit>
<trans-unit id="member %@ role: %@" xml:space="preserve">
<source>member %1$@ role: %2$@</source>
<target>member %1$@ role: %2$@</target>
<note>rcv group event chat item
snd group event chat item</note>
</trans-unit>
<trans-unit id="member connected" xml:space="preserve">
<source>connected</source>
<target>connected</target>
@@ -3167,6 +3301,11 @@ SimpleX servers cannot see your profile.</target>
<target>via contact address 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>
<note>chat list item description</note>
</trans-unit>
<trans-unit id="via one-time link" xml:space="preserve">
<source>via one-time link</source>
<target>via one-time link</target>
@@ -3202,6 +3341,26 @@ SimpleX servers cannot see your profile.</target>
<target>you are invited to group</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="you changed address" xml:space="preserve">
<source>you changed address</source>
<target>you changed address</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>
<note>chat item text</note>
</trans-unit>
<trans-unit id="you changed role for yourself to %@" xml:space="preserve">
<source>you changed role for yourself to %@</source>
<target>you changed role for yourself to %@</target>
<note>snd group event chat item</note>
</trans-unit>
<trans-unit id="you changed role of %@ to %@" xml:space="preserve">
<source>you changed role of %1$@ to %2$@</source>
<target>you changed role of %1$@ to %2$@</target>
<note>snd group event chat item</note>
</trans-unit>
<trans-unit id="you left" xml:space="preserve">
<source>you left</source>
<target>you left</target>
@@ -3227,12 +3386,6 @@ SimpleX servers cannot see your profile.</target>
<target>you: </target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="your role: %@" xml:space="preserve">
<source>your role: %@</source>
<target>your role: %@</target>
<note>rcv group event chat item
snd group event chat item</note>
</trans-unit>
<trans-unit id="~strike~" xml:space="preserve">
<source>\~strike~</source>
<target>\~strike~</target>
@@ -253,11 +253,21 @@
<target>Принять инкогнито</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Accept requests" xml:space="preserve">
<source>Accept requests</source>
<target>Принимать запросы</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Advanced network settings" xml:space="preserve">
<source>Advanced network settings</source>
<target>Настройки сети</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="All group members will remain connected." xml:space="preserve">
<source>All group members will remain connected.</source>
<target>Все члены группы, которые соединились через эту ссылку, останутся в группе.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="All messages will be deleted - this cannot be undone! The messages will be deleted ONLY for you." xml:space="preserve">
<source>All messages will be deleted - this cannot be undone! The messages will be deleted ONLY for you.</source>
<target>Все сообщения будут удалены - это действие нельзя отменить! Сообщения будут удалены только для вас.</target>
@@ -313,6 +323,11 @@
<target>Автоприем изображений</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Automatically" xml:space="preserve">
<source>Automatically</source>
<target>Автоматически</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Call already ended!" xml:space="preserve">
<source>Call already ended!</source>
<target>Звонок уже завершен!</target>
@@ -323,11 +338,6 @@
<target>Звонки</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Can't delete contact!" xml:space="preserve">
<source>Can't delete contact!</source>
<target>Невозможно удалить контакт!</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Can't invite contact!" xml:space="preserve">
<source>Can't invite contact!</source>
<target>Нельзя пригласить контакт!</target>
@@ -368,6 +378,16 @@
<target>Поменять роль члена группы?</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Change receiving address (BETA)" xml:space="preserve">
<source>Change receiving address (BETA)</source>
<target>Поменять адрес получения (БЕТА)</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>Поменять адрес получения?</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Change role" xml:space="preserve">
<source>Change role</source>
<target>Поменять роль</target>
@@ -408,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>
@@ -478,6 +493,11 @@
<target>Соединиться через ссылку-контакт?</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Connect via group link?" xml:space="preserve">
<source>Connect via group link?</source>
<target>Соединиться через ссылку группы?</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Connect via link" xml:space="preserve">
<source>Connect via link</source>
<target>Соединиться через ссылку</target>
@@ -538,11 +558,6 @@
<target>Превышено время соединения</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Contact %@ cannot be deleted, they are a member of the group(s) %@." xml:space="preserve">
<source>Contact %@ cannot be deleted, they are a member of the group(s) %@.</source>
<target>Контакт %@ не может быть удален, так как является членом групп(ы) %@.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Contact already exists" xml:space="preserve">
<source>Contact already exists</source>
<target>Существующий контакт</target>
@@ -573,6 +588,11 @@
<target>Имена контактов</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Contact requests" xml:space="preserve">
<source>Contact requests</source>
<target>Запросы контактов</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Copy" xml:space="preserve">
<source>Copy</source>
<target>Скопировать</target>
@@ -588,6 +608,11 @@
<target>Создать адрес</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Create link" xml:space="preserve">
<source>Create link</source>
<target>Создать ссылку</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Create one-time invitation link" xml:space="preserve">
<source>Create one-time invitation link</source>
<target>Создать ссылку-приглашение</target>
@@ -801,6 +826,16 @@
<target>Удалить приглашение</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Delete link" xml:space="preserve">
<source>Delete link</source>
<target>Удалить ссылку</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Delete link?" xml:space="preserve">
<source>Delete link?</source>
<target>Удалить ссылку?</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Delete message?" xml:space="preserve">
<source>Delete message?</source>
<target>Удалить сообщение?</target>
@@ -996,6 +1031,11 @@
<target>Ошибка при добавлении членов группы</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error changing address" xml:space="preserve">
<source>Error changing address</source>
<target>Ошибка при изменении адреса</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error changing role" xml:space="preserve">
<source>Error changing role</source>
<target>Ошибка при изменении роли</target>
@@ -1016,6 +1056,11 @@
<target>Ошибка при создании группы</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error creating group link" xml:space="preserve">
<source>Error creating group link</source>
<target>Ошибка при создании ссылки группы</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error deleting chat database" xml:space="preserve">
<source>Error deleting chat database</source>
<target>Ошибка при удалении данных чата</target>
@@ -1031,6 +1076,11 @@
<target>Ошибка при удалении соединения</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error deleting contact" xml:space="preserve">
<source>Error deleting contact</source>
<target>Ошибка при удалении контакта</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error deleting database" xml:space="preserve">
<source>Error deleting database</source>
<target>Ошибка при удалении данных чата</target>
@@ -1166,11 +1216,6 @@
<target>Файл: %@</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Files" xml:space="preserve">
<source>Files</source>
<target>Файлы</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="For console" xml:space="preserve">
<source>For console</source>
<target>Для консоли</target>
@@ -1221,6 +1266,11 @@
<target>Приглашение в группу больше не действительно, оно было удалено отправителем.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Group link" xml:space="preserve">
<source>Group link</source>
<target>Ссылка группы</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Group message:" xml:space="preserve">
<source>Group message:</source>
<target>Групповое сообщение:</target>
@@ -1833,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>
@@ -2048,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>
@@ -2113,11 +2173,6 @@ 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="Shared one-time link" xml:space="preserve">
<source>Shared one-time link</source>
<target>Одноразовая ссылка-приглашение</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Show QR code" xml:space="preserve">
<source>Show QR code</source>
<target>Показать QR код</target>
@@ -2198,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>
@@ -2333,6 +2393,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="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>Это экспериментальная функция! Она будет работать, только если на другом клиенте установлена версия 4.2. После завершения смены адреса вы увидите сообщение — убедитесь, что вы все еще можете получать сообщения от этого контакта (или члена группы).</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="This group no longer exists." xml:space="preserve">
<source>This group no longer exists.</source>
<target>Эта группа больше не существует.</target>
@@ -2375,6 +2440,11 @@ You will be prompted to complete authentication before this feature is enabled.<
<target>Для поддержки мгновенный доставки уведомлений данные чата должны быть перемещены.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Transfer images faster (BETA)" xml:space="preserve">
<source>Transfer images faster (BETA)</source>
<target>Передавать изображения быстрее (БЕТА)</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">
<source>Trying to connect to the server used to receive messages from this contact (error: %@).</source>
<target>Устанавливается соединение с сервером, через который вы получаете сообщения от этого контакта (ошибка: %@).</target>
@@ -2437,6 +2507,11 @@ 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="Unread" xml:space="preserve">
<source>Unread</source>
<target>Не прочитано</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Update" xml:space="preserve">
<source>Update</source>
<target>Обновить</target>
@@ -2517,6 +2592,11 @@ To connect, please ask your contact to create another connection link and check
<target>Здравствуйте %@!</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Welcome message" xml:space="preserve">
<source>Welcome message</source>
<target>Приветственное сообщение</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="When available" xml:space="preserve">
<source>When available</source>
<target>Когда возможно</target>
@@ -2547,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">
@@ -2577,6 +2657,11 @@ 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 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." xml:space="preserve">
<source>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.</source>
<target>Вы можете поделиться ссылкой или QR кодом - через них можно присоединиться к группе. Вы сможете удалить ссылку, сохранив членов группы, которые через нее соединились.</target>
<note>No comment provided by engineer.</note>
</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>Вы можете использовать ваш адрес как ссылку или как QR код - кто угодно сможет соединиться с вами. Вы сможете удалить адрес, сохранив контакты, которые через него соединились.</target>
@@ -2642,6 +2727,11 @@ 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 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>Соединение с группой будет установлено, когда хост группы будет онлайн. Пожалуйста, подождите или проверьте позже!</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>Соединение будет установлено, когда ваш запрос будет принят. Пожалуйста, подождите или проверьте позже!</target>
@@ -2657,6 +2747,11 @@ To connect, please ask your contact to create another connection link and check
<target>Вы будете аутентифицированы при запуске и возобновлении приложения, которое было 30 секунд в фоновом режиме.</target>
<note>No comment provided by engineer.</note>
</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>Вы вступите в группу, на которую ссылается эта ссылка, и соединитесь с её членами.</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">
<source>You will stop receiving messages from this group. Chat history will be preserved.</source>
<target>Вы перестанете получать сообщения от этой группы. История чата будет сохранена.</target>
@@ -2781,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>
@@ -2846,6 +2951,31 @@ SimpleX серверы не могут получить доступ к ваше
<target>входящий звонок…</target>
<note>call status</note>
</trans-unit>
<trans-unit id="changed address for you" xml:space="preserve">
<source>changed address for you</source>
<target>поменял(а) адрес для вас</target>
<note>chat item text</note>
</trans-unit>
<trans-unit id="changed role of %@ to %@" xml:space="preserve">
<source>changed role of %1$@ to %2$@</source>
<target>поменял(а) роль члена %1$@ на: %2$@</target>
<note>rcv group event chat item</note>
</trans-unit>
<trans-unit id="changed your role to %@" xml:space="preserve">
<source>changed your role to %@</source>
<target>поменял(а) вашу роль на: %@</target>
<note>rcv group event chat item</note>
</trans-unit>
<trans-unit id="changing address for %@..." xml:space="preserve">
<source>changing address for %@...</source>
<target>смена адреса для %@...</target>
<note>chat item text</note>
</trans-unit>
<trans-unit id="changing address..." xml:space="preserve">
<source>changing address...</source>
<target>смена адреса...</target>
<note>chat item text</note>
</trans-unit>
<trans-unit id="colored" xml:space="preserve">
<source>colored</source>
<target>цвет</target>
@@ -2991,6 +3121,11 @@ SimpleX серверы не могут получить доступ к ваше
<target>инкогнито через ссылку-контакт</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>инкогнито через ссылку группы</target>
<note>chat list item description</note>
</trans-unit>
<trans-unit id="incognito via one-time link" xml:space="preserve">
<source>incognito via one-time link</source>
<target>инкогнито через одноразовую ссылку</target>
@@ -3021,6 +3156,11 @@ SimpleX серверы не могут получить доступ к ваше
<target>приглашение</target>
<note>chat list item title</note>
</trans-unit>
<trans-unit id="invited via your group link" xml:space="preserve">
<source>invited via your group link</source>
<target>приглашен(а) через вашу ссылку группы</target>
<note>rcv group event chat item</note>
</trans-unit>
<trans-unit id="italic" xml:space="preserve">
<source>italic</source>
<target>курсив</target>
@@ -3041,12 +3181,6 @@ SimpleX серверы не могут получить доступ к ваше
<target>член группы</target>
<note>member role</note>
</trans-unit>
<trans-unit id="member %@ role: %@" xml:space="preserve">
<source>member %1$@ role: %2$@</source>
<target>роль %1$@: %2$@</target>
<note>rcv group event chat item
snd group event chat item</note>
</trans-unit>
<trans-unit id="member connected" xml:space="preserve">
<source>connected</source>
<target>соединен(а)</target>
@@ -3167,6 +3301,11 @@ SimpleX серверы не могут получить доступ к ваше
<target>через ссылку-контакт</target>
<note>chat list item description</note>
</trans-unit>
<trans-unit id="via group link" xml:space="preserve">
<source>via group link</source>
<target>через ссылку группы</target>
<note>chat list item description</note>
</trans-unit>
<trans-unit id="via one-time link" xml:space="preserve">
<source>via one-time link</source>
<target>через одноразовую ссылку</target>
@@ -3202,6 +3341,26 @@ SimpleX серверы не могут получить доступ к ваше
<target>вы приглашены в группу</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="you changed address" xml:space="preserve">
<source>you changed address</source>
<target>вы поменяли адрес</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>вы поменяли адрес для %@</target>
<note>chat item text</note>
</trans-unit>
<trans-unit id="you changed role for yourself to %@" xml:space="preserve">
<source>you changed role for yourself to %@</source>
<target>вы поменяли роль себе на: %@</target>
<note>snd group event chat item</note>
</trans-unit>
<trans-unit id="you changed role of %@ to %@" xml:space="preserve">
<source>you changed role of %1$@ to %2$@</source>
<target>вы поменяли роль члена %1$@ на: %2$@</target>
<note>snd group event chat item</note>
</trans-unit>
<trans-unit id="you left" xml:space="preserve">
<source>you left</source>
<target>вы покинули группу</target>
@@ -3227,12 +3386,6 @@ SimpleX серверы не могут получить доступ к ваше
<target>вы: </target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="your role: %@" xml:space="preserve">
<source>your role: %@</source>
<target>ваша роль: %@</target>
<note>rcv group event chat item
snd group event chat item</note>
</trans-unit>
<trans-unit id="~strike~" xml:space="preserve">
<source>\~strike~</source>
<target>\~зачеркнуть~</target>
@@ -207,7 +207,7 @@ func chatRecvMsg() async -> ChatResponse? {
func receivedMsgNtf(_ res: ChatResponse) async -> (String, UNMutableNotificationContent)? {
logger.debug("NotificationService processReceivedMsg: \(res.responseType)")
switch res {
case let .contactConnected(contact):
case let .contactConnected(contact, _):
return (contact.id, createContactConnectedNtf(contact))
// case let .contactConnecting(contact):
// TODO profile update
@@ -220,7 +220,8 @@ func receivedMsgNtf(_ res: ChatResponse) async -> (String, UNMutableNotification
if let file = cItem.file,
file.fileSize <= maxImageSize,
privacyAcceptImagesGroupDefault.get() {
cItem = apiReceiveFile(fileId: file.fileId)?.chatItem ?? cItem
let inline = privacyTransferImagesInlineGroupDefault.get()
cItem = apiReceiveFile(fileId: file.fileId, inline: inline)?.chatItem ?? cItem
}
}
return cItem.isCall() ? nil : (aChatItem.chatId, createMessageReceivedNtf(cInfo, cItem))
@@ -274,8 +275,8 @@ func apiGetNtfMessage(nonce: String, encNtfInfo: String) -> NtfMessages? {
return nil
}
func apiReceiveFile(fileId: Int64) -> AChatItem? {
let r = sendSimpleXCmd(.receiveFile(fileId: fileId))
func apiReceiveFile(fileId: Int64, inline: Bool) -> AChatItem? {
let r = sendSimpleXCmd(.receiveFile(fileId: fileId, inline: inline))
if case let .rcvFileAccepted(chatItem) = r { return chatItem }
logger.error("receiveFile error: \(responseError(r))")
return nil
+42 -34
View File
@@ -42,6 +42,11 @@
5C55A923283CEDE600C4E99E /* SoundPlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C55A922283CEDE600C4E99E /* SoundPlayer.swift */; };
5C55A92E283D0FDE00C4E99E /* sounds in Resources */ = {isa = PBXBuildFile; fileRef = 5C55A92D283D0FDE00C4E99E /* sounds */; };
5C577F7D27C83AA10006112D /* MarkdownHelp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C577F7C27C83AA10006112D /* MarkdownHelp.swift */; };
5C58BCD0292AA93700AF9E4F /* libHSsimplex-chat-4.2.1-HrC8bBnv4qm8Sq6Eue57W1-ghc8.10.7.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C58BCCB292AA93700AF9E4F /* libHSsimplex-chat-4.2.1-HrC8bBnv4qm8Sq6Eue57W1-ghc8.10.7.a */; };
5C58BCD1292AA93700AF9E4F /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C58BCCC292AA93700AF9E4F /* libgmp.a */; };
5C58BCD2292AA93700AF9E4F /* libHSsimplex-chat-4.2.1-HrC8bBnv4qm8Sq6Eue57W1.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C58BCCD292AA93700AF9E4F /* libHSsimplex-chat-4.2.1-HrC8bBnv4qm8Sq6Eue57W1.a */; };
5C58BCD3292AA93700AF9E4F /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C58BCCE292AA93700AF9E4F /* libffi.a */; };
5C58BCD4292AA93700AF9E4F /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C58BCCF292AA93700AF9E4F /* libgmpxx.a */; };
5C5DB70E289ABDD200730FFF /* AppearanceSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C5DB70D289ABDD200730FFF /* AppearanceSettings.swift */; };
5C5E5D3B2824468B00B0488A /* ActiveCallView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C5E5D3A2824468B00B0488A /* ActiveCallView.swift */; };
5C5F2B6D27EBC3FE006A9D5F /* ImagePicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C5F2B6C27EBC3FE006A9D5F /* ImagePicker.swift */; };
@@ -89,6 +94,7 @@
5CC1C99527A6CF7F000D9FF6 /* ShareSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CC1C99427A6CF7F000D9FF6 /* ShareSheet.swift */; };
5CC2C0FC2809BF11000C35E3 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 5CC2C0FA2809BF11000C35E3 /* Localizable.strings */; };
5CC2C0FF2809BF11000C35E3 /* SimpleX--iOS--InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 5CC2C0FD2809BF11000C35E3 /* SimpleX--iOS--InfoPlist.strings */; };
5CCA7DF32905735700C8FEBA /* AcceptRequestsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CCA7DF22905735700C8FEBA /* AcceptRequestsView.swift */; };
5CCD403427A5F6DF00368C90 /* AddContactView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CCD403327A5F6DF00368C90 /* AddContactView.swift */; };
5CCD403727A5F9A200368C90 /* ScanToConnectView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CCD403627A5F9A200368C90 /* ScanToConnectView.swift */; };
5CDCAD482818589900503DA2 /* NotificationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CDCAD472818589900503DA2 /* NotificationService.swift */; };
@@ -119,15 +125,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 */; };
6440CA00288857A10062C672 /* CIGroupEventView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6440C9FF288857A10062C672 /* CIGroupEventView.swift */; };
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 */; };
6442E0BE2880182D00CEC0F9 /* GroupChatInfoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6442E0BD2880182D00CEC0F9 /* GroupChatInfoView.swift */; };
6448BBA628EAF728000D2AB9 /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 6448BBA128EAF728000D2AB9 /* libffi.a */; };
6448BBA728EAF728000D2AB9 /* libHSsimplex-chat-4.0.1-FvHSNBJjHeqKQoixUekUiI-ghc8.10.7.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 6448BBA228EAF728000D2AB9 /* libHSsimplex-chat-4.0.1-FvHSNBJjHeqKQoixUekUiI-ghc8.10.7.a */; };
6448BBA828EAF728000D2AB9 /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 6448BBA328EAF728000D2AB9 /* libgmpxx.a */; };
6448BBA928EAF728000D2AB9 /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 6448BBA428EAF728000D2AB9 /* libgmp.a */; };
6448BBAA28EAF728000D2AB9 /* libHSsimplex-chat-4.0.1-FvHSNBJjHeqKQoixUekUiI.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 6448BBA528EAF728000D2AB9 /* libHSsimplex-chat-4.0.1-FvHSNBJjHeqKQoixUekUiI.a */; };
6448BBB628FA9D56000D2AB9 /* GroupLinkView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6448BBB528FA9D56000D2AB9 /* GroupLinkView.swift */; };
6454036F2822A9750090DDFF /* ComposeFileView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6454036E2822A9750090DDFF /* ComposeFileView.swift */; };
646BB38C283BEEB9001CE359 /* LocalAuthentication.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 646BB38B283BEEB9001CE359 /* LocalAuthentication.framework */; };
646BB38E283FDB6D001CE359 /* LocalAuthenticationUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 646BB38D283FDB6D001CE359 /* LocalAuthenticationUtils.swift */; };
@@ -235,6 +237,11 @@
5C55A922283CEDE600C4E99E /* SoundPlayer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SoundPlayer.swift; sourceTree = "<group>"; };
5C55A92D283D0FDE00C4E99E /* sounds */ = {isa = PBXFileReference; lastKnownFileType = folder; path = sounds; sourceTree = "<group>"; };
5C577F7C27C83AA10006112D /* MarkdownHelp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MarkdownHelp.swift; sourceTree = "<group>"; };
5C58BCCB292AA93700AF9E4F /* libHSsimplex-chat-4.2.1-HrC8bBnv4qm8Sq6Eue57W1-ghc8.10.7.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-4.2.1-HrC8bBnv4qm8Sq6Eue57W1-ghc8.10.7.a"; sourceTree = "<group>"; };
5C58BCCC292AA93700AF9E4F /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = "<group>"; };
5C58BCCD292AA93700AF9E4F /* libHSsimplex-chat-4.2.1-HrC8bBnv4qm8Sq6Eue57W1.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-4.2.1-HrC8bBnv4qm8Sq6Eue57W1.a"; sourceTree = "<group>"; };
5C58BCCE292AA93700AF9E4F /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = "<group>"; };
5C58BCCF292AA93700AF9E4F /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = "<group>"; };
5C5DB70D289ABDD200730FFF /* AppearanceSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppearanceSettings.swift; sourceTree = "<group>"; };
5C5E5D3A2824468B00B0488A /* ActiveCallView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActiveCallView.swift; sourceTree = "<group>"; };
5C5E5D3C282447AB00B0488A /* CallTypes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallTypes.swift; sourceTree = "<group>"; };
@@ -288,6 +295,7 @@
5CC1C99427A6CF7F000D9FF6 /* ShareSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareSheet.swift; sourceTree = "<group>"; };
5CC2C0FB2809BF11000C35E3 /* ru */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ru; path = ru.lproj/Localizable.strings; sourceTree = "<group>"; };
5CC2C0FE2809BF11000C35E3 /* ru */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ru; path = "ru.lproj/SimpleX--iOS--InfoPlist.strings"; sourceTree = "<group>"; };
5CCA7DF22905735700C8FEBA /* AcceptRequestsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AcceptRequestsView.swift; sourceTree = "<group>"; };
5CCD403327A5F6DF00368C90 /* AddContactView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddContactView.swift; sourceTree = "<group>"; };
5CCD403627A5F9A200368C90 /* ScanToConnectView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScanToConnectView.swift; sourceTree = "<group>"; };
5CDCAD452818589900503DA2 /* SimpleX NSE.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = "SimpleX NSE.appex"; sourceTree = BUILT_PRODUCTS_DIR; };
@@ -316,15 +324,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>"; };
6440C9FF288857A10062C672 /* CIGroupEventView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CIGroupEventView.swift; 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>"; };
6442E0BD2880182D00CEC0F9 /* GroupChatInfoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupChatInfoView.swift; sourceTree = "<group>"; };
6448BBA128EAF728000D2AB9 /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = "<group>"; };
6448BBA228EAF728000D2AB9 /* libHSsimplex-chat-4.0.1-FvHSNBJjHeqKQoixUekUiI-ghc8.10.7.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-4.0.1-FvHSNBJjHeqKQoixUekUiI-ghc8.10.7.a"; sourceTree = "<group>"; };
6448BBA328EAF728000D2AB9 /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = "<group>"; };
6448BBA428EAF728000D2AB9 /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = "<group>"; };
6448BBA528EAF728000D2AB9 /* libHSsimplex-chat-4.0.1-FvHSNBJjHeqKQoixUekUiI.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-4.0.1-FvHSNBJjHeqKQoixUekUiI.a"; sourceTree = "<group>"; };
6448BBB528FA9D56000D2AB9 /* GroupLinkView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupLinkView.swift; sourceTree = "<group>"; };
6454036E2822A9750090DDFF /* ComposeFileView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeFileView.swift; sourceTree = "<group>"; };
646BB38B283BEEB9001CE359 /* LocalAuthentication.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = LocalAuthentication.framework; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS15.4.sdk/System/Library/Frameworks/LocalAuthentication.framework; sourceTree = DEVELOPER_DIR; };
646BB38D283FDB6D001CE359 /* LocalAuthenticationUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalAuthenticationUtils.swift; sourceTree = "<group>"; };
@@ -370,13 +374,13 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
6448BBAA28EAF728000D2AB9 /* libHSsimplex-chat-4.0.1-FvHSNBJjHeqKQoixUekUiI.a in Frameworks */,
5C58BCD2292AA93700AF9E4F /* libHSsimplex-chat-4.2.1-HrC8bBnv4qm8Sq6Eue57W1.a in Frameworks */,
5C58BCD3292AA93700AF9E4F /* libffi.a in Frameworks */,
5C58BCD0292AA93700AF9E4F /* libHSsimplex-chat-4.2.1-HrC8bBnv4qm8Sq6Eue57W1-ghc8.10.7.a in Frameworks */,
5CE2BA93284534B000EC33A6 /* libiconv.tbd in Frameworks */,
6448BBA828EAF728000D2AB9 /* libgmpxx.a in Frameworks */,
6448BBA728EAF728000D2AB9 /* libHSsimplex-chat-4.0.1-FvHSNBJjHeqKQoixUekUiI-ghc8.10.7.a in Frameworks */,
6448BBA628EAF728000D2AB9 /* libffi.a in Frameworks */,
6448BBA928EAF728000D2AB9 /* libgmp.a in Frameworks */,
5C58BCD1292AA93700AF9E4F /* libgmp.a in Frameworks */,
5CE2BA94284534BB00EC33A6 /* libz.tbd in Frameworks */,
5C58BCD4292AA93700AF9E4F /* libgmpxx.a in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -431,11 +435,11 @@
5C764E5C279C70B7000C6508 /* Libraries */ = {
isa = PBXGroup;
children = (
6448BBA128EAF728000D2AB9 /* libffi.a */,
6448BBA428EAF728000D2AB9 /* libgmp.a */,
6448BBA328EAF728000D2AB9 /* libgmpxx.a */,
6448BBA228EAF728000D2AB9 /* libHSsimplex-chat-4.0.1-FvHSNBJjHeqKQoixUekUiI-ghc8.10.7.a */,
6448BBA528EAF728000D2AB9 /* libHSsimplex-chat-4.0.1-FvHSNBJjHeqKQoixUekUiI.a */,
5C58BCCE292AA93700AF9E4F /* libffi.a */,
5C58BCCC292AA93700AF9E4F /* libgmp.a */,
5C58BCCF292AA93700AF9E4F /* libgmpxx.a */,
5C58BCCB292AA93700AF9E4F /* libHSsimplex-chat-4.2.1-HrC8bBnv4qm8Sq6Eue57W1-ghc8.10.7.a */,
5C58BCCD292AA93700AF9E4F /* libHSsimplex-chat-4.2.1-HrC8bBnv4qm8Sq6Eue57W1.a */,
);
path = Libraries;
sourceTree = "<group>";
@@ -571,6 +575,7 @@
5C05DF522840AA1D00C683F9 /* CallSettings.swift */,
5C3F1D57284363C400EC8A82 /* PrivacySettings.swift */,
5CB924E327A8683A00ACCCDD /* UserAddress.swift */,
5CCA7DF22905735700C8FEBA /* AcceptRequestsView.swift */,
5CB924E027A867BA00ACCCDD /* UserProfile.swift */,
5C577F7C27C83AA10006112D /* MarkdownHelp.swift */,
640F50E227CF991C001E05C2 /* SMPServers.swift */,
@@ -642,7 +647,7 @@
5C029EA72837DBB3004A9677 /* CICallItemView.swift */,
5C3F1D552842B68D00EC8A82 /* IntegrityErrorItemView.swift */,
64E972062881BB22008DBC02 /* CIGroupInvitationView.swift */,
6440C9FF288857A10062C672 /* CIGroupEventView.swift */,
6440C9FF288857A10062C672 /* CIEventView.swift */,
);
path = ChatItem;
sourceTree = "<group>";
@@ -679,6 +684,7 @@
6442E0BD2880182D00CEC0F9 /* GroupChatInfoView.swift */,
647F090D288EA27B00644C40 /* GroupMemberInfoView.swift */,
5C9C2DA42894777E00CC63B1 /* GroupProfileView.swift */,
6448BBB528FA9D56000D2AB9 /* GroupLinkView.swift */,
);
path = Group;
sourceTree = "<group>";
@@ -889,6 +895,7 @@
5CE4407927ADB701007B033A /* EmojiItemView.swift in Sources */,
5C3F1D562842B68D00EC8A82 /* IntegrityErrorItemView.swift in Sources */,
5C029EAA283942EA004A9677 /* CallController.swift in Sources */,
5CCA7DF32905735700C8FEBA /* AcceptRequestsView.swift in Sources */,
5C5346A827B59A6A004DF848 /* ChatHelp.swift in Sources */,
5CFA59C42860BC6200863A68 /* MigrateToAppGroupView.swift in Sources */,
648010AB281ADD15009009B9 /* CIFileView.swift in Sources */,
@@ -943,6 +950,7 @@
5C6BA667289BD954009B8ECC /* DismissSheets.swift in Sources */,
5C577F7D27C83AA10006112D /* MarkdownHelp.swift in Sources */,
5CA059EB279559F40002BEB4 /* SimpleXApp.swift in Sources */,
6448BBB628FA9D56000D2AB9 /* GroupLinkView.swift in Sources */,
5CB346E92869E8BA001FD2EF /* PushEnvironment.swift in Sources */,
5C55A91F283AD0E400C4E99E /* CallManager.swift in Sources */,
5CCD403727A5F9A200368C90 /* ScanToConnectView.swift in Sources */,
@@ -958,7 +966,7 @@
5C2E260727A2941F00F70299 /* SimpleXAPI.swift in Sources */,
5CB924D427A853F100ACCCDD /* SettingsButton.swift in Sources */,
3C714777281C081000CB4D4B /* WebRTCView.swift in Sources */,
6440CA00288857A10062C672 /* CIGroupEventView.swift in Sources */,
6440CA00288857A10062C672 /* CIEventView.swift in Sources */,
5CB0BA92282713FD00B3292C /* CreateProfile.swift in Sources */,
5C5F2B7027EBC704006A9D5F /* ProfileImage.swift in Sources */,
64AA1C6C27F3537400AC7277 /* DeletedItemView.swift in Sources */,
@@ -1203,7 +1211,7 @@
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 83;
CURRENT_PROJECT_VERSION = 92;
DEVELOPMENT_TEAM = 5NN7GUYB6T;
ENABLE_BITCODE = NO;
ENABLE_PREVIEWS = YES;
@@ -1224,7 +1232,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 4.1;
MARKETING_VERSION = 4.2.2;
PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.app;
PRODUCT_NAME = SimpleX;
SDKROOT = iphoneos;
@@ -1245,7 +1253,7 @@
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 83;
CURRENT_PROJECT_VERSION = 92;
DEVELOPMENT_TEAM = 5NN7GUYB6T;
ENABLE_BITCODE = NO;
ENABLE_PREVIEWS = YES;
@@ -1266,7 +1274,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 4.1;
MARKETING_VERSION = 4.2.2;
PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.app;
PRODUCT_NAME = SimpleX;
SDKROOT = iphoneos;
@@ -1285,7 +1293,7 @@
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = 9767FTRA3G;
GENERATE_INFOPLIST_FILE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 15.2;
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.Tests-iOS";
PRODUCT_NAME = "$(TARGET_NAME)";
@@ -1305,7 +1313,7 @@
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = 9767FTRA3G;
GENERATE_INFOPLIST_FILE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 15.2;
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.Tests-iOS";
PRODUCT_NAME = "$(TARGET_NAME)";
@@ -1324,7 +1332,7 @@
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = "SimpleX NSE/SimpleX NSE.entitlements";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 83;
CURRENT_PROJECT_VERSION = 92;
DEVELOPMENT_TEAM = 5NN7GUYB6T;
ENABLE_BITCODE = NO;
GENERATE_INFOPLIST_FILE = YES;
@@ -1337,7 +1345,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 4.1;
MARKETING_VERSION = 4.2.2;
PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.app.SimpleX-NSE";
PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = iphoneos;
@@ -1354,7 +1362,7 @@
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = "SimpleX NSE/SimpleX NSE.entitlements";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 83;
CURRENT_PROJECT_VERSION = 92;
DEVELOPMENT_TEAM = 5NN7GUYB6T;
ENABLE_BITCODE = NO;
GENERATE_INFOPLIST_FILE = YES;
@@ -1367,7 +1375,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 4.1;
MARKETING_VERSION = 4.2.2;
PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.app.SimpleX-NSE";
PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = iphoneos;
@@ -44,6 +44,7 @@
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
enableUBSanitizer = "YES"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
+79 -14
View File
@@ -43,6 +43,9 @@ public enum ChatCommand {
case apiLeaveGroup(groupId: Int64)
case apiListMembers(groupId: Int64)
case apiUpdateGroupProfile(groupId: Int64, groupProfile: GroupProfile)
case apiCreateGroupLink(groupId: Int64)
case apiDeleteGroupLink(groupId: Int64)
case apiGetGroupLink(groupId: Int64)
case getUserSMPServers
case setUserSMPServers(smpServers: [String])
case apiSetChatItemTTL(seconds: Int64?)
@@ -52,6 +55,8 @@ public enum ChatCommand {
case apiSetChatSettings(type: ChatType, id: Int64, chatSettings: ChatSettings)
case apiContactInfo(contactId: Int64)
case apiGroupMemberInfo(groupId: Int64, groupMemberId: Int64)
case apiSwitchContact(contactId: Int64)
case apiSwitchGroupMember(groupId: Int64, groupMemberId: Int64)
case addContact
case connect(connReq: String)
case apiDeleteChat(type: ChatType, id: Int64)
@@ -63,6 +68,7 @@ public enum ChatCommand {
case createMyAddress
case deleteMyAddress
case showMyAddress
case addressAutoAccept(autoAccept: AutoAccept?)
case apiAcceptContact(contactReqId: Int64)
case apiRejectContact(contactReqId: Int64)
// WebRTC calls
@@ -75,7 +81,8 @@ public enum ChatCommand {
case apiGetCallInvitations
case apiCallStatus(contact: Contact, callStatus: WebRTCCallStatus)
case apiChatRead(type: ChatType, id: Int64, itemRange: (Int64, Int64))
case receiveFile(fileId: Int64)
case apiChatUnread(type: ChatType, id: Int64, unreadChat: Bool)
case receiveFile(fileId: Int64, inline: Bool)
case string(String)
public var cmdString: String {
@@ -114,6 +121,9 @@ public enum ChatCommand {
case let .apiLeaveGroup(groupId): return "/_leave #\(groupId)"
case let .apiListMembers(groupId): return "/_members #\(groupId)"
case let .apiUpdateGroupProfile(groupId, groupProfile): return "/_group_profile #\(groupId) \(encodeJSON(groupProfile))"
case let .apiCreateGroupLink(groupId): return "/_create link #\(groupId)"
case let .apiDeleteGroupLink(groupId): return "/_delete link #\(groupId)"
case let .apiGetGroupLink(groupId): return "/_get link #\(groupId)"
case .getUserSMPServers: return "/smp_servers"
case let .setUserSMPServers(smpServers): return "/smp_servers \(smpServersStr(smpServers: smpServers))"
case let .apiSetChatItemTTL(seconds): return "/_ttl \(chatItemTTLStr(seconds: seconds))"
@@ -123,6 +133,8 @@ public enum ChatCommand {
case let .apiSetChatSettings(type, id, chatSettings): return "/_settings \(ref(type, id)) \(encodeJSON(chatSettings))"
case let .apiContactInfo(contactId): return "/_info @\(contactId)"
case let .apiGroupMemberInfo(groupId, groupMemberId): return "/_info #\(groupId) \(groupMemberId)"
case let .apiSwitchContact(contactId): return "/_switch @\(contactId)"
case let .apiSwitchGroupMember(groupId, groupMemberId): return "/_switch #\(groupId) \(groupMemberId)"
case .addContact: return "/connect"
case let .connect(connReq): return "/connect \(connReq)"
case let .apiDeleteChat(type, id): return "/_delete \(ref(type, id))"
@@ -134,6 +146,7 @@ public enum ChatCommand {
case .createMyAddress: return "/address"
case .deleteMyAddress: return "/delete_address"
case .showMyAddress: return "/show_address"
case let .addressAutoAccept(autoAccept): return "/auto_accept \(AutoAccept.cmdString(autoAccept))"
case let .apiAcceptContact(contactReqId): return "/_accept \(contactReqId)"
case let .apiRejectContact(contactReqId): return "/_reject \(contactReqId)"
case let .apiSendCallInvitation(contact, callType): return "/_call invite @\(contact.apiId) \(encodeJSON(callType))"
@@ -145,7 +158,8 @@ public enum ChatCommand {
case .apiGetCallInvitations: return "/_call get"
case let .apiCallStatus(contact, callStatus): return "/_call status @\(contact.apiId) \(callStatus.rawValue)"
case let .apiChatRead(type, id, itemRange: (from, to)): return "/_read chat \(ref(type, id)) from=\(from) to=\(to)"
case let .receiveFile(fileId): return "/freceive \(fileId)"
case let .apiChatUnread(type, id, unreadChat): return "/_unread chat \(ref(type, id)) \(onOff(unreadChat))"
case let .receiveFile(fileId, inline): return "/freceive \(fileId) inline=\(onOff(inline))"
case let .string(str): return str
}
}
@@ -184,6 +198,9 @@ public enum ChatCommand {
case .apiLeaveGroup: return "apiLeaveGroup"
case .apiListMembers: return "apiListMembers"
case .apiUpdateGroupProfile: return "apiUpdateGroupProfile"
case .apiCreateGroupLink: return "apiCreateGroupLink"
case .apiDeleteGroupLink: return "apiDeleteGroupLink"
case .apiGetGroupLink: return "apiGetGroupLink"
case .getUserSMPServers: return "getUserSMPServers"
case .setUserSMPServers: return "setUserSMPServers"
case .apiSetChatItemTTL: return "apiSetChatItemTTL"
@@ -193,6 +210,8 @@ public enum ChatCommand {
case .apiSetChatSettings: return "apiSetChatSettings"
case .apiContactInfo: return "apiContactInfo"
case .apiGroupMemberInfo: return "apiGroupMemberInfo"
case .apiSwitchContact: return "apiSwitchContact"
case .apiSwitchGroupMember: return "apiSwitchGroupMember"
case .addContact: return "addContact"
case .connect: return "connect"
case .apiDeleteChat: return "apiDeleteChat"
@@ -204,6 +223,7 @@ public enum ChatCommand {
case .createMyAddress: return "createMyAddress"
case .deleteMyAddress: return "deleteMyAddress"
case .showMyAddress: return "showMyAddress"
case .addressAutoAccept: return "addressAutoAccept"
case .apiAcceptContact: return "apiAcceptContact"
case .apiRejectContact: return "apiRejectContact"
case .apiSendCallInvitation: return "apiSendCallInvitation"
@@ -215,6 +235,7 @@ public enum ChatCommand {
case .apiGetCallInvitations: return "apiGetCallInvitations"
case .apiCallStatus: return "apiCallStatus"
case .apiChatRead: return "apiChatRead"
case .apiChatUnread: return "apiChatUnread"
case .receiveFile: return "receiveFile"
case .string: return "console command"
}
@@ -226,7 +247,7 @@ public enum ChatCommand {
}
func smpServersStr(smpServers: [String]) -> String {
smpServers.isEmpty ? "default" : smpServers.joined(separator: ",")
smpServers.isEmpty ? "default" : smpServers.joined(separator: ";")
}
func chatItemTTLStr(seconds: Int64?) -> String {
@@ -282,10 +303,11 @@ public enum ChatResponse: Decodable, Error {
case userProfileUpdated(fromProfile: Profile, toProfile: Profile)
case contactAliasUpdated(toContact: Contact)
case connectionAliasUpdated(toConnection: PendingContactConnection)
case userContactLink(connReqContact: String)
case userContactLink(contactLink: UserContactLink)
case userContactLinkUpdated(contactLink: UserContactLink)
case userContactLinkCreated(connReqContact: String)
case userContactLinkDeleted
case contactConnected(contact: Contact)
case contactConnected(contact: Contact, userCustomProfile: Profile?)
case contactConnecting(contact: Contact)
case receivedContactRequest(contactRequest: UserContactRequest)
case acceptingContactRequest(contact: Contact)
@@ -307,7 +329,7 @@ public enum ChatResponse: Decodable, Error {
// group events
case groupCreated(groupInfo: GroupInfo)
case sentGroupInvitation(groupInfo: GroupInfo, contact: Contact, member: GroupMember)
case userAcceptedGroupSent(groupInfo: GroupInfo)
case userAcceptedGroupSent(groupInfo: GroupInfo, hostContact: Contact?)
case userDeletedMember(groupInfo: GroupInfo, member: GroupMember)
case leftMemberUser(groupInfo: GroupInfo)
case groupMembers(group: Group)
@@ -327,6 +349,9 @@ public enum ChatResponse: Decodable, Error {
case connectedToGroupMember(groupInfo: GroupInfo, member: GroupMember)
case groupRemoved(groupInfo: GroupInfo) // unused
case groupUpdated(toGroup: GroupInfo)
case groupLinkCreated(groupInfo: GroupInfo, connReqContact: String)
case groupLink(groupInfo: GroupInfo, connReqContact: String)
case groupLinkDeleted(groupInfo: GroupInfo)
// receiving file events
case rcvFileAccepted(chatItem: AChatItem)
case rcvFileAcceptedSndCancelled(rcvFileTransfer: RcvFileTransfer)
@@ -380,6 +405,7 @@ public enum ChatResponse: Decodable, Error {
case .contactAliasUpdated: return "contactAliasUpdated"
case .connectionAliasUpdated: return "connectionAliasUpdated"
case .userContactLink: return "userContactLink"
case .userContactLinkUpdated: return "userContactLinkUpdated"
case .userContactLinkCreated: return "userContactLinkCreated"
case .userContactLinkDeleted: return "userContactLinkDeleted"
case .contactConnected: return "contactConnected"
@@ -423,6 +449,9 @@ public enum ChatResponse: Decodable, Error {
case .connectedToGroupMember: return "connectedToGroupMember"
case .groupRemoved: return "groupRemoved"
case .groupUpdated: return "groupUpdated"
case .groupLinkCreated: return "groupLinkCreated"
case .groupLink: return "groupLink"
case .groupLinkDeleted: return "groupLinkDeleted"
case .rcvFileAccepted: return "rcvFileAccepted"
case .rcvFileAcceptedSndCancelled: return "rcvFileAcceptedSndCancelled"
case .rcvFileStart: return "rcvFileStart"
@@ -476,10 +505,11 @@ public enum ChatResponse: Decodable, Error {
case let .userProfileUpdated(_, toProfile): return String(describing: toProfile)
case let .contactAliasUpdated(toContact): return String(describing: toContact)
case let .connectionAliasUpdated(toConnection): return String(describing: toConnection)
case let .userContactLink(connReq): return connReq
case let .userContactLink(contactLink): return contactLink.responseDetails
case let .userContactLinkUpdated(contactLink): return contactLink.responseDetails
case let .userContactLinkCreated(connReq): return connReq
case .userContactLinkDeleted: return noDetails
case let .contactConnected(contact): return String(describing: contact)
case let .contactConnected(contact, _): return String(describing: contact)
case let .contactConnecting(contact): return String(describing: contact)
case let .receivedContactRequest(contactRequest): return String(describing: contactRequest)
case let .acceptingContactRequest(contact): return String(describing: contact)
@@ -500,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)
@@ -520,6 +550,9 @@ public enum ChatResponse: Decodable, Error {
case let .connectedToGroupMember(groupInfo, member): return "groupInfo: \(groupInfo)\nmember: \(member)"
case let .groupRemoved(groupInfo): return String(describing: groupInfo)
case let .groupUpdated(toGroup): return String(describing: toGroup)
case let .groupLinkCreated(groupInfo, connReqContact): return "groupInfo: \(groupInfo)\nconnReqContact: \(connReqContact)"
case let .groupLink(groupInfo, connReqContact): return "groupInfo: \(groupInfo)\nconnReqContact: \(connReqContact)"
case let .groupLinkDeleted(groupInfo): return String(describing: groupInfo)
case let .rcvFileAccepted(chatItem): return String(describing: chatItem)
case .rcvFileAcceptedSndCancelled: return noDetails
case let .rcvFileStart(chatItem): return String(describing: chatItem)
@@ -601,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
)
@@ -681,6 +714,37 @@ public struct ConnectionStats: Codable {
public var sndServers: [String]?
}
public struct UserContactLink: Decodable {
public var connReqContact: String
public var autoAccept: AutoAccept?
public init(connReqContact: String, autoAccept: AutoAccept? = nil) {
self.connReqContact = connReqContact
self.autoAccept = autoAccept
}
var responseDetails: String {
"connReqContact: \(connReqContact)\nautoAccept: \(AutoAccept.cmdString(autoAccept))"
}
}
public struct AutoAccept: Codable {
public var acceptIncognito: Bool
public var autoReply: MsgContent?
public init(acceptIncognito: Bool, autoReply: MsgContent? = nil) {
self.acceptIncognito = acceptIncognito
self.autoReply = autoReply
}
static func cmdString(_ autoAccept: AutoAccept?) -> String {
guard let autoAccept = autoAccept else { return "off" }
let s = "on" + (autoAccept.acceptIncognito ? " incognito=on" : "")
guard let msg = autoAccept.autoReply else { return s }
return s + " " + msg.cmdString
}
}
public protocol SelectableItem: Hashable, Identifiable {
var label: LocalizedStringKey { get }
static var values: [Self] { get }
@@ -783,7 +847,6 @@ public enum ChatErrorType: Decodable {
case invalidConnReq
case invalidChatMessage(message: String)
case contactNotReady(contact: Contact)
case contactGroups(contact: Contact, groupNames: [GroupName])
case groupUserRole
case groupContactRole(contactName: ContactName)
case groupDuplicateMember(contactName: ContactName)
@@ -841,6 +904,8 @@ public enum StoreError: Decodable {
case quotedChatItemNotFound
case chatItemSharedMsgIdNotFound(sharedMsgId: String)
case chatItemNotFoundByFileId(fileId: Int64)
case duplicateGroupLink(groupInfo: GroupInfo)
case groupLinkNotFound(groupInfo: GroupInfo)
}
public enum DatabaseError: Decodable {
+3
View File
@@ -14,6 +14,7 @@ let GROUP_DEFAULT_DB_CONTAINER = "dbContainer"
public let GROUP_DEFAULT_CHAT_LAST_START = "chatLastStart"
let GROUP_DEFAULT_NTF_PREVIEW_MODE = "ntfPreviewMode"
let GROUP_DEFAULT_PRIVACY_ACCEPT_IMAGES = "privacyAcceptImages"
public let GROUP_DEFAULT_PRIVACY_TRANSFER_IMAGES_INLINE = "privacyTransferImagesInline"
let GROUP_DEFAULT_NTF_BADGE_COUNT = "ntgBadgeCount"
let GROUP_DEFAULT_NETWORK_USE_ONION_HOSTS = "networkUseOnionHosts"
let GROUP_DEFAULT_NETWORK_TCP_CONNECT_TIMEOUT = "networkTCPConnectTimeout"
@@ -92,6 +93,8 @@ public let incognitoGroupDefault = BoolDefault(defaults: groupDefaults, forKey:
public let privacyAcceptImagesGroupDefault = BoolDefault(defaults: groupDefaults, forKey: GROUP_DEFAULT_PRIVACY_ACCEPT_IMAGES)
public let privacyTransferImagesInlineGroupDefault = BoolDefault(defaults: groupDefaults, forKey: GROUP_DEFAULT_PRIVACY_TRANSFER_IMAGES_INLINE)
public let ntfBadgeCountGroupDefault = IntDefault(defaults: groupDefaults, forKey: GROUP_DEFAULT_NTF_BADGE_COUNT)
public let networkUseOnionHostsGroupDefault = EnumDefault<OnionHosts>(
+77 -17
View File
@@ -303,13 +303,15 @@ public struct ChatData: Decodable, Identifiable {
}
public struct ChatStats: Decodable {
public init(unreadCount: Int = 0, minUnreadItemId: Int64 = 0) {
public init(unreadCount: Int = 0, minUnreadItemId: Int64 = 0, unreadChat: Bool = false) {
self.unreadCount = unreadCount
self.minUnreadItemId = minUnreadItemId
self.unreadChat = unreadChat
}
public var unreadCount: Int = 0
public var minUnreadItemId: Int64 = 0
public var unreadChat: Bool = false
}
public struct Contact: Identifiable, Decodable, NamedChat {
@@ -335,6 +337,10 @@ public struct Contact: Identifiable, Decodable, NamedChat {
activeConn.connLevel > 0 || viaGroup != nil
}
public var viaGroupLink: Bool {
activeConn.viaGroupLink
}
public var contactConnIncognito: Bool {
activeConn.customUserProfileId != nil
}
@@ -366,6 +372,7 @@ public struct Connection: Decodable {
var connId: Int64
var connStatus: ConnStatus
public var connLevel: Int
public var viaGroupLink: Bool
public var customUserProfileId: Int64?
public var id: ChatId { get { ":\(connId)" } }
@@ -373,7 +380,8 @@ public struct Connection: Decodable {
static let sampleData = Connection(
connId: 1,
connStatus: .ready,
connLevel: 0
connLevel: 0,
viaGroupLink: false
)
}
@@ -425,6 +433,7 @@ public struct PendingContactConnection: Decodable, NamedChat {
var pccAgentConnId: String
var pccConnStatus: ConnStatus
public var viaContactUri: Bool
public var groupLinkId: String?
public var customUserProfileId: Int64?
public var connReqInv: String?
public var localAlias: String
@@ -469,10 +478,18 @@ public struct PendingContactConnection: Decodable, NamedChat {
desc = NSLocalizedString("you shared one-time link", comment: "chat list item description")
}
} else if viaContactUri {
if incognito {
desc = NSLocalizedString("incognito via contact address link", comment: "chat list item description")
if groupLinkId != nil {
if incognito {
desc = NSLocalizedString("incognito via group link", comment: "chat list item description")
} else {
desc = NSLocalizedString("via group link", comment: "chat list item description")
}
} else {
desc = NSLocalizedString("via contact address link", comment: "chat list item description")
if incognito {
desc = NSLocalizedString("incognito via contact address link", comment: "chat list item description")
} else {
desc = NSLocalizedString("via contact address link", comment: "chat list item description")
}
}
} else {
if incognito {
@@ -703,6 +720,11 @@ public struct GroupMember: Identifiable, Decodable {
)
}
public struct GroupMemberRef: Decodable {
var groupMemberId: Int64
var profile: Profile
}
public enum GroupMemberRole: String, Identifiable, CaseIterable, Comparable, Decodable {
case member = "member"
case admin = "admin"
@@ -913,6 +935,7 @@ public struct ChatItem: Identifiable, Decodable {
case .memberAdded: return false
case .memberLeft: return false
case .memberDeleted: return false
case .invitedViaGroupLink: return false
}
case .sndGroupEvent: return true
default: return false
@@ -1087,6 +1110,8 @@ public enum CIContent: Decodable, ItemContent {
case sndGroupInvitation(groupInvitation: CIGroupInvitation, memberRole: GroupMemberRole)
case rcvGroupEvent(rcvGroupEvent: RcvGroupEvent)
case sndGroupEvent(sndGroupEvent: SndGroupEvent)
case rcvConnEvent(rcvConnEvent: RcvConnEvent)
case sndConnEvent(sndConnEvent: SndConnEvent)
public var text: String {
get {
@@ -1102,6 +1127,8 @@ public enum CIContent: Decodable, ItemContent {
case let .sndGroupInvitation(groupInvitation, _): return groupInvitation.text
case let .rcvGroupEvent(rcvGroupEvent): return rcvGroupEvent.text
case let .sndGroupEvent(sndGroupEvent): return sndGroupEvent.text
case let .rcvConnEvent(rcvConnEvent): return rcvConnEvent.text
case let .sndConnEvent(sndConnEvent): return sndConnEvent.text
}
}
}
@@ -1196,7 +1223,7 @@ public enum MsgContent {
// TODO include original JSON, possibly using https://github.com/zoul/generic-json-swift
case unknown(type: String, text: String)
var text: String {
public var text: String {
get {
switch self {
case let .text(text): return text
@@ -1217,13 +1244,6 @@ public enum MsgContent {
}
}
public func isFile() -> Bool {
switch self {
case .file: return true
default: return false
}
}
enum CodingKeys: String, CodingKey {
case type
case text
@@ -1446,6 +1466,7 @@ public enum RcvGroupEvent: Decodable {
case userDeleted
case groupDeleted
case groupUpdated(groupProfile: GroupProfile)
case invitedViaGroupLink
var text: String {
switch self {
@@ -1454,14 +1475,15 @@ public enum RcvGroupEvent: Decodable {
case .memberConnected: return NSLocalizedString("member connected", comment: "rcv group event chat item")
case .memberLeft: return NSLocalizedString("left", comment: "rcv group event chat item")
case let .memberRole(_, profile, role):
return String.localizedStringWithFormat(NSLocalizedString("member %@ role: %@", comment: "rcv group event chat item"), profile.profileViewName, role.text)
return String.localizedStringWithFormat(NSLocalizedString("changed role of %@ to %@", comment: "rcv group event chat item"), profile.profileViewName, role.text)
case let .userRole(role):
return String.localizedStringWithFormat(NSLocalizedString("your role: %@", comment: "rcv group event chat item"), role.text)
return String.localizedStringWithFormat(NSLocalizedString("changed your role to %@", comment: "rcv group event chat item"), role.text)
case let .memberDeleted(_, profile):
return String.localizedStringWithFormat(NSLocalizedString("removed %@", comment: "rcv group event chat item"), profile.profileViewName)
case .userDeleted: return NSLocalizedString("removed you", comment: "rcv group event chat item")
case .groupDeleted: return NSLocalizedString("deleted group", comment: "rcv group event chat item")
case .groupUpdated: return NSLocalizedString("updated group profile", comment: "rcv group event chat item")
case .invitedViaGroupLink: return NSLocalizedString("invited via your group link", comment: "rcv group event chat item")
}
}
}
@@ -1476,9 +1498,9 @@ public enum SndGroupEvent: Decodable {
var text: String {
switch self {
case let .memberRole(_, profile, role):
return String.localizedStringWithFormat(NSLocalizedString("member %@ role: %@", comment: "snd group event chat item"), profile.profileViewName, role.text)
return String.localizedStringWithFormat(NSLocalizedString("you changed role of %@ to %@", comment: "snd group event chat item"), profile.profileViewName, role.text)
case let .userRole(role):
return String.localizedStringWithFormat(NSLocalizedString("your role: %@", comment: "snd group event chat item"), role.text)
return String.localizedStringWithFormat(NSLocalizedString("you changed role for yourself to %@", comment: "snd group event chat item"), role.text)
case let .memberDeleted(_, profile):
return String.localizedStringWithFormat(NSLocalizedString("you removed %@", comment: "snd group event chat item"), profile.profileViewName)
case .userLeft: return NSLocalizedString("you left", comment: "snd group event chat item")
@@ -1487,6 +1509,44 @@ public enum SndGroupEvent: Decodable {
}
}
public enum RcvConnEvent: Decodable {
case switchQueue(phase: SwitchPhase)
var text: String {
switch self {
case let .switchQueue(phase):
if case .completed = phase {
return NSLocalizedString("changed address for you", comment: "chat item text")
}
return NSLocalizedString("changing address...", comment: "chat item text")
}
}
}
public enum SndConnEvent: Decodable {
case switchQueue(phase: SwitchPhase, member: GroupMemberRef?)
var text: String {
switch self {
case let .switchQueue(phase, member):
if let name = member?.profile.profileViewName {
return phase == .completed
? String.localizedStringWithFormat(NSLocalizedString("you changed address for %@", comment: "chat item text"), name)
: String.localizedStringWithFormat(NSLocalizedString("changing address for %@...", comment: "chat item text"), name)
}
return phase == .completed
? NSLocalizedString("you changed address", comment: "chat item text")
: NSLocalizedString("changing address...", comment: "chat item text")
}
}
}
public enum SwitchPhase: String, Decodable {
case started
case confirmed
case completed
}
public enum ChatItemTTL: Hashable, Identifiable, Comparable {
case day
case week
+144 -47
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.";
@@ -116,13 +122,13 @@
"~strike~" = "\\~durchstreichen~";
/* No comment provided by engineer. */
"1 day" = "1 Tag";
"1 day" = "täglich";
/* No comment provided by engineer. */
"1 month" = "1 Monat";
"1 month" = "monatlich";
/* No comment provided by engineer. */
"1 week" = "1 Woche";
"1 week" = "wöchentlich";
/* No comment provided by engineer. */
"6" = "6";
@@ -161,6 +167,9 @@
/* No comment provided by engineer. */
"Accept incognito" = "Inkognito akzeptieren";
/* No comment provided by engineer. */
"Accept requests" = "Anfragen annehmen";
/* call status */
"accepted call" = "Anruf angenommen";
@@ -170,6 +179,9 @@
/* No comment provided by engineer. */
"Advanced network settings" = "Erweiterte Netzwerkeinstellungen";
/* No comment provided by engineer. */
"All group members will remain connected." = "Alle Gruppenmitglieder bleiben verbunden.";
/* No comment provided by engineer. */
"All messages will be deleted - this cannot be undone! The messages will be deleted ONLY for you." = "Alle Nachrichten werden gelöscht - dies kann nicht rückgängig gemacht werden! Die Nachrichten werden NUR bei Ihnen gelöscht.";
@@ -206,6 +218,9 @@
/* No comment provided by engineer. */
"Auto-accept images" = "Bilder automatisch akzeptieren";
/* No comment provided by engineer. */
"Automatically" = "Automatisch";
/* integrity error chat item */
"bad message hash" = "Ungültiger Nachrichten-Hash";
@@ -230,9 +245,6 @@
/* No comment provided by engineer. */
"Calls" = "Anrufe";
/* No comment provided by engineer. */
"Can't delete contact!" = "Der Kontakt kann nicht gelöscht werden!";
/* No comment provided by engineer. */
"Can't invite contact!" = "Kontakt kann nicht eingeladen werden!";
@@ -257,6 +269,30 @@
/* No comment provided by engineer. */
"Change member role?" = "Die Mitgliederrolle ändern?";
/* No comment provided by engineer. */
"Change receiving address (BETA)" = "Wechseln der Empfängeradresse (BETA)";
/* No comment provided by engineer. */
"Change receiving address?" = "Empfängeradresse wechseln?";
/* No comment provided by engineer. */
"Change role" = "Rolle ändern";
/* chat item text */
"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$@";
/* rcv group event chat item */
"changed your role to %@" = "änderte Ihre Rolle auf %@";
/* chat item text */
"changing address for %@..." = "Wechseln der Adresse für %@ ...";
/* chat item text */
"changing address..." = "Wechseln der Adresse ...";
/* No comment provided by engineer. */
"Chat archive" = "Datenbank Archiv";
@@ -278,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";
@@ -329,6 +362,9 @@
/* No comment provided by engineer. */
"Connect via contact link?" = "Über den Kontakt-Link verbinden?";
/* No comment provided by engineer. */
"Connect via group link?" = "Über den Gruppen-Link verbinden?";
/* No comment provided by engineer. */
"Connect via link" = "Über einen Link verbinden";
@@ -395,9 +431,6 @@
/* connection information */
"connection:%@" = "Verbindung:%@";
/* No comment provided by engineer. */
"Contact %@ cannot be deleted, they are a member of the group(s) %@." = "Der Kontakt mit %@ kann nicht gelöscht werden, da er Mitglied einer oder mehrerer dieser Gruppen ist %@.";
/* No comment provided by engineer. */
"Contact already exists" = "Der Kontakt ist bereits vorhanden";
@@ -422,6 +455,9 @@
/* No comment provided by engineer. */
"Contact name" = "Kontaktname";
/* No comment provided by engineer. */
"Contact requests" = "Kontaktanfragen";
/* chat item action */
"Copy" = "Kopieren";
@@ -431,6 +467,9 @@
/* No comment provided by engineer. */
"Create address" = "Adresse erstellen";
/* No comment provided by engineer. */
"Create link" = "Link erzeugen";
/* No comment provided by engineer. */
"Create one-time invitation link" = "Erstellen Sie einen einmaligen Einladungslink";
@@ -557,6 +596,12 @@
/* No comment provided by engineer. */
"Delete invitation" = "Einladung löschen";
/* No comment provided by engineer. */
"Delete link" = "Link löschen";
/* No comment provided by engineer. */
"Delete link?" = "Link löschen?";
/* No comment provided by engineer. */
"Delete message?" = "Die Nachricht löschen?";
@@ -564,7 +609,7 @@
"Delete messages" = "Nachrichten löschen";
/* No comment provided by engineer. */
"Delete messages after" = "Löschen der Nachrichten nach";
"Delete messages after" = "Löschen der Nachrichten";
/* No comment provided by engineer. */
"Delete old database" = "Alte Datenbank löschen";
@@ -698,6 +743,9 @@
/* No comment provided by engineer. */
"Error adding member(s)" = "Fehler beim Hinzufügen von Mitgliedern";
/* No comment provided by engineer. */
"Error changing address" = "Fehler beim Wechseln der Adresse";
/* No comment provided by engineer. */
"Error changing role" = "Fehler beim Ändern der Rolle";
@@ -710,6 +758,9 @@
/* No comment provided by engineer. */
"Error creating group" = "Fehler beim Erzeugen der Gruppe";
/* No comment provided by engineer. */
"Error creating group link" = "Fehler beim Erzeugen des Gruppen-Links";
/* No comment provided by engineer. */
"Error deleting chat database" = "Fehler beim Löschen der Chat-Datenbank";
@@ -719,6 +770,9 @@
/* No comment provided by engineer. */
"Error deleting connection" = "Fehler beim Löschen der Verbindung";
/* No comment provided by engineer. */
"Error deleting contact" = "Fehler beim Löschen des Kontakts";
/* No comment provided by engineer. */
"Error deleting database" = "Fehler beim Löschen der Datenbank";
@@ -795,14 +849,11 @@
"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: %@";
/* No comment provided by engineer. */
"Files" = "Dateien";
/* No comment provided by engineer. */
"For console" = "Für Konsole";
@@ -836,6 +887,9 @@
/* No comment provided by engineer. */
"Group invitation is no longer valid, it was removed by sender." = "Die Gruppeneinladung ist nicht mehr gültig, sie wurde vom Absender entfernt.";
/* No comment provided by engineer. */
"Group link" = "Gruppen-Link";
/* notification */
"Group message:" = "Grppennachricht:";
@@ -891,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";
@@ -918,7 +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" = "Inkognito über einen Gruppen-Link";
/* chat list item description */
"incognito via one-time link" = "Inkognito über einen Einmal-Link";
@@ -933,7 +990,7 @@
"Incoming video call" = "Eingehender Videoanruf";
/* connection level description */
"indirect (%d)" = "Indirekt (%d)";
"indirect (%d)" = "indirekt (%d)";
/* No comment provided by engineer. */
"Install [SimpleX Chat for terminal](https://github.com/simplex-chat/simplex-chat)" = "Installieren Sie [SimpleX Chat als Terminalanwendung](https://github.com/simplex-chat/simplex-chat)";
@@ -963,11 +1020,14 @@
"invited" = "eingeladen";
/* rcv group event chat item */
"invited %@" = "Sie haben %@ eingeladen";
"invited %@" = "hat %@ eingeladen.";
/* chat list item title */
"invited to connect" = "Für eine Verbindung eingeladen";
/* rcv group event chat item */
"invited via your group link" = "wurde über Ihren Gruppen-Link eingeladen";
/* No comment provided by engineer. */
"iOS Keychain is used to securely store passphrase - it allows receiving push notifications." = "Für die sichere Speicherung des Passworts wird der iOS Schlüsselbund verwendet - dies erlaubt den Empfang von Push-Benachrichtigungen.";
@@ -1017,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";
@@ -1052,12 +1112,8 @@
/* No comment provided by engineer. */
"Member" = "Mitglied";
/* rcv group event chat item
snd group event chat item */
"member %@ role: %@" = "Mitglieder %1$@ Rolle: %2$@";
/* rcv group event chat item */
"member connected" = "verbunden";
"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.";
@@ -1201,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";
@@ -1243,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.";
@@ -1269,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";
@@ -1324,10 +1383,10 @@
"removed" = "entfernt";
/* rcv group event chat item */
"removed %@" = "Entfernt %@";
"removed %@" = "hat %@ aus der Gruppe entfernt";
/* rcv group event chat item */
"removed you" = "hat Sie entfernt";
"removed you" = "hat Sie aus der Gruppe entfernt";
/* chat item action */
"Reply" = "Antwort";
@@ -1362,6 +1421,9 @@
/* No comment provided by engineer. */
"Revert" = "Zurückkehren";
/* No comment provided by engineer. */
"Role" = "Rolle";
/* No comment provided by engineer. */
"Run chat" = "Chat starten";
@@ -1413,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.";
@@ -1452,9 +1517,6 @@
/* No comment provided by engineer. */
"Share one-time invitation link" = "Einmal-Einladungslink teilen";
/* No comment provided by engineer. */
"Shared one-time link" = "Geteilter Einmal-Link";
/* No comment provided by engineer. */
"Show preview" = "Vorschau anzeigen";
@@ -1515,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";
@@ -1588,17 +1653,20 @@
"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 - Die gesendeten und empfangenen Nachrichten, welche älter als die Ausgewählten sind, 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)." = "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.";
@@ -1623,6 +1691,9 @@
/* No comment provided by engineer. */
"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)" = "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: %@).";
@@ -1662,6 +1733,9 @@
/* No comment provided by engineer. */
"Unmute" = "Stummschaltung aufheben";
/* No comment provided by engineer. */
"Unread" = "Ungelesen";
/* No comment provided by engineer. */
"Update" = "Aktualisieren";
@@ -1702,7 +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" = "über einen Gruppen-Link";
/* chat list item description */
"via one-time link" = "über einen Einmal-Link";
@@ -1737,6 +1814,9 @@
/* No comment provided by engineer. */
"Welcome %@!" = "Willkommen %@!";
/* No comment provided by engineer. */
"Welcome message" = "Begrüßungsmeldung";
/* No comment provided by engineer. */
"When available" = "Wenn verfügbar";
@@ -1756,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.";
@@ -1777,7 +1857,10 @@
"You can set lock screen notification preview via settings." = "Über die Geräte-Einstellungen können Sie die Benachrichtigungsvorschau im Sperrbildschirm erlauben.";
/* 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 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 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.";
@@ -1785,6 +1868,18 @@
/* No comment provided by engineer. */
"You can use markdown to format messages:" = "Um Nachrichteninhalte zu formatieren, können Sie Markdowns verwenden:";
/* chat item text */
"you changed address" = "Sie haben die Adresse gewechselt";
/* chat item text */
"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";
/* snd group event chat item */
"you changed role of %@ to %@" = "Sie haben die Rolle von %1$@ auf %2$@ geändert";
/* No comment provided by engineer. */
"You control through which server(s) **to receive** the messages, your contacts the servers you use to message them." = "Sie legen fest, über welche Server Sie Ihre Nachrichten **empfangen** und an Ihre Kontakte **senden**.";
@@ -1816,7 +1911,7 @@
"You rejected group invitation" = "Sie haben die Gruppeneinladung abgelehnt";
/* snd group event chat item */
"you removed %@" = "Sie haben %@ entfernt";
"you removed %@" = "entfernt %@ aus der Gruppe";
/* No comment provided by engineer. */
"You sent group invitation" = "Sie haben eine Gruppeneinladung gesendet";
@@ -1828,14 +1923,20 @@
"you shared one-time link incognito" = "Sie haben Inkognito einen Einmal-Link geteilt";
/* 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 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 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 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, 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." = "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.";
@@ -1902,10 +2003,6 @@
/* No comment provided by engineer. */
"Your random profile" = "Ihr Zufallsprofil";
/* rcv group event chat item
snd group event chat item */
"your role: %@" = "Meine Rolle: %@";
/* No comment provided by engineer. */
"Your settings" = "Meine Einstellungen";
+115 -24
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 код или ссылку для вашего контакта.";
@@ -161,6 +167,9 @@
/* No comment provided by engineer. */
"Accept incognito" = "Принять инкогнито";
/* No comment provided by engineer. */
"Accept requests" = "Принимать запросы";
/* call status */
"accepted call" = " принятый звонок";
@@ -170,6 +179,9 @@
/* No comment provided by engineer. */
"Advanced network settings" = "Настройки сети";
/* No comment provided by engineer. */
"All group members will remain connected." = "Все члены группы, которые соединились через эту ссылку, останутся в группе.";
/* No comment provided by engineer. */
"All messages will be deleted - this cannot be undone! The messages will be deleted ONLY for you." = "Все сообщения будут удалены - это действие нельзя отменить! Сообщения будут удалены только для вас.";
@@ -206,6 +218,9 @@
/* No comment provided by engineer. */
"Auto-accept images" = "Автоприем изображений";
/* No comment provided by engineer. */
"Automatically" = "Автоматически";
/* integrity error chat item */
"bad message hash" = "ошибка хэш сообщения";
@@ -230,9 +245,6 @@
/* No comment provided by engineer. */
"Calls" = "Звонки";
/* No comment provided by engineer. */
"Can't delete contact!" = "Невозможно удалить контакт!";
/* No comment provided by engineer. */
"Can't invite contact!" = "Нельзя пригласить контакт!";
@@ -257,9 +269,30 @@
/* No comment provided by engineer. */
"Change member role?" = "Поменять роль члена группы?";
/* No comment provided by engineer. */
"Change receiving address (BETA)" = "Поменять адрес получения (БЕТА)";
/* No comment provided by engineer. */
"Change receiving address?" = "Поменять адрес получения?";
/* No comment provided by engineer. */
"Change role" = "Поменять роль";
/* chat item text */
"changed address for you" = "поменял(а) адрес для вас";
/* rcv group event chat item */
"changed role of %@ to %@" = "поменял(а) роль члена %1$@ на: %2$@";
/* rcv group event chat item */
"changed your role to %@" = "поменял(а) вашу роль на: %@";
/* chat item text */
"changing address for %@..." = "смена адреса для %@...";
/* chat item text */
"changing address..." = "смена адреса...";
/* No comment provided by engineer. */
"Chat archive" = "Архив чата";
@@ -281,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" = "Чаты";
@@ -332,6 +362,9 @@
/* No comment provided by engineer. */
"Connect via contact link?" = "Соединиться через ссылку-контакт?";
/* No comment provided by engineer. */
"Connect via group link?" = "Соединиться через ссылку группы?";
/* No comment provided by engineer. */
"Connect via link" = "Соединиться через ссылку";
@@ -398,9 +431,6 @@
/* connection information */
"connection:%@" = "connection:%@";
/* No comment provided by engineer. */
"Contact %@ cannot be deleted, they are a member of the group(s) %@." = "Контакт %@ не может быть удален, так как является членом групп(ы) %@.";
/* No comment provided by engineer. */
"Contact already exists" = "Существующий контакт";
@@ -425,6 +455,9 @@
/* No comment provided by engineer. */
"Contact name" = "Имена контактов";
/* No comment provided by engineer. */
"Contact requests" = "Запросы контактов";
/* chat item action */
"Copy" = "Скопировать";
@@ -434,6 +467,9 @@
/* No comment provided by engineer. */
"Create address" = "Создать адрес";
/* No comment provided by engineer. */
"Create link" = "Создать ссылку";
/* No comment provided by engineer. */
"Create one-time invitation link" = "Создать ссылку-приглашение";
@@ -560,6 +596,12 @@
/* No comment provided by engineer. */
"Delete invitation" = "Удалить приглашение";
/* No comment provided by engineer. */
"Delete link" = "Удалить ссылку";
/* No comment provided by engineer. */
"Delete link?" = "Удалить ссылку?";
/* No comment provided by engineer. */
"Delete message?" = "Удалить сообщение?";
@@ -701,6 +743,9 @@
/* No comment provided by engineer. */
"Error adding member(s)" = "Ошибка при добавлении членов группы";
/* No comment provided by engineer. */
"Error changing address" = "Ошибка при изменении адреса";
/* No comment provided by engineer. */
"Error changing role" = "Ошибка при изменении роли";
@@ -713,6 +758,9 @@
/* No comment provided by engineer. */
"Error creating group" = "Ошибка при создании группы";
/* No comment provided by engineer. */
"Error creating group link" = "Ошибка при создании ссылки группы";
/* No comment provided by engineer. */
"Error deleting chat database" = "Ошибка при удалении данных чата";
@@ -722,6 +770,9 @@
/* No comment provided by engineer. */
"Error deleting connection" = "Ошибка при удалении соединения";
/* No comment provided by engineer. */
"Error deleting contact" = "Ошибка при удалении контакта";
/* No comment provided by engineer. */
"Error deleting database" = "Ошибка при удалении данных чата";
@@ -803,9 +854,6 @@
/* No comment provided by engineer. */
"File: %@" = "Файл: %@";
/* No comment provided by engineer. */
"Files" = "Файлы";
/* No comment provided by engineer. */
"For console" = "Для консоли";
@@ -839,6 +887,9 @@
/* No comment provided by engineer. */
"Group invitation is no longer valid, it was removed by sender." = "Приглашение в группу больше не действительно, оно было удалено отправителем.";
/* No comment provided by engineer. */
"Group link" = "Ссылка группы";
/* notification */
"Group message:" = "Групповое сообщение:";
@@ -923,6 +974,9 @@
/* chat list item description */
"incognito via contact address link" = "инкогнито через ссылку-контакт";
/* chat list item description */
"incognito via group link" = "инкогнито через ссылку группы";
/* chat list item description */
"incognito via one-time link" = "инкогнито через одноразовую ссылку";
@@ -971,6 +1025,9 @@
/* chat list item title */
"invited to connect" = "приглашение";
/* rcv group event chat item */
"invited via your group link" = "приглашен(а) через вашу ссылку группы";
/* No comment provided by engineer. */
"iOS Keychain is used to securely store passphrase - it allows receiving push notifications." = "iOS Keychain используется для безопасного хранения пароля - это позволяет получать мгновенные уведомления.";
@@ -1055,10 +1112,6 @@
/* No comment provided by engineer. */
"Member" = "Член группы";
/* rcv group event chat item
snd group event chat item */
"member %@ role: %@" = "роль %1$@: %2$@";
/* rcv group event chat item */
"member connected" = "соединен(а)";
@@ -1272,6 +1325,9 @@
/* No comment provided by engineer. */
"Push notifications" = "Доставка уведомлений";
/* No comment provided by engineer. */
"Rate the app" = "Оценить приложение";
/* No comment provided by engineer. */
"Read" = "Прочитано";
@@ -1419,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." = "Отправитель отменил передачу файла.";
@@ -1458,9 +1517,6 @@
/* No comment provided by engineer. */
"Share one-time invitation link" = "Поделиться ссылкой-приглашением";
/* No comment provided by engineer. */
"Shared one-time link" = "Одноразовая ссылка-приглашение";
/* No comment provided by engineer. */
"Show preview" = "Показывать уведомления";
@@ -1521,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" = "Системная";
@@ -1605,6 +1664,9 @@
/* notification title */
"this contact" = "этот контакт";
/* 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)." = "Это экспериментальная функция! Она будет работать, только если на другом клиенте установлена версия 4.2. После завершения смены адреса вы увидите сообщение — убедитесь, что вы все еще можете получать сообщения от этого контакта (или члена группы).";
/* No comment provided by engineer. */
"This group no longer exists." = "Эта группа больше не существует.";
@@ -1629,6 +1691,9 @@
/* No comment provided by engineer. */
"To support instant push notifications the chat database has to be migrated." = "Для поддержки мгновенный доставки уведомлений данные чата должны быть перемещены.";
/* No comment provided by engineer. */
"Transfer images faster (BETA)" = "Передавать изображения быстрее (БЕТА)";
/* No comment provided by engineer. */
"Trying to connect to the server used to receive messages from this contact (error: %@)." = "Устанавливается соединение с сервером, через который вы получаете сообщения от этого контакта (ошибка: %@).";
@@ -1668,6 +1733,9 @@
/* No comment provided by engineer. */
"Unmute" = "Уведомлять";
/* No comment provided by engineer. */
"Unread" = "Не прочитано";
/* No comment provided by engineer. */
"Update" = "Обновить";
@@ -1710,6 +1778,9 @@
/* chat list item description */
"via contact address link" = "через ссылку-контакт";
/* chat list item description */
"via group link" = "через ссылку группы";
/* chat list item description */
"via one-time link" = "через одноразовую ссылку";
@@ -1743,6 +1814,9 @@
/* No comment provided by engineer. */
"Welcome %@!" = "Здравствуйте %@!";
/* No comment provided by engineer. */
"Welcome message" = "Приветственное сообщение";
/* No comment provided by engineer. */
"When available" = "Когда возможно";
@@ -1762,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." = "Установлено соединение с сервером, через который вы получаете сообщения от этого контакта.";
@@ -1782,6 +1856,9 @@
/* No comment provided by engineer. */
"You can set lock screen notification preview via settings." = "Вы можете установить просмотр уведомлений на экране блокировки в настройках.";
/* No comment provided by engineer. */
"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." = "Вы можете поделиться ссылкой или QR кодом - через них можно присоединиться к группе. Вы сможете удалить ссылку, сохранив членов группы, которые через нее соединились.";
/* 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." = "Вы можете использовать ваш адрес как ссылку или как QR код - кто угодно сможет соединиться с вами. Вы сможете удалить адрес, сохранив контакты, которые через него соединились.";
@@ -1791,6 +1868,18 @@
/* No comment provided by engineer. */
"You can use markdown to format messages:" = "Вы можете форматировать сообщения:";
/* chat item text */
"you changed address" = "вы поменяли адрес";
/* chat item text */
"you changed address for %@" = "вы поменяли адрес для %@";
/* snd group event chat item */
"you changed role for yourself to %@" = "вы поменяли роль себе на: %@";
/* snd group event chat item */
"you changed role of %@ to %@" = "вы поменяли роль члена %1$@ на: %2$@";
/* No comment provided by engineer. */
"You control through which server(s) **to receive** the messages, your contacts the servers you use to message them." = "Вы определяете через какие серверы вы **получаете сообщения**, ваши контакты - серверы, которые вы используете для отправки.";
@@ -1833,6 +1922,9 @@
/* chat list item description */
"you shared one-time link incognito" = "вы создали ссылку инкогнито";
/* No comment provided by engineer. */
"You will be connected to group when the group host's device is online, please wait or check later!" = "Соединение с группой будет установлено, когда хост группы будет онлайн. Пожалуйста, подождите или проверьте позже!";
/* No comment provided by engineer. */
"You will be connected when your connection request is accepted, please wait or check later!" = "Соединение будет установлено, когда ваш запрос будет принят. Пожалуйста, подождите или проверьте позже!";
@@ -1842,6 +1934,9 @@
/* No comment provided by engineer. */
"You will be required to authenticate when you start or resume the app after 30 seconds in background." = "Вы будете аутентифицированы при запуске и возобновлении приложения, которое было 30 секунд в фоновом режиме.";
/* No comment provided by engineer. */
"You will join a group this link refers to and connect to its group members." = "Вы вступите в группу, на которую ссылается эта ссылка, и соединитесь с её членами.";
/* No comment provided by engineer. */
"You will stop receiving messages from this group. Chat history will be preserved." = "Вы перестанете получать сообщения от этой группы. История чата будет сохранена.";
@@ -1908,10 +2003,6 @@
/* No comment provided by engineer. */
"Your random profile" = "Ваш случайный профиль";
/* rcv group event chat item
snd group event chat item */
"your role: %@" = "ваша роль: %@";
/* No comment provided by engineer. */
"Your settings" = "Настройки";
+2 -2
View File
@@ -25,9 +25,9 @@ main = do
welcome opts
t <- withTerminal pure
simplexChatTerminal terminalChatConfig opts t
else simplexChatCore terminalChatConfig opts Nothing $ \_ cc -> do
else simplexChatCore terminalChatConfig opts Nothing $ \user cc -> do
r <- sendChatCmd cc chatCmd
putStrLn $ serializeChatResponse r
putStrLn $ serializeChatResponse (Just user) r
threadDelay $ chatCmdDelay opts * 1000000
welcome :: ChatOpts -> IO ()
+1
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"
---

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