Compare commits

..

285 Commits

Author SHA1 Message Date
spaced4ndy cbf67f83bd Merge branch 'master-ghc8107' into master-ios 2024-01-22 18:45:17 +04:00
spaced4ndy 35cf113d98 Merge branch 'master' into master-ghc8107 2024-01-22 18:44:13 +04:00
Evgeny Poberezkin 9d38074ac0 Merge branch 'master-ghc8107' into master-ios 2024-01-20 21:37:17 +00:00
Evgeny Poberezkin a0e056994e Merge branch 'master' into master-ghc8107 2024-01-20 21:36:23 +00:00
Evgeny Poberezkin c8a4f1ad7f Merge branch 'master-ghc8107' into master-ios 2024-01-20 15:08:05 +00:00
Evgeny Poberezkin e69db17804 Merge branch 'master' into master-ghc8107 2024-01-20 15:07:20 +00:00
Evgeny Poberezkin ad2eaeb004 Merge branch 'master' into master-ghc8107 2024-01-17 19:29:49 +00:00
Evgeny Poberezkin a8c61bce67 Merge branch 'master-ghc8107' into master-ios 2024-01-10 12:09:48 +00:00
Evgeny Poberezkin 4296515473 Merge branch 'master' into master-ghc8107 2024-01-10 12:06:48 +00:00
Evgeny Poberezkin 4c42f8e922 Merge branch 'master-ghc8107' into master-ios 2024-01-09 11:08:12 +00:00
Evgeny Poberezkin c8a269e391 Merge branch 'master' into master-ghc8107 2024-01-09 11:07:32 +00:00
Evgeny Poberezkin 40fbcca512 Merge branch 'master-ghc8107' into master-ios 2023-12-30 19:32:28 +00:00
Evgeny Poberezkin 42ea1df342 Merge branch 'master' into master-ghc8107 2023-12-30 19:19:39 +00:00
Evgeny Poberezkin 2c6b0ef917 Merge branch 'master-ghc8107' into master-ios 2023-12-27 21:03:06 +00:00
Evgeny Poberezkin ff239d81e5 Merge branch 'master' into master-ghc8107 2023-12-27 21:02:11 +00:00
Evgeny Poberezkin 0a6e47dd61 Merge branch 'master' into master-ghc8107 2023-12-27 20:57:25 +00:00
Evgeny Poberezkin ee75219ed3 core: 5.4.2.1 (simplexmq 5.5.0.2) 2023-12-27 20:57:05 +00:00
Evgeny Poberezkin 5d8dd8ffd3 Merge branch 'master-ghc8107' into master-ios 2023-12-27 14:10:02 +00:00
Evgeny Poberezkin 9918d91def Merge branch 'master' into master-ghc8107 2023-12-27 14:09:23 +00:00
Evgeny Poberezkin 235b220378 Merge branch 'master-ghc8107' into master-ios 2023-12-26 21:27:34 +00:00
Evgeny Poberezkin de637fab50 Merge branch 'master' into master-ghc8107 2023-12-26 21:12:36 +00:00
Evgeny Poberezkin 6a8dba25f0 Merge branch 'master-ghc8107' into master-ios 2023-12-23 19:57:49 +00:00
Evgeny Poberezkin 6d0a83aa58 Merge branch 'master' into master-ghc8107 2023-12-23 19:57:08 +00:00
Evgeny Poberezkin decfbdb07b Merge branch 'master-ghc8107' into master-ios 2023-12-23 17:25:40 +00:00
Evgeny Poberezkin 6a66525927 Merge branch 'master' into master-ghc8107 2023-12-23 17:24:47 +00:00
Evgeny Poberezkin 9becf48db6 Merge branch 'master-ghc8107' into master-ios 2023-12-23 15:13:16 +00:00
Evgeny Poberezkin f5ed8debcc Merge branch 'master' into master-ghc8107 2023-12-23 15:11:38 +00:00
Evgeny Poberezkin 355d2449c5 fix for GHC 8.10.7 2023-12-23 15:05:36 +00:00
Evgeny Poberezkin f7382cdd6f Merge branch 'master' into master-ghc8107 2023-12-23 13:52:44 +00:00
Evgeny Poberezkin 2590fc0c80 Merge branch 'master-ghc8107' into master-ios 2023-12-21 10:00:44 +00:00
Evgeny Poberezkin 521b901cc9 Merge branch 'master' into master-ghc8107 (without changes for iPhone7) 2023-12-21 09:59:25 +00:00
Evgeny Poberezkin cdd735930d Merge branch 'master-ghc8107' into master-ios 2023-12-21 00:45:46 +00:00
Evgeny Poberezkin 754c76d6fd Merge branch 'master' into master-ghc8107 2023-12-21 00:45:10 +00:00
Evgeny Poberezkin aea7ff1c89 nix: fix script 2023-12-13 20:27:58 +00:00
Evgeny Poberezkin f16da09213 Merge branch 'master-ghc8107' into master-ios 2023-12-13 12:36:22 +00:00
Evgeny Poberezkin 256f85024f Merge branch 'master' into master-ghc8107 2023-12-13 12:35:37 +00:00
Evgeny Poberezkin 870f9e42dd Merge branch 'master' into master-ghc8107 2023-12-12 20:09:39 +00:00
Evgeny Poberezkin a993a26920 Merge branch 'master-ghc8107' into master-ios 2023-12-11 17:37:17 +00:00
Evgeny Poberezkin 8a66390a78 fix for GHC 8.10.7 2023-12-11 16:14:56 +00:00
Evgeny Poberezkin aff589becd Merge branch 'master-ghc8107' into master-ios 2023-12-11 14:57:49 +00:00
Evgeny Poberezkin 6546426ec0 Merge branch 'master' into master-ghc8107 2023-12-11 14:56:25 +00:00
spaced4ndy 5125b27938 Merge branch 'master-ghc8107' into master-ios 2023-12-11 17:53:26 +04:00
spaced4ndy 53560378bb Merge branch 'master' into master-ghc8107 2023-12-11 17:52:48 +04:00
Evgeny Poberezkin 28b7bad69f Merge branch 'master-ghc8107' into master-ios 2023-12-07 15:13:12 +00:00
Evgeny Poberezkin 9f3d3e8ba4 Merge branch 'master' into master-ghc8107 2023-12-07 15:00:23 +00:00
Evgeny Poberezkin 4efd505fd3 Merge branch 'master-ghc8107' into master-ios 2023-12-04 12:32:23 +00:00
Evgeny Poberezkin 087acd9180 changes to support GHC 8.10.7 (#3512)
* Revert "raise lower bound on mtl to a real version (#3499)"

This reverts commit f94c0311c1.

* Revert "core: expand ranges to fit ghc 8.10 & 9.6 (#3496)"

This reverts commit 9a1c7f41f7.

* update simplexmq

* remove netword-transport fork

* compatibility with GHC 8.10.7

* simplexmq

* fix test

* simplexmq, deps

* update sqlcipher deps in sha256nix

* fix index-state in cabal.project

* index-state

* remove import

* add cabal.project.freeze

* simplexmq

* remove freeze

* simplexmq

* bytestring,simplexmq

* template-haskell, simplexmq

* simplexmq

* simplexmq

* simplexmq

* mtl

* simplexmq

* remove duplicate index-state
2023-12-04 12:29:49 +00:00
Evgeny Poberezkin 0b822e4a5c Merge branch 'master' into master-ghc8107 2023-12-04 10:07:16 +00:00
Evgeny Poberezkin f8a469488e Merge branch 'master' into master-ghc8107 2023-12-02 12:30:36 +00:00
Evgeny Poberezkin 3b5e806418 Merge branch 'master' into master-ghc8107 2023-12-01 17:46:12 +00:00
spaced4ndy bcb2e9fb9b Merge branch 'master-ghc8107' into master-ios 2023-11-30 21:11:07 +04:00
spaced4ndy ef5c13b1c1 Merge branch 'master' into master-ghc8107 2023-11-30 21:10:51 +04:00
spaced4ndy 48056d7452 Merge branch 'master-ghc8107' into master-ios 2023-11-30 20:57:16 +04:00
spaced4ndy 38533213d2 Merge branch 'master' into master-ghc8107 2023-11-30 20:56:51 +04:00
spaced4ndy b5a99b2b14 Merge branch 'master-ghc8107' into master-ios 2023-11-27 19:17:56 +04:00
spaced4ndy b0002fe07d Merge branch 'master' into master-ghc8107 2023-11-27 19:17:39 +04:00
Evgeny Poberezkin 5899b8f734 Merge branch 'master-ghc8107' into master-ios 2023-11-26 20:00:03 +00:00
Evgeny Poberezkin 4a311b9578 fix for ghc 8.10.7 2023-11-26 19:34:39 +00:00
Evgeny Poberezkin b8da5e225b Merge branch 'master' into master-ghc8107 2023-11-26 18:53:40 +00:00
Evgeny Poberezkin 9e9a6ac64e Merge branch 'master' into master-ios 2023-11-26 11:51:34 +00:00
Evgeny Poberezkin 5cc537f14c Merge branch 'master' into master-ghc8107 2023-11-26 11:50:50 +00:00
Evgeny Poberezkin 1bbaffe0a6 Merge branch 'master-ghc8107' into master-ios 2023-11-24 20:02:30 +00:00
Evgeny Poberezkin 0c096e2c89 Merge branch 'master' into master-ghc8107 2023-11-24 19:00:30 +00:00
Evgeny Poberezkin 57eea0947f Merge branch 'master' into master-ios 2023-11-23 16:23:30 +00:00
Evgeny Poberezkin 1781495ee3 Merge branch 'master' into master-ghc8107 2023-11-23 16:22:46 +00:00
Evgeny Poberezkin 006ab7c46a Merge branch 'master-ghc8107' into master-ios 2023-11-21 21:15:39 +00:00
Evgeny Poberezkin 45102442f4 Merge branch 'master' into master-ghc8107 2023-11-21 21:15:10 +00:00
spaced4ndy 45026b6bf5 Merge branch 'master-ghc8107' into master-ios 2023-11-21 19:42:06 +04:00
spaced4ndy 3bdc6b5e28 Merge branch 'master' into master-ghc8107 2023-11-21 19:41:06 +04:00
Evgeny Poberezkin 12f38028f3 Merge branch 'master-ghc8107' into master-ios 2023-11-21 00:01:53 +00:00
Evgeny Poberezkin 3597d34716 Merge branch 'master' into master-ghc8107 2023-11-21 00:00:59 +00:00
Evgeny Poberezkin 55ead740cc update hpack 2023-11-20 14:43:05 +00:00
Evgeny Poberezkin 4fecfe825a update hpack 2023-11-20 14:34:25 +00:00
Evgeny Poberezkin fad4cbf7e5 Merge branch 'master-ghc8107' into master-ios 2023-11-20 14:30:44 +00:00
Evgeny Poberezkin 49a9b0e7d6 update hpack version 2023-11-20 14:30:10 +00:00
Evgeny Poberezkin b6b23d6ddd Merge branch 'master-ghc8107' into master-ios 2023-11-20 13:25:10 +00:00
Evgeny Poberezkin 307a1b3c5e fix for ghc 8.10.7 2023-11-20 13:23:45 +00:00
Evgeny Poberezkin ed6b3bbead Merge branch 'master' into master-ghc8107 2023-11-20 13:01:22 +00:00
spaced4ndy 9ecd5ff1b2 Merge branch 'master-ghc8107' into master-ios 2023-11-20 14:07:45 +04:00
spaced4ndy 7d4127c51d Merge branch 'master' into master-ghc8107 2023-11-20 14:07:08 +04:00
Evgeny Poberezkin 0297cc43d0 Merge branch 'master-ghc8107' into master-ios 2023-11-20 00:07:33 +00:00
Evgeny Poberezkin e1a8099474 fix for GHC 8.10.7 2023-11-20 00:06:45 +00:00
Evgeny Poberezkin daa8d9bb21 Merge branch 'master' into master-ghc8107 2023-11-19 23:42:13 +00:00
Evgeny Poberezkin 20dcfbbffe Merge branch 'master-ghc8107' into master-ios 2023-11-17 11:48:16 +00:00
Evgeny Poberezkin 3937ffa9a6 Merge branch 'master' into master-ghc8107 2023-11-17 11:47:52 +00:00
Evgeny Poberezkin 909ff6516b Merge branch 'master-ghc8107' into master-ios 2023-11-16 18:55:39 +00:00
Evgeny Poberezkin f6e66f1c53 Merge branch 'master' into master-ghc8107 2023-11-16 18:13:02 +00:00
Evgeny Poberezkin bfb6dfb2c5 Merge branch 'master-ghc8107' into master-ios 2023-11-11 13:57:13 +00:00
Evgeny Poberezkin 1570bc2b99 Merge branch 'master' into master-ghc8107 2023-11-11 13:56:53 +00:00
Evgeny Poberezkin 2f1c7400e9 Merge branch 'master-ghc8107' into master-ios 2023-11-11 09:52:27 +00:00
Evgeny Poberezkin f3014f258d Merge branch 'master' into master-ghc8107 2023-11-11 09:51:42 +00:00
Evgeny Poberezkin 168a31c9d7 Merge branch 'master-ghc8107' into master-ios 2023-11-10 21:22:56 +00:00
Evgeny Poberezkin 74b78a8d7b Merge branch 'master' into master-ghc8107 2023-11-10 21:11:08 +00:00
Evgeny Poberezkin 8e33b92f39 Merge branch 'master-ghc8107' into master-ios 2023-11-10 21:08:36 +00:00
Evgeny Poberezkin fe4eb7b5af Merge branch 'master' into master-ghc8107 without changes, to skip update for ghc 9.6.3 2023-11-10 21:04:20 +00:00
Evgeny Poberezkin 3c35dcb4f2 Merge branch 'master-ghc8107' into master-ios 2023-11-06 11:27:10 +00:00
Evgeny Poberezkin 2516d5a393 Merge branch 'master' into master-ghc8107 2023-11-06 11:26:22 +00:00
spaced4ndy 552dc9ab64 Merge branch 'master-ghc8107' into master-ios 2023-11-06 11:42:23 +04:00
spaced4ndy 4253cd7fb9 Merge branch 'master' into master-ghc8107 2023-11-06 11:41:55 +04:00
Evgeny Poberezkin 916e14e44c Merge branch 'master-ghc8107' into master-ios 2023-11-04 13:39:57 +00:00
Evgeny Poberezkin 1f5b80d560 fix for ghc8107 2023-11-04 13:37:25 +00:00
Evgeny Poberezkin 2de111e76c Merge branch 'master' into master-ghc8107 2023-11-04 13:02:08 +00:00
spaced4ndy c27e7fb68d Merge branch 'master-ghc8107' into master-ios 2023-10-30 21:00:55 +04:00
spaced4ndy 5dbe2b2745 Merge branch 'master' into master-ghc8107 2023-10-30 21:00:11 +04:00
Evgeny Poberezkin c84d96b534 Merge branch 'master-ghc8107' into master-ios 2023-10-29 18:27:26 +00:00
Evgeny Poberezkin 6881600e06 Merge branch 'master' into master-ghc8107 2023-10-29 18:24:13 +00:00
spaced4ndy e2a6ff28fb Merge branch 'master-ghc8107' into master-ios 2023-10-25 10:47:53 +04:00
spaced4ndy 9ded1c9821 Merge branch 'master' into master-ghc8107 2023-10-25 10:47:35 +04:00
spaced4ndy aaab15175d Merge branch 'master-ghc8107' into master-ios 2023-10-24 18:14:10 +04:00
spaced4ndy c3e82a6a4e Merge branch 'master' into master-ghc8107 2023-10-24 18:13:56 +04:00
spaced4ndy 64b4d6c6f6 Merge branch 'master-ghc8107' into master-ios 2023-10-24 17:41:36 +04:00
spaced4ndy e7e66ff873 Merge branch 'master' into master-ghc8107 2023-10-24 17:40:55 +04:00
Evgeny Poberezkin 567ba1974e Merge branch 'master-ghc8107' into master-ios 2023-10-22 15:35:27 +01:00
Evgeny Poberezkin d6b9a45a39 Merge branch 'master' into master-ghc8107 2023-10-22 15:10:33 +01:00
Evgeny Poberezkin 64f429f4d2 Merge branch 'master-ghc8107' into master-ios 2023-10-18 22:44:49 +01:00
Evgeny Poberezkin 4004aafbc5 Merge branch 'master' into master-ghc8107 2023-10-18 22:44:27 +01:00
spaced4ndy b30b1d67d5 Merge branch 'master-ghc8107' into master-ios 2023-10-16 20:05:35 +04:00
spaced4ndy c7a8992043 core: fix compilation for ghc 8.10.7 2023-10-16 20:05:13 +04:00
spaced4ndy 48e7418c3a Merge branch 'master-ghc8107' into master-ios 2023-10-16 19:28:31 +04:00
spaced4ndy ed9f277421 Merge branch 'master' into master-ghc8107 2023-10-16 19:28:06 +04:00
Evgeny Poberezkin e0d71dd784 Merge branch 'master-ghc8107' into master-ios 2023-10-15 18:53:52 +01:00
Evgeny Poberezkin d8fb31f167 Merge branch 'master' into master-ghc8107 2023-10-15 18:53:23 +01:00
Evgeny Poberezkin a4e07cbbb3 Merge branch 'master-ghc8107' into master-ios 2023-10-11 21:58:42 +01:00
Evgeny Poberezkin 7692195bfa core: fix for ghc 8.10.7 2023-10-11 21:57:53 +01:00
Evgeny Poberezkin b4a72fafaa Merge branch 'master-ghc8107' into master-ios 2023-10-11 21:27:37 +01:00
Evgeny Poberezkin effc281271 Merge branch 'master' into master-ghc8107 2023-10-11 21:27:21 +01:00
spaced4ndy a5c81d8c77 Merge branch 'master-ghc8107' into master-ios 2023-10-11 13:22:10 +04:00
spaced4ndy 67d74a0a27 Merge branch 'master' into master-ghc8107 2023-10-11 13:21:46 +04:00
Evgeny Poberezkin bae6a53ec8 Merge branch 'master-ghc8107' into master-ios 2023-10-09 17:32:16 +01:00
Evgeny Poberezkin 74d186af16 Merge branch 'master' into master-ghc8107 2023-10-09 17:31:27 +01:00
Evgeny Poberezkin 373da2b528 Merge branch 'master-ghc8107' into master-ios 2023-10-09 14:05:41 +01:00
Evgeny Poberezkin 4782cab507 Merge branch 'master' into master-ghc8107 2023-10-09 14:05:04 +01:00
Evgeny Poberezkin d19d2d58a3 Merge branch 'master-ghc8107' into master-ios 2023-10-08 08:41:36 +01:00
Evgeny Poberezkin 2501cbe55d Merge branch 'master' into master-ghc8107 2023-10-08 08:38:02 +01:00
Evgeny Poberezkin 48158a9db1 Merge branch 'master-ghc8107' into master-ios 2023-10-07 21:10:44 +01:00
Evgeny Poberezkin 6b8b9ab4fd Merge branch 'master' into master-ghc8107 2023-10-07 19:06:38 +01:00
Evgeny Poberezkin 25972f8f6c Merge branch 'master-ghc8107' into master-ios 2023-10-02 23:05:10 +01:00
Evgeny Poberezkin 316d605899 Merge branch 'master' into master-ghc8107 2023-10-02 23:04:13 +01:00
Evgeny Poberezkin 8ac12a58c6 Merge branch 'master-ghc8107' into master-ios 2023-10-01 13:22:14 +01:00
Evgeny Poberezkin a3f2d5c919 Merge branch 'master' into master-ghc8107 2023-10-01 13:20:06 +01:00
Evgeny Poberezkin 4de3d6a8f4 Merge branch 'master-ghc8107' into master-ios 2023-10-01 11:20:19 +01:00
Evgeny Poberezkin 0312fde818 Merge branch 'master' into master-ghc8107 2023-10-01 11:19:27 +01:00
Evgeny Poberezkin f7478f5699 Merge branch 'master-ghc8107' into master-ios 2023-09-29 13:15:50 +01:00
Evgeny Poberezkin 915b53054c Merge branch 'master' into master-ghc8107 2023-09-29 13:14:57 +01:00
Evgeny Poberezkin dce29e91d4 Merge branch 'master-ghc8107' into master-ios 2023-09-27 22:10:30 +01:00
Evgeny Poberezkin e273bd1239 Merge branch 'master' into master-ghc8107 2023-09-27 22:04:00 +01:00
spaced4ndy 760ab4a45c Merge branch 'master-ghc8107' into master-ios 2023-09-27 20:38:19 +04:00
spaced4ndy e7f0234134 Merge branch 'master' into master-ghc8107 2023-09-27 20:11:39 +04:00
Evgeny Poberezkin 9c99e4fae2 Merge branch 'master-ghc8107' into master-ios 2023-09-27 16:07:08 +01:00
Evgeny Poberezkin 98a3fc214d Merge branch 'master' into master-ghc8107 2023-09-27 16:04:25 +01:00
Evgeny Poberezkin dacc075fe8 Merge branch 'master' into master-ghc8107 2023-09-25 16:52:33 +01:00
spaced4ndy c931f52631 Merge branch 'master-ghc8107' into master-ios 2023-09-25 17:44:12 +04:00
spaced4ndy f2b5c0f3a8 Merge branch 'master' into master-ghc8107 2023-09-25 17:43:37 +04:00
spaced4ndy 2d8739dec2 Merge branch 'master-ghc8107' into master-ios 2023-09-25 17:07:43 +04:00
spaced4ndy 8e045764df Merge branch 'master' into master-ghc8107 2023-09-25 16:40:08 +04:00
Evgeny Poberezkin 87aff89d5e Merge branch 'master-ghc8107' into master-ios 2023-09-22 17:23:28 +01:00
Evgeny Poberezkin 81bd7d97c5 Merge branch 'master' into master-ghc8107 2023-09-22 17:21:54 +01:00
Evgeny Poberezkin ff182c97ee Merge branch 'master-ghc8107' into master-ios 2023-09-22 14:15:01 +01:00
Evgeny Poberezkin 8f57925067 compatibility with GHC 8.10.7 2023-09-22 14:01:25 +01:00
Evgeny Poberezkin 9bf99db82e Merge branch 'master' into master-ghc8107 2023-09-22 13:46:50 +01:00
Evgeny Poberezkin 9f117a30db Merge branch 'master-android' into master-ios 2023-09-21 17:05:05 +01:00
Evgeny Poberezkin 5615cdbf1a Merge branch 'master' into master-android 2023-09-21 17:04:47 +01:00
Evgeny Poberezkin 2cab235888 Merge branch 'master-android' into master-ios 2023-09-21 12:06:35 +01:00
Evgeny Poberezkin d802ae0058 Merge branch 'master' into master-android 2023-09-21 12:06:10 +01:00
Evgeny Poberezkin d4201071e0 Merge branch 'master-android' into master-ios 2023-09-20 14:56:57 +01:00
Evgeny Poberezkin 8f2278198c Merge branch 'master' into master-android 2023-09-20 14:55:25 +01:00
spaced4ndy 10937a5a4e Merge branch 'master' into master-android 2023-09-20 17:36:53 +04:00
Evgeny Poberezkin c0e666770b Merge branch 'master-android' into master-ios 2023-09-18 21:58:07 +01:00
Evgeny Poberezkin 6aff6e9804 Merge branch 'master-ghc9' into master-ghc8107 2023-09-18 21:56:35 +01:00
Evgeny Poberezkin 95477cae7e core: use commit from simplexmq branch master-ghc8107 2023-09-18 21:45:50 +01:00
Evgeny Poberezkin 016f3c9670 Merge branch 'master' into master-ios 2023-09-18 21:30:32 +01:00
spaced4ndy 49116233a8 Merge branch 'master' into master-ios 2023-09-18 14:12:37 +04:00
Evgeny Poberezkin e54419e4f0 Merge branch 'master' into master-ios 2023-09-17 22:41:16 +01:00
spaced4ndy d6c6da6edf Merge branch 'master' into master-ios 2023-09-12 18:14:58 +04:00
spaced4ndy e5828f4495 Merge branch 'master' into master-ios 2023-09-11 18:39:29 +04:00
Evgeny Poberezkin 5e23bbecf1 Merge branch 'master' into master-ios 2023-09-10 21:13:18 +01:00
Evgeny Poberezkin 17c734dcb9 Merge branch 'master' into master-ios 2023-09-07 20:19:20 +01:00
Evgeny Poberezkin 85adf2951e Merge branch 'master' into master-ios 2023-09-07 13:45:06 +01:00
Evgeny Poberezkin a250a8e6ca Merge branch 'master' into master-ios 2023-09-06 20:22:00 +01:00
Evgeny Poberezkin 38e10d843b Merge branch 'master' into master-ios 2023-09-06 19:54:48 +01:00
Evgeny Poberezkin 9153cb93de Merge branch 'master' into master-ios 2023-09-05 21:07:22 +01:00
Evgeny Poberezkin f978198fe6 Merge branch 'master' into master-ios 2023-09-04 23:20:27 +01:00
Evgeny Poberezkin 7b434d9f97 Merge branch 'master' into master-ios 2023-09-04 18:29:53 +01:00
Evgeny Poberezkin aaf6dd57c7 Merge branch 'master' into master-ios 2023-09-02 23:34:35 +01:00
Evgeny Poberezkin dc2272ac6e Merge branch 'master' into master-ios 2023-09-01 22:27:15 +01:00
Evgeny Poberezkin 4c172ac680 Merge branch 'master' into master-ios 2023-09-01 20:40:14 +01:00
Evgeny Poberezkin 096bd9ea72 Merge branch 'master' into master-ios 2023-08-26 18:37:12 +01:00
Evgeny Poberezkin f8b757ddf9 Merge branch 'master' into master-ios 2023-08-25 14:11:13 +01:00
Evgeny Poberezkin 62a4fd2c7c Merge branch 'master' into master-ios 2023-08-25 11:32:07 +01:00
Evgeny Poberezkin d6dba0e26a Merge branch 'master' into master-ios 2023-08-24 20:43:20 +01:00
Evgeny Poberezkin 20ebccdbf2 Merge branch 'master' into master-ios 2023-08-22 16:14:32 +01:00
Evgeny Poberezkin 2ac6c2dec9 Merge branch 'master' into master-ios 2023-08-18 21:36:18 +01:00
Evgeny Poberezkin 2babe6d548 Merge branch 'master' into master-ios 2023-08-17 23:16:40 +01:00
spaced4ndy 0a3f2444d9 Merge branch 'master' into master-ios 2023-08-16 10:44:15 +04:00
Evgeny Poberezkin ebfa9e699b Merge branch 'master' into master-ios 2023-08-12 22:00:40 +01:00
Evgeny Poberezkin 2cf07ae0a7 Merge branch 'master' into master-ios 2023-08-12 18:16:16 +01:00
Evgeny Poberezkin 5199cb9e82 Merge branch 'master' into master-ios 2023-08-12 15:26:18 +01:00
spaced4ndy 4bf43c3cde Merge branch 'master' into master-ios 2023-08-08 19:05:23 +04:00
Evgeny Poberezkin b2523e07f8 Merge branch 'master' into master-ios 2023-08-05 15:16:34 +01:00
spaced4ndy deb4fae07c Merge branch 'master' into master-ios 2023-08-03 18:28:23 +04:00
Evgeny Poberezkin 1f4e99f291 Merge branch 'master' into master-ios 2023-08-02 16:23:36 +01:00
Evgeny Poberezkin 0f2ac69562 Merge branch 'master' into master-ios 2023-07-31 16:13:41 +01:00
spaced4ndy 4c3bab1402 Merge branch 'master' into master-ios 2023-07-28 11:10:08 +04:00
spaced4ndy 281f6d0bf9 Merge branch 'master' into master-ios 2023-07-26 15:03:26 +04:00
Evgeny Poberezkin 1022124f69 Merge branch 'stable' into master-ios 2023-07-21 23:31:58 +01:00
Evgeny Poberezkin 5803274950 Merge branch 'master' into master-ios 2023-07-20 18:20:12 +01:00
spaced4ndy 444c5e2fc4 Merge branch 'master' into master-ios 2023-07-20 18:44:13 +04:00
Evgeny Poberezkin 1038305802 Merge branch 'master' into master-ios 2023-07-17 14:45:16 +01:00
Evgeny Poberezkin f9530c70fc Merge branch 'master' into master-ios 2023-07-17 13:34:22 +01:00
Evgeny Poberezkin 5e0b2a8706 Merge branch 'master' into master-ios 2023-07-13 23:48:55 +01:00
spaced4ndy 7e9990d710 Merge branch 'master' into master-ios 2023-07-11 20:57:44 +04:00
spaced4ndy 27244da82a Merge branch 'master' into master-ios 2023-07-10 17:40:43 +04:00
Evgeny Poberezkin d89c6ae672 Merge branch 'master' into master-ios 2023-07-09 23:25:16 +01:00
spaced4ndy 121e0ede6c Merge branch 'master' into master-ios 2023-07-05 19:45:23 +04:00
Evgeny Poberezkin ae7a71c4a4 Merge branch 'master' into master-ios 2023-07-05 09:10:43 +01:00
spaced4ndy 8df46c8630 Merge branch 'master' into master-ios 2023-07-03 20:16:45 +04:00
spaced4ndy 55a17a564e Merge branch 'master' into master-ios 2023-06-28 16:49:43 +04:00
spaced4ndy e0956b656c Merge branch 'master' into master-ios 2023-06-28 16:13:45 +04:00
spaced4ndy ca0192abfc Merge branch 'master' into master-ios 2023-06-26 21:48:51 +04:00
Evgeny Poberezkin 3eec371c3f Merge branch 'master' into master-ios 2023-06-24 17:40:05 +01:00
spaced4ndy 1c21afddff Merge branch 'master' into master-ios 2023-06-20 10:19:40 +04:00
spaced4ndy 94b3adb515 Merge branch 'master' into master-ios 2023-06-19 16:07:45 +04:00
spaced4ndy 4c7784c9dc Merge branch 'master' into master-ios 2023-06-16 19:06:31 +04:00
spaced4ndy 99b39fda91 Merge branch 'master' into master-ios 2023-06-12 13:47:27 +04:00
spaced4ndy c16bd3f7b1 Merge branch 'master' into master-ios 2023-06-09 17:31:57 +04:00
spaced4ndy 1c49ba9eaf Merge branch 'master' into master-ios 2023-05-29 15:22:06 +04:00
Evgeny Poberezkin b83fb77b9e Merge branch 'master' into master-ios 2023-05-27 19:36:22 +01:00
spaced4ndy 5cac9c4594 Merge branch 'master' into master-ios 2023-05-26 17:36:58 +04:00
spaced4ndy d17623d20b Merge branch 'master' into master-ios 2023-05-26 14:06:07 +04:00
spaced4ndy 3b452ae4af Merge branch 'master' into master-ios 2023-05-26 14:04:05 +04:00
spaced4ndy 08ba0b2755 Merge branch 'master' into master-ios 2023-05-25 20:55:25 +04:00
spaced4ndy abd362f10a Merge branch 'master' into master-ios 2023-05-24 16:17:05 +04:00
spaced4ndy 31b47746e5 Merge branch 'master' into master-ios 2023-05-23 19:12:25 +04:00
spaced4ndy 76ce8bec1d Merge branch 'master' into master-ios 2023-05-23 18:40:52 +04:00
spaced4ndy eeb2fee659 Merge branch 'master' into master-ios 2023-05-23 15:55:11 +04:00
spaced4ndy d034cb08f9 Merge branch 'master' into master-ios 2023-05-23 13:51:52 +04:00
Evgeny Poberezkin 9d9761ad98 Merge branch 'master' into master-ios 2023-05-20 23:09:20 +01:00
spaced4ndy ffdd598a9c Merge branch 'master' into master-ios 2023-05-19 17:00:44 +04:00
Evgeny Poberezkin e4649c4776 Merge branch 'master' into master-ios 2023-05-19 13:53:30 +01:00
Evgeny Poberezkin 3e9df58955 Merge branch 'master' into master-ios 2023-05-18 17:13:13 +01:00
spaced4ndy b014bfcdb4 Merge branch 'master' into master-ios 2023-05-17 16:14:18 +04:00
Evgeny Poberezkin b8b4ef39e9 Merge branch 'master' into master-ios 2023-05-17 00:23:36 +01:00
spaced4ndy 2a9f98e43f Merge branch 'master' into master-ios 2023-05-16 15:06:01 +04:00
spaced4ndy 6e0ec89fdc Merge branch 'master' into master-ios 2023-05-15 21:23:22 +04:00
Evgeny Poberezkin 080ab90e26 Merge branch 'master' into master-ios 2023-05-15 13:28:27 +01:00
spaced4ndy a6d1f7d449 Merge branch 'master' into master-ios 2023-05-11 16:00:32 +04:00
Evgeny Poberezkin 19c94e59e6 Merge branch 'master' into master-ios 2023-05-09 09:34:05 +01:00
spaced4ndy 1bb4672815 Merge branch 'master' into master-ios 2023-05-09 11:00:46 +04:00
spaced4ndy 371db95950 Merge branch 'master' into master-ios 2023-05-05 15:55:54 +04:00
Evgeny Poberezkin 672c807e47 Merge branch 'master' into master-ios 2023-05-04 13:28:20 +01:00
spaced4ndy 512394b8a6 Merge branch 'master' into master-ios 2023-05-03 20:26:46 +04:00
spaced4ndy 7d3c188cd2 Merge branch 'master' into master-ios 2023-05-03 15:31:03 +04:00
spaced4ndy 2fe765b0bc Merge branch 'master' into master-ios 2023-05-02 10:25:26 +04:00
Evgeny Poberezkin 40cf101148 Merge branch 'master' into master-ios 2023-05-01 11:34:16 +01:00
spaced4ndy 680903a763 Merge branch 'master' into master-ios 2023-04-26 14:35:44 +04:00
spaced4ndy 6b6eff6b52 Merge branch 'master' into master-ios 2023-04-21 13:48:21 +04:00
spaced4ndy e949b4fc3c Merge branch 'master' into master-ios 2023-04-20 20:28:49 +04:00
Evgeny Poberezkin 6790362036 Merge branch 'master' into master-ios 2023-04-20 14:19:27 +01:00
spaced4ndy 623b84d3e1 Merge branch 'master' into master-ios 2023-04-19 15:21:51 +04:00
Evgeny Poberezkin 101ab76a33 Merge branch 'master' into master-ios 2023-04-18 21:04:45 +01:00
spaced4ndy c1909b91d0 Merge branch 'master' into master-ios 2023-04-18 19:49:26 +04:00
spaced4ndy cb9cf6229c Merge branch 'master' into master-ios 2023-04-18 13:49:47 +04:00
spaced4ndy 1561268c5e Merge branch 'master' into master-ios 2023-04-18 12:49:17 +04:00
Evgeny Poberezkin 80368f97ef Merge branch 'master' into master-ios 2023-04-17 10:18:27 +01:00
Evgeny Poberezkin 7ec6f2b421 Merge branch 'master' into master-ios 2023-04-16 18:31:05 +01:00
Evgeny Poberezkin d3a1aec7ad Merge branch 'master' into master-ios 2023-04-16 15:04:35 +01:00
spaced4ndy 990f96a19f Merge branch 'master' into master-ios 2023-04-14 18:06:36 +04:00
spaced4ndy 330c9afc1b Merge branch 'master' into master-ios 2023-04-14 15:55:38 +04:00
spaced4ndy 93922a592c Merge branch 'master' into master-ios 2023-04-13 20:49:23 +04:00
Evgeny Poberezkin a9b08e5c2f Merge branch 'master' into master-ios 2023-04-07 23:23:26 +01:00
Evgeny Poberezkin c677baaa22 Merge branch 'master' into master-ios 2023-04-06 23:16:48 +01:00
Evgeny Poberezkin 5448573d0b Merge branch 'master' into master-ios 2023-04-06 22:49:53 +01:00
Evgeny Poberezkin 1a1b5aade5 Merge branch 'master' into master-ios 2023-04-06 21:07:07 +01:00
Evgeny Poberezkin bd9a60af5a Merge branch 'master' into master-ios 2023-04-05 22:00:08 +01:00
Evgeny Poberezkin 617e12a3d4 Merge branch 'master' into master-ios 2023-04-04 18:43:42 +01:00
spaced4ndy 8c625faa5e Merge branch 'master' into master-ios 2023-03-31 19:20:03 +04:00
spaced4ndy cab7201b72 Merge branch 'master' into master-ios 2023-03-31 17:36:32 +04:00
spaced4ndy c2fd1d5d29 Merge branch 'master' into master-ios 2023-03-30 19:45:49 +04:00
spaced4ndy 5b541e296b Merge branch 'master' into master-ios 2023-03-30 18:42:13 +04:00
Evgeny Poberezkin daaecf11ec Merge branch 'master' into master-ios 2023-03-29 20:46:34 +01:00
spaced4ndy 9c4df591a7 Merge branch 'master' into master-ios 2023-03-29 19:17:15 +04:00
Evgeny Poberezkin c339e8290b Merge branch 'master' into master-ios 2023-03-28 19:26:26 +01:00
spaced4ndy ac606336ed Merge branch 'master' into master-ios 2023-03-28 19:29:30 +04:00
Evgeny Poberezkin f79e6fddec Merge branch 'master' into master-ios 2023-03-27 23:22:16 +01:00
Evgeny Poberezkin 2aab931589 nix: revert changes from 05c4a6c (support for ARMv7a and Android 8+) 2023-03-27 20:48:49 +01:00
203 changed files with 3349 additions and 6390 deletions
+16 -25
View File
@@ -1,41 +1,32 @@
ARG TAG=22.04
FROM ubuntu:focal AS build
FROM ubuntu:${TAG} AS build
### Build stage
# Install curl and git and simplex-chat dependencies
RUN apt-get update && apt-get install -y curl git build-essential libgmp3-dev zlib1g-dev llvm-12 llvm-12-dev libnuma-dev libssl-dev
# Specify bootstrap Haskell versions
ENV BOOTSTRAP_HASKELL_GHC_VERSION=9.6.3
ENV BOOTSTRAP_HASKELL_CABAL_VERSION=3.10.1.0
# Install curl and simplex-chat-related dependencies
RUN apt-get update && apt-get install -y curl git build-essential libgmp3-dev zlib1g-dev libssl-dev
# Install ghcup
RUN curl --proto '=https' --tlsv1.2 -sSf https://get-ghcup.haskell.org | BOOTSTRAP_HASKELL_NONINTERACTIVE=1 sh
# Adjust PATH
ENV PATH="/root/.cabal/bin:/root/.ghcup/bin:$PATH"
RUN a=$(arch); curl https://downloads.haskell.org/~ghcup/$a-linux-ghcup -o /usr/bin/ghcup && \
chmod +x /usr/bin/ghcup
# Install ghc
RUN ghcup install ghc 9.6.3
# Install cabal
RUN ghcup install cabal 3.10.1.0
# Set both as default
RUN ghcup set ghc "${BOOTSTRAP_HASKELL_GHC_VERSION}" && \
ghcup set cabal "${BOOTSTRAP_HASKELL_CABAL_VERSION}"
RUN ghcup set ghc 9.6.3 && \
ghcup set cabal 3.10.1.0
COPY . /project
WORKDIR /project
# Adjust PATH
ENV PATH="/root/.cabal/bin:/root/.ghcup/bin:$PATH"
# Adjust build
RUN cp ./scripts/cabal.project.local.linux ./cabal.project.local
# Compile simplex-chat
RUN cabal update
RUN cabal build exe:simplex-chat
RUN cabal install
# Strip the binary from debug symbols to reduce size
RUN bin=$(find /project/dist-newstyle -name "simplex-chat" -type f -executable) && \
mv "$bin" ./ && \
strip ./simplex-chat
# Copy compiled app from build stage
FROM scratch AS export-stage
COPY --from=build /project/simplex-chat /
COPY --from=build /root/.cabal/bin/simplex-chat /
+3 -6
View File
@@ -234,8 +234,6 @@ You can use SimpleX with your own servers and still communicate with people usin
Recent and important updates:
[Jan 24, 2024. SimpleX Chat: free infrastructure from Linode, v5.5 released with private notes, group history and a simpler UX to connect.](./blog/20240124-simplex-chat-infrastructure-costs-v5-5-simplex-ux-private-notes-group-history.md)
[Nov 25, 2023. SimpleX Chat v5.4 released: link mobile and desktop apps via quantum resistant protocol, and much better groups](./blog/20231125-simplex-chat-v5-4-link-mobile-desktop-quantum-resistant-better-groups.md).
[Sep 25, 2023. SimpleX Chat v5.3 released: desktop app, local file encryption, improved groups and directory service](./blog/20230925-simplex-chat-v5-3-desktop-app-local-file-encryption-directory-service.md).
@@ -301,7 +299,7 @@ What is already implemented:
11. Transport isolation - different TCP connections and Tor circuits are used for traffic of different user profiles, optionally - for different contacts and group member connections.
12. Manual messaging queue rotations to move conversation to another SMP relay.
13. Sending end-to-end encrypted files using [XFTP protocol](https://simplex.chat/blog/20230301-simplex-file-transfer-protocol.html).
14. Local files encryption.
14. Local files encryption, except videos (to be added later).
We plan to add:
@@ -373,13 +371,12 @@ Please also join [#simplex-devs](https://simplex.chat/contact#/?v=1-2&smp=smp%3A
- ✅ Desktop client.
- ✅ Encryption of local files stored in the app.
- ✅ Using mobile profiles from the desktop app.
- ✅ Private notes.
- ✅ Improve sending videos (including encryption of locally stored videos).
- 🏗 Improve experience for the new users.
- 🏗 Post-quantum resistant key exchange in double ratchet protocol.
- 🏗 Large groups, communities and public channels.
- 🏗 Message delivery relay for senders (to conceal IP address from the recipients' servers and to reduce the traffic).
- Message delivery relay for senders (to conceal IP address from the recipients' servers and to reduce the traffic).
- Privacy & security slider - a simple way to set all settings at once.
- Improve sending videos (including encryption of locally stored videos).
- SMP queue redundancy and rotation (manual is supported).
- Include optional message into connection request sent via contact address.
- Improved navigation and search in the conversation (expand and scroll to quoted message, scroll to search results, etc.).
+4 -52
View File
@@ -34,8 +34,6 @@ struct ContentView: View {
@State private var waitingForOrPassedAuth = true
@State private var chatListActionSheet: ChatListActionSheet? = nil
private let callTopPadding: CGFloat = 50
private enum ChatListActionSheet: Identifiable {
case planAndConnectSheet(sheet: PlanAndConnectActionSheet)
@@ -52,28 +50,16 @@ struct ContentView: View {
var body: some View {
ZStack {
let showCallArea = chatModel.activeCall != nil && chatModel.activeCall?.callState != .waitCapabilities && chatModel.activeCall?.callState != .invitationAccepted
// contentView() has to be in a single branch, so that enabling authentication doesn't trigger re-rendering and close settings.
// i.e. with separate branches like this settings are closed: `if prefPerformLA { ... contentView() ... } else { contentView() }
if !prefPerformLA || accessAuthenticated {
contentView()
.padding(.top, showCallArea ? callTopPadding : 0)
} else {
lockButton()
.padding(.top, showCallArea ? callTopPadding : 0)
}
if showCallArea, let call = chatModel.activeCall {
VStack {
activeCallInteractiveArea(call)
Spacer()
}
}
if chatModel.showCallView, let call = chatModel.activeCall {
callView(call)
}
if !showSettings, let la = chatModel.laRequest {
LocalAuthView(authRequest: la)
.onDisappear {
@@ -149,11 +135,11 @@ struct ContentView: View {
if case .onboardingComplete = step,
chatModel.currentUser != nil {
mainView()
.actionSheet(item: $chatListActionSheet) { sheet in
switch sheet {
case let .planAndConnectSheet(sheet): return planAndConnectActionSheet(sheet, dismiss: false)
}
.actionSheet(item: $chatListActionSheet) { sheet in
switch sheet {
case let .planAndConnectSheet(sheet): return planAndConnectActionSheet(sheet, dismiss: false)
}
}
} else {
OnboardingView(onboarding: step)
}
@@ -177,40 +163,6 @@ struct ContentView: View {
}
}
@ViewBuilder private func activeCallInteractiveArea(_ call: Call) -> some View {
HStack {
Text(call.contact.displayName).font(.body).foregroundColor(.white)
Spacer()
CallDuration(call: call)
}
.padding(.horizontal)
.frame(height: callTopPadding - 10)
.background(Color(uiColor: UIColor(red: 47/255, green: 208/255, blue: 88/255, alpha: 1)))
.onTapGesture {
chatModel.activeCallViewIsCollapsed = false
}
}
struct CallDuration: View {
let call: Call
@State var text: String = ""
@State var timer: Timer? = nil
var body: some View {
Text(text).frame(minWidth: text.count <= 5 ? 52 : 77, alignment: .leading).offset(x: 4).font(.body).foregroundColor(.white)
.onAppear {
timer = Timer.scheduledTimer(withTimeInterval: 0.3, repeats: true) { timer in
if let connectedAt = call.connectedAt {
text = durationText(Int(Date.now.timeIntervalSince1970 - connectedAt.timeIntervalSince1970))
}
}
}
.onDisappear {
_ = timer?.invalidate()
}
}
}
private func lockButton() -> some View {
Button(action: authenticateContentViewAccess) { Label("Unlock", systemImage: "lock") }
}
-2
View File
@@ -80,7 +80,6 @@ final class ChatModel: ObservableObject {
@Published var tokenRegistered = false
@Published var tokenStatus: NtfTknStatus?
@Published var notificationMode = NotificationsMode.off
@Published var notificationServer: String?
@Published var notificationPreview: NotificationPreviewMode = ntfPreviewModeGroupDefault.get()
// pending notification actions
@Published var ntfContactRequest: NTFContactRequest?
@@ -90,7 +89,6 @@ final class ChatModel: ObservableObject {
@Published var activeCall: Call?
let callCommand: WebRTCCommandProcessor = WebRTCCommandProcessor()
@Published var showCallView = false
@Published var activeCallViewIsCollapsed = false
// remote desktop
@Published var remoteCtrlSession: RemoteCtrlSession?
// currently showing invitation
+14 -9
View File
@@ -252,6 +252,12 @@ func apiSetFilesFolder(filesFolder: String) throws {
throw r
}
func setXFTPConfig(_ cfg: XFTPFileConfig?) throws {
let r = chatSendCmdSync(.apiSetXFTPConfig(config: cfg))
if case .cmdOk = r { return }
throw r
}
func apiSetEncryptLocalFiles(_ enable: Bool) throws {
let r = chatSendCmdSync(.apiSetEncryptLocalFiles(enable: enable))
if case .cmdOk = r { return }
@@ -406,14 +412,14 @@ func apiDeleteMemberChatItem(groupId: Int64, groupMemberId: Int64, itemId: Int64
throw r
}
func apiGetNtfToken() -> (DeviceToken?, NtfTknStatus?, NotificationsMode, String?) {
func apiGetNtfToken() -> (DeviceToken?, NtfTknStatus?, NotificationsMode) {
let r = chatSendCmdSync(.apiGetNtfToken)
switch r {
case let .ntfToken(token, status, ntfMode, ntfServer): return (token, status, ntfMode, ntfServer)
case .chatCmdError(_, .errorAgent(.CMD(.PROHIBITED))): return (nil, nil, .off, nil)
case let .ntfToken(token, status, ntfMode): return (token, status, ntfMode)
case .chatCmdError(_, .errorAgent(.CMD(.PROHIBITED))): return (nil, nil, .off)
default:
logger.debug("apiGetNtfToken response: \(String(describing: r))")
return (nil, nil, .off, nil)
return (nil, nil, .off)
}
}
@@ -1166,7 +1172,7 @@ func filterMembersToAdd(_ ms: [GMember]) -> [Contact] {
let memberContactIds = ms.compactMap{ m in m.wrapped.memberCurrent ? m.wrapped.memberContactId : nil }
return ChatModel.shared.chats
.compactMap{ $0.chatInfo.contact }
.filter{ c in c.ready && c.active && !memberContactIds.contains(c.apiId) }
.filter{ !memberContactIds.contains($0.apiId) }
.sorted{ $0.displayName.lowercased() < $1.displayName.lowercased() }
}
@@ -1243,6 +1249,7 @@ func initializeChat(start: Bool, confirmStart: Bool = false, dbKey: String? = ni
}
try apiSetTempFolder(tempFolder: getTempFilesDirectory().path)
try apiSetFilesFolder(filesFolder: getAppFilesDirectory().path)
try setXFTPConfig(getXFTPCfg())
try apiSetEncryptLocalFiles(privacyEncryptLocalFilesGroupDefault.get())
m.chatInitialized = true
m.currentUser = try apiGetActiveUser()
@@ -1302,7 +1309,7 @@ func startChat(refreshInvitations: Bool = true) throws {
if (refreshInvitations) {
try refreshCallInvitations()
}
(m.savedToken, m.tokenStatus, m.notificationMode, m.notificationServer) = apiGetNtfToken()
(m.savedToken, m.tokenStatus, m.notificationMode) = apiGetNtfToken()
// deviceToken is set when AppDelegate.application(didRegisterForRemoteNotificationsWithDeviceToken:) is called,
// when it is called before startChat
if let token = m.deviceToken {
@@ -1854,9 +1861,7 @@ func chatItemSimpleUpdate(_ user: any UserLike, _ aChatItem: AChatItem) async {
let cItem = aChatItem.chatItem
if active(user) {
if await MainActor.run(body: { m.upsertChatItem(cInfo, cItem) }) {
if cItem.showNotification {
NtfManager.shared.notifyMessageReceived(user, cInfo, cItem)
}
NtfManager.shared.notifyMessageReceived(user, cInfo, cItem)
}
}
}
+33 -93
View File
@@ -12,67 +12,49 @@ import SimpleXChat
struct ActiveCallView: View {
@EnvironmentObject var m: ChatModel
@Environment(\.colorScheme) var colorScheme
@ObservedObject var call: Call
@Environment(\.scenePhase) var scenePhase
@State private var client: WebRTCClient? = nil
@State private var activeCall: WebRTCClient.Call? = nil
@State private var localRendererAspectRatio: CGFloat? = nil
@Binding var canConnectCall: Bool
@State var prevColorScheme: ColorScheme = .dark
@State var pipShown = false
var body: some View {
ZStack(alignment: .topLeading) {
ZStack(alignment: .bottom) {
if let client = client, [call.peerMedia, call.localMedia].contains(.video), activeCall != nil {
GeometryReader { g in
let width = g.size.width * 0.3
ZStack(alignment: .topTrailing) {
CallViewRemote(client: client, activeCall: $activeCall, activeCallViewIsCollapsed: $m.activeCallViewIsCollapsed, pipShown: $pipShown)
CallViewLocal(client: client, activeCall: $activeCall, localRendererAspectRatio: $localRendererAspectRatio, pipShown: $pipShown)
.cornerRadius(10)
.frame(width: width, height: width / (localRendererAspectRatio ?? 1))
.padding([.top, .trailing], 17)
ZStack(alignment: .center) {
// For some reason, when the view in GeometryReader and ZStack is visible, it steals clicks on a back button, so showing something on top like this with background color helps (.clear color doesn't work)
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color.primary.opacity(0.000001))
}
ZStack(alignment: .bottom) {
if let client = client, [call.peerMedia, call.localMedia].contains(.video), activeCall != nil {
GeometryReader { g in
let width = g.size.width * 0.3
ZStack(alignment: .topTrailing) {
CallViewRemote(client: client, activeCall: $activeCall)
CallViewLocal(client: client, activeCall: $activeCall, localRendererAspectRatio: $localRendererAspectRatio)
.cornerRadius(10)
.frame(width: width, height: width / (localRendererAspectRatio ?? 1))
.padding([.top, .trailing], 17)
}
}
if let call = m.activeCall, let client = client, (!pipShown || !call.supportsVideo) {
ActiveCallOverlay(call: call, client: client)
}
}
if let call = m.activeCall, let client = client {
ActiveCallOverlay(call: call, client: client)
}
}
.allowsHitTesting(!m.activeCallViewIsCollapsed)
.opacity(m.activeCallViewIsCollapsed ? 0 : 1)
.onAppear {
logger.debug("ActiveCallView: appear client is nil \(client == nil), scenePhase \(String(describing: scenePhase)), canConnectCall \(canConnectCall)")
AppDelegate.keepScreenOn(true)
createWebRTCClient()
dismissAllSheets()
hideKeyboard()
prevColorScheme = colorScheme
}
.onChange(of: canConnectCall) { _ in
logger.debug("ActiveCallView: canConnectCall changed to \(canConnectCall)")
createWebRTCClient()
}
.onChange(of: m.activeCallViewIsCollapsed) { _ in
hideKeyboard()
}
.onDisappear {
logger.debug("ActiveCallView: disappear")
Task { await m.callCommand.setClient(nil) }
AppDelegate.keepScreenOn(false)
client?.endCall()
}
.background(m.activeCallViewIsCollapsed ? .clear : .black)
// Quite a big delay when opening/closing the view when a scheme changes (globally) this way. It's not needed when CallKit is used since status bar is green with white text on it
.preferredColorScheme(m.activeCallViewIsCollapsed || CallController.useCallKit() ? prevColorScheme : .dark)
.background(.black)
.preferredColorScheme(.dark)
}
private func createWebRTCClient() {
@@ -87,8 +69,8 @@ struct ActiveCallView: View {
@MainActor
private func processRtcMessage(msg: WVAPIMessage) {
if call == m.activeCall,
let call = m.activeCall,
let client = client {
let call = m.activeCall,
let client = client {
logger.debug("ActiveCallView: response \(msg.resp.respType)")
switch msg.resp {
case let .capabilities(capabilities):
@@ -108,7 +90,7 @@ struct ActiveCallView: View {
Task {
do {
try await apiSendCallOffer(call.contact, offer, iceCandidates,
media: call.localMedia, capabilities: capabilities)
media: call.localMedia, capabilities: capabilities)
} catch {
logger.error("apiSendCallOffer \(responseError(error))")
}
@@ -140,15 +122,13 @@ struct ActiveCallView: View {
if let callStatus = WebRTCCallStatus.init(rawValue: state.connectionState),
case .connected = callStatus {
call.direction == .outgoing
? CallController.shared.reportOutgoingCall(call: call, connectedAt: nil)
: CallController.shared.reportIncomingCall(call: call, connectedAt: nil)
? CallController.shared.reportOutgoingCall(call: call, connectedAt: nil)
: CallController.shared.reportIncomingCall(call: call, connectedAt: nil)
call.callState = .connected
call.connectedAt = .now
}
if state.connectionState == "closed" {
closeCallView(client)
m.activeCall = nil
m.activeCallViewIsCollapsed = false
}
Task {
do {
@@ -160,7 +140,6 @@ struct ActiveCallView: View {
case let .connected(connectionInfo):
call.callState = .connected
call.connectionInfo = connectionInfo
call.connectedAt = .now
case .ended:
closeCallView(client)
call.callState = .ended
@@ -174,7 +153,6 @@ struct ActiveCallView: View {
case .end:
closeCallView(client)
m.activeCall = nil
m.activeCallViewIsCollapsed = false
default: ()
}
case let .error(message):
@@ -203,7 +181,7 @@ struct ActiveCallOverlay: View {
VStack {
switch call.localMedia {
case .video:
videoCallInfoView(call)
callInfoView(call, .leading)
.foregroundColor(.white)
.opacity(0.8)
.padding()
@@ -230,25 +208,16 @@ struct ActiveCallOverlay: View {
.frame(maxWidth: .infinity, alignment: .center)
case .audio:
ZStack(alignment: .topLeading) {
Button {
chatModel.activeCallViewIsCollapsed = true
} label: {
Label("Back", systemImage: "chevron.left")
.padding()
.foregroundColor(.white.opacity(0.8))
}
VStack {
ProfileImage(imageStr: call.contact.profile.image)
.scaledToFit()
.frame(width: 192, height: 192)
audioCallInfoView(call)
}
.foregroundColor(.white)
.opacity(0.8)
.padding()
.frame(maxHeight: .infinity)
VStack {
ProfileImage(imageStr: call.contact.profile.image)
.scaledToFit()
.frame(width: 192, height: 192)
callInfoView(call, .center)
}
.foregroundColor(.white)
.opacity(0.8)
.padding()
.frame(maxHeight: .infinity)
Spacer()
@@ -266,12 +235,12 @@ struct ActiveCallOverlay: View {
.frame(maxWidth: .infinity)
}
private func audioCallInfoView(_ call: Call) -> some View {
private func callInfoView(_ call: Call, _ alignment: Alignment) -> some View {
VStack {
Text(call.contact.chatViewName)
.lineLimit(1)
.font(.title)
.frame(maxWidth: .infinity, alignment: .center)
.frame(maxWidth: .infinity, alignment: alignment)
Group {
Text(call.callState.text)
HStack {
@@ -282,36 +251,7 @@ struct ActiveCallOverlay: View {
}
}
.font(.subheadline)
.frame(maxWidth: .infinity, alignment: .center)
}
}
private func videoCallInfoView(_ call: Call) -> some View {
VStack {
Button {
chatModel.activeCallViewIsCollapsed = true
} label: {
HStack(alignment: .center, spacing: 16) {
Image(systemName: "chevron.left")
.resizable()
.frame(width: 10, height: 18)
Text(call.contact.chatViewName)
.lineLimit(1)
.font(.title)
.frame(maxWidth: .infinity, alignment: .leading)
}
}
Group {
Text(call.callState.text)
HStack {
Text(call.encryptionStatus)
if let connInfo = call.connectionInfo {
Text("(") + Text(connInfo.text) + Text(")")
}
}
}
.font(.subheadline)
.frame(maxWidth: .infinity, alignment: .leading)
.frame(maxWidth: .infinity, alignment: alignment)
}
}
@@ -92,7 +92,6 @@ class CallManager {
if case .ended = call.callState {
logger.debug("CallManager.endCall: call ended")
m.activeCall = nil
m.activeCallViewIsCollapsed = false
m.showCallView = false
completed()
} else {
@@ -101,7 +100,6 @@ class CallManager {
await m.callCommand.processCommand(.end)
await MainActor.run {
m.activeCall = nil
m.activeCallViewIsCollapsed = false
m.showCallView = false
completed()
}
@@ -6,20 +6,14 @@
import SwiftUI
import WebRTC
import SimpleXChat
import AVKit
struct CallViewRemote: UIViewRepresentable {
var client: WebRTCClient
var activeCall: Binding<WebRTCClient.Call?>
@State var enablePip: (Bool) -> Void = {_ in }
@Binding var activeCallViewIsCollapsed: Bool
@Binding var pipShown: Bool
init(client: WebRTCClient, activeCall: Binding<WebRTCClient.Call?>, activeCallViewIsCollapsed: Binding<Bool>, pipShown: Binding<Bool>) {
init(client: WebRTCClient, activeCall: Binding<WebRTCClient.Call?>) {
self.client = client
self.activeCall = activeCall
self._activeCallViewIsCollapsed = activeCallViewIsCollapsed
self._pipShown = pipShown
}
func makeUIView(context: Context) -> UIView {
@@ -29,120 +23,12 @@ struct CallViewRemote: UIViewRepresentable {
remoteRenderer.videoContentMode = .scaleAspectFill
client.addRemoteRenderer(call, remoteRenderer)
addSubviewAndResize(remoteRenderer, into: view)
if AVPictureInPictureController.isPictureInPictureSupported() {
makeViewWithRTCRenderer(call, remoteRenderer, view, context)
}
}
return view
}
func makeViewWithRTCRenderer(_ call: WebRTCClient.Call, _ remoteRenderer: RTCMTLVideoView, _ view: UIView, _ context: Context) {
let pipRemoteRenderer = RTCMTLVideoView(frame: view.frame)
pipRemoteRenderer.videoContentMode = .scaleAspectFill
let pipVideoCallViewController = AVPictureInPictureVideoCallViewController()
pipVideoCallViewController.preferredContentSize = CGSize(width: 1080, height: 1920)
addSubviewAndResize(pipRemoteRenderer, into: pipVideoCallViewController.view)
let pipContentSource = AVPictureInPictureController.ContentSource(
activeVideoCallSourceView: view,
contentViewController: pipVideoCallViewController
)
let pipController = AVPictureInPictureController(contentSource: pipContentSource)
pipController.canStartPictureInPictureAutomaticallyFromInline = true
pipController.delegate = context.coordinator
context.coordinator.pipController = pipController
context.coordinator.willShowHide = { show in
if show {
client.addRemoteRenderer(call, pipRemoteRenderer)
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
activeCallViewIsCollapsed = true
}
} else {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.05) {
activeCallViewIsCollapsed = false
}
}
}
context.coordinator.didShowHide = { show in
if show {
remoteRenderer.isHidden = true
} else {
client.removeRemoteRenderer(call, pipRemoteRenderer)
remoteRenderer.isHidden = false
}
pipShown = show
}
DispatchQueue.main.async {
enablePip = { enable in
if enable != pipShown /* pipController.isPictureInPictureActive */ {
if enable {
pipController.startPictureInPicture()
} else {
pipController.stopPictureInPicture()
}
}
}
}
}
func makeCoordinator() -> Coordinator {
Coordinator()
}
func updateUIView(_ view: UIView, context: Context) {
logger.debug("CallView.updateUIView remote")
DispatchQueue.main.async {
if activeCallViewIsCollapsed != pipShown {
enablePip(activeCallViewIsCollapsed)
}
}
}
// MARK: - Coordinator
class Coordinator: NSObject, AVPictureInPictureControllerDelegate {
var pipController: AVPictureInPictureController? = nil
var willShowHide: (Bool) -> Void = { _ in }
var didShowHide: (Bool) -> Void = { _ in }
func pictureInPictureControllerWillStartPictureInPicture(_ pictureInPictureController: AVPictureInPictureController) {
willShowHide(true)
}
func pictureInPictureControllerDidStartPictureInPicture(_ pictureInPictureController: AVPictureInPictureController) {
didShowHide(true)
}
func pictureInPictureController(_ pictureInPictureController: AVPictureInPictureController, failedToStartPictureInPictureWithError error: Error) {
logger.error("PiP failed to start: \(error.localizedDescription)")
}
func pictureInPictureControllerWillStopPictureInPicture(_ pictureInPictureController: AVPictureInPictureController) {
willShowHide(false)
}
func pictureInPictureControllerDidStopPictureInPicture(_ pictureInPictureController: AVPictureInPictureController) {
didShowHide(false)
}
deinit {
pipController?.stopPictureInPicture()
pipController?.canStartPictureInPictureAutomaticallyFromInline = false
pipController?.contentSource = nil
pipController?.delegate = nil
pipController = nil
}
}
class SampleBufferVideoCallView: UIView {
override class var layerClass: AnyClass {
get { return AVSampleBufferDisplayLayer.self }
}
var sampleBufferDisplayLayer: AVSampleBufferDisplayLayer {
return layer as! AVSampleBufferDisplayLayer
}
}
}
@@ -150,14 +36,11 @@ struct CallViewLocal: UIViewRepresentable {
var client: WebRTCClient
var activeCall: Binding<WebRTCClient.Call?>
var localRendererAspectRatio: Binding<CGFloat?>
@State var pipStateChanged: (Bool) -> Void = {_ in }
@Binding var pipShown: Bool
init(client: WebRTCClient, activeCall: Binding<WebRTCClient.Call?>, localRendererAspectRatio: Binding<CGFloat?>, pipShown: Binding<Bool>) {
init(client: WebRTCClient, activeCall: Binding<WebRTCClient.Call?>, localRendererAspectRatio: Binding<CGFloat?>) {
self.client = client
self.activeCall = activeCall
self.localRendererAspectRatio = localRendererAspectRatio
self._pipShown = pipShown
}
func makeUIView(context: Context) -> UIView {
@@ -167,18 +50,12 @@ struct CallViewLocal: UIViewRepresentable {
client.addLocalRenderer(call, localRenderer)
client.startCaptureLocalVideo(call)
addSubviewAndResize(localRenderer, into: view)
DispatchQueue.main.async {
pipStateChanged = { shown in
localRenderer.isHidden = shown
}
}
}
return view
}
func updateUIView(_ view: UIView, context: Context) {
logger.debug("CallView.updateUIView local")
pipStateChanged(pipShown)
}
}
-2
View File
@@ -28,7 +28,6 @@ class Call: ObservableObject, Equatable {
@Published var speakerEnabled = false
@Published var videoEnabled: Bool
@Published var connectionInfo: ConnectionInfo?
@Published var connectedAt: Date? = nil
init(
direction: CallDirection,
@@ -60,7 +59,6 @@ class Call: ObservableObject, Equatable {
}
}
var hasMedia: Bool { get { callState == .offerSent || callState == .negotiated || callState == .connected } }
var supportsVideo: Bool { get { peerMedia == .video || localMedia == .video } }
}
enum CallDirection {
@@ -331,10 +331,6 @@ final class WebRTCClient: NSObject, RTCVideoViewDelegate, RTCFrameEncryptorDeleg
activeCall.remoteStream?.add(renderer)
}
func removeRemoteRenderer(_ activeCall: Call, _ renderer: RTCVideoRenderer) {
activeCall.remoteStream?.remove(renderer)
}
func startCaptureLocalVideo(_ activeCall: Call) {
#if targetEnvironment(simulator)
guard
@@ -414,7 +410,6 @@ final class WebRTCClient: NSObject, RTCVideoViewDelegate, RTCFrameEncryptorDeleg
guard let call = activeCall.wrappedValue else { return }
logger.debug("WebRTCClient: ending the call")
activeCall.wrappedValue = nil
(call.localCamera as? RTCCameraVideoCapturer)?.stopCapture()
call.connection.close()
call.connection.delegate = nil
call.frameEncryptor?.delegate = nil
@@ -29,9 +29,6 @@ struct CIImageView: View {
FullScreenMediaView(chatItem: chatItem, image: uiImage, showView: $showFullScreenImage, scrollProxy: scrollProxy)
}
.onTapGesture { showFullScreenImage = true }
.onChange(of: m.activeCallViewIsCollapsed) { _ in
showFullScreenImage = false
}
} else if let data = Data(base64Encoded: dropImagePrefix(image)),
let uiImage = UIImage(data: data) {
imageView(uiImage)
@@ -120,9 +120,6 @@ struct CIVideoView: View {
showFullScreenPlayer = urlDecrypted != nil
}
}
.onChange(of: m.activeCallViewIsCollapsed) { _ in
showFullScreenPlayer = false
}
if !decryptionInProgress {
Button {
decrypt(file: file) {
@@ -171,9 +168,6 @@ struct CIVideoView: View {
default: ()
}
}
.onChange(of: m.activeCallViewIsCollapsed) { _ in
showFullScreenPlayer = false
}
if !videoPlaying {
Button {
m.stopPreviousRecPlay = url
@@ -65,8 +65,6 @@ struct MarkedDeletedItemView: View {
}
}
// same texts are in markedDeletedText in ChatPreviewView, but it returns String;
// can be refactored into a single function if functions calling these are changed to return same type
var markedDeletedText: LocalizedStringKey {
switch chatItem.meta.itemDeleted {
case let .moderated(_, byGroupMember): "moderated by \(byGroupMember.displayName)"
+6 -25
View File
@@ -159,17 +159,12 @@ struct ChatView: View {
switch cInfo {
case let .direct(contact):
HStack {
let callsPrefEnabled = contact.mergedPreferences.calls.enabled.forUser
if callsPrefEnabled {
if chatModel.activeCall == nil {
callButton(contact, .audio, imageName: "phone")
.disabled(!contact.ready || !contact.active)
} else if let call = chatModel.activeCall, call.contact.id == cInfo.id {
endCallButton(call)
}
if contact.allowsFeature(.calls) {
callButton(contact, .audio, imageName: "phone")
.disabled(!contact.ready || !contact.active)
}
Menu {
if callsPrefEnabled && chatModel.activeCall == nil {
if contact.allowsFeature(.calls) {
Button {
CallController.shared.startCall(contact, .video)
} label: {
@@ -426,19 +421,7 @@ struct ChatView: View {
Image(systemName: imageName)
}
}
private func endCallButton(_ call: Call) -> some View {
Button {
if let uuid = call.callkitUUID {
CallController.shared.endCall(callUUID: uuid)
} else {
CallController.shared.endCall(call: call) {}
}
} label: {
Image(systemName: "phone.down.fill").tint(.red)
}
}
private func searchButton() -> some View {
Button {
searchMode = true
@@ -765,9 +748,7 @@ struct ChatView: View {
if ci.meta.editable && !mc.isVoice && !live {
menu.append(editAction(ci))
}
if !ci.isLiveDummy {
menu.append(viewInfoUIAction(ci))
}
menu.append(viewInfoUIAction(ci))
if revealed {
menu.append(hideUIAction())
}
@@ -978,9 +978,6 @@ struct ComposeView: View {
}
private func cancelLinkPreview() {
if let pendingLink = pendingLinkUrl?.absoluteString {
cancelledLinks.insert(pendingLink)
}
if let uri = composeState.linkPreview?.uri.absoluteString {
cancelledLinks.insert(uri)
}
@@ -234,29 +234,39 @@ struct GroupChatInfoView: View {
Spacer()
memberInfo(member)
}
// revert from this:
if user {
v
} else if groupInfo.membership.memberRole >= .admin {
// TODO if there are more actions, refactor with lists of swipeActions
let canBlockForAll = member.canBlockForAll(groupInfo: groupInfo)
let canRemove = member.canBeRemoved(groupInfo: groupInfo)
if canBlockForAll && canRemove {
removeSwipe(member, blockForAllSwipe(member, v))
} else if canBlockForAll {
blockForAllSwipe(member, v)
} else if canRemove {
removeSwipe(member, v)
} else {
v
}
} else if member.canBeRemoved(groupInfo: groupInfo) {
removeSwipe(member, blockSwipe(member, v))
} else {
if !member.blockedByAdmin {
blockSwipe(member, v)
} else {
v
}
blockSwipe(member, v)
}
// revert to this: vvv
// if user {
// v
// } else if groupInfo.membership.memberRole >= .admin {
// // TODO if there are more actions, refactor with lists of swipeActions
// let canBlockForAll = member.canBlockForAll(groupInfo: groupInfo)
// let canRemove = member.canBeRemoved(groupInfo: groupInfo)
// if canBlockForAll && canRemove {
// removeSwipe(member, blockForAllSwipe(member, v))
// } else if canBlockForAll {
// blockForAllSwipe(member, v)
// } else if canRemove {
// removeSwipe(member, v)
// } else {
// v
// }
// } else {
// if !member.blockedByAdmin {
// blockSwipe(member, v)
// } else {
// v
// }
// }
// ^^^
}
@ViewBuilder private func memberInfo(_ member: GroupMember) -> some View {
@@ -360,11 +370,7 @@ struct GroupChatInfoView: View {
private func addOrEditWelcomeMessage() -> some View {
NavigationLink {
GroupWelcomeView(
groupInfo: $groupInfo,
groupProfile: groupInfo.groupProfile,
welcomeText: groupInfo.groupProfile.description ?? ""
)
GroupWelcomeView(groupId: groupInfo.groupId, groupInfo: $groupInfo)
.navigationTitle("Welcome message")
.navigationBarTitleDisplayMode(.large)
} label: {
@@ -168,11 +168,24 @@ struct GroupMemberInfoView: View {
}
}
if groupInfo.membership.memberRole >= .admin {
adminDestructiveSection(member)
} else {
nonAdminBlockSection(member)
// revert from this:
Section {
if member.memberSettings.showMessages {
blockMemberButton(member)
} else {
unblockMemberButton(member)
}
if member.canBeRemoved(groupInfo: groupInfo) {
removeMemberButton(member)
}
}
// revert to this: vvv
// if groupInfo.membership.memberRole >= .admin {
// adminDestructiveSection(member)
// } else {
// nonAdminBlockSection(member)
// }
// ^^^
if developerTools {
Section("For console") {
@@ -11,32 +11,29 @@ import SimpleXChat
struct GroupWelcomeView: View {
@Environment(\.dismiss) var dismiss: DismissAction
@EnvironmentObject private var m: ChatModel
var groupId: Int64
@Binding var groupInfo: GroupInfo
@State var groupProfile: GroupProfile
@State var welcomeText: String
@State private var welcomeText: String = ""
@State private var editMode = true
@FocusState private var keyboardVisible: Bool
@State private var showSaveDialog = false
let maxByteCount = 1200
var body: some View {
VStack {
if groupInfo.canEdit {
editorView()
.modifier(BackButton {
if welcomeTextUnchanged() {
if welcomeText == groupInfo.groupProfile.description || (welcomeText == "" && groupInfo.groupProfile.description == nil) {
dismiss()
} else {
showSaveDialog = true
}
})
.confirmationDialog(
welcomeTextFitsLimit() ? "Save welcome message?" : "Welcome message is too long",
isPresented: $showSaveDialog
) {
if welcomeTextFitsLimit() {
Button("Save and update group profile") { save() }
.confirmationDialog("Save welcome message?", isPresented: $showSaveDialog) {
Button("Save and update group profile") {
save()
dismiss()
}
Button("Exit without saving") { dismiss() }
}
@@ -50,15 +47,14 @@ struct GroupWelcomeView: View {
}
}
.onAppear {
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
keyboardVisible = true
}
welcomeText = groupInfo.groupProfile.description ?? ""
keyboardVisible = true
}
}
private func textPreview() -> some View {
messageText(welcomeText, parseSimpleXMarkdown(welcomeText), nil, showSecrets: false)
.frame(minHeight: 130, alignment: .topLeading)
.frame(minHeight: 140, alignment: .topLeading)
.frame(maxWidth: .infinity, alignment: .leading)
}
@@ -78,7 +74,7 @@ struct GroupWelcomeView: View {
}
.padding(.horizontal, -5)
.padding(.top, -8)
.frame(height: 130, alignment: .topLeading)
.frame(height: 140, alignment: .topLeading)
.frame(maxWidth: .infinity, alignment: .leading)
}
} else {
@@ -97,9 +93,6 @@ struct GroupWelcomeView: View {
}
.disabled(welcomeText.isEmpty)
copyButton()
} footer: {
Text(!welcomeTextFitsLimit() ? "Message too large" : "")
.foregroundColor(.red)
}
Section {
@@ -120,15 +113,7 @@ struct GroupWelcomeView: View {
Button("Save and update group profile") {
save()
}
.disabled(welcomeTextUnchanged() || !welcomeTextFitsLimit())
}
private func welcomeTextUnchanged() -> Bool {
welcomeText == groupInfo.groupProfile.description || (welcomeText == "" && groupInfo.groupProfile.description == nil)
}
private func welcomeTextFitsLimit() -> Bool {
chatJsonLength(welcomeText) <= maxByteCount
.disabled(welcomeText == groupInfo.groupProfile.description || (welcomeText == "" && groupInfo.groupProfile.description == nil))
}
private func save() {
@@ -138,13 +123,11 @@ struct GroupWelcomeView: View {
if welcome?.count == 0 {
welcome = nil
}
groupProfile.description = welcome
let gInfo = try await apiUpdateGroup(groupInfo.groupId, groupProfile)
await MainActor.run {
groupInfo = gInfo
ChatModel.shared.updateGroup(gInfo)
dismiss()
}
var groupProfileUpdated = groupInfo.groupProfile
groupProfileUpdated.description = welcome
groupInfo = try await apiUpdateGroup(groupId, groupProfileUpdated)
m.updateGroup(groupInfo)
welcomeText = welcome ?? ""
} catch let error {
logger.error("apiUpdateGroup error: \(responseError(error))")
}
@@ -154,6 +137,6 @@ struct GroupWelcomeView: View {
struct GroupWelcomeView_Previews: PreviewProvider {
static var previews: some View {
GroupProfileView(groupInfo: Binding.constant(GroupInfo.sampleData), groupProfile: GroupProfile.sampleData)
GroupWelcomeView(groupId: 1, groupInfo: Binding.constant(GroupInfo.sampleData))
}
}
@@ -34,7 +34,7 @@ struct ChatPreviewView: View {
HStack(alignment: .top) {
chatPreviewTitle()
Spacer()
(cItem?.timestampText ?? formatTimestampText(chat.chatInfo.chatTs))
(cItem?.timestampText ?? formatTimestampText(chat.chatInfo.updatedAt))
.font(.subheadline)
.frame(minWidth: 60, alignment: .trailing)
.foregroundColor(.secondary)
@@ -171,21 +171,10 @@ struct ChatPreviewView: View {
}
func chatItemPreview(_ cItem: ChatItem) -> Text {
let itemText = cItem.meta.itemDeleted == nil ? cItem.text : markedDeletedText()
let itemText = cItem.meta.itemDeleted == nil ? cItem.text : NSLocalizedString("marked deleted", comment: "marked deleted chat item preview text")
let itemFormattedText = cItem.meta.itemDeleted == nil ? cItem.formattedText : nil
return messageText(itemText, itemFormattedText, cItem.memberDisplayName, icon: attachment(), preview: true, showSecrets: false)
// same texts are in markedDeletedText in MarkedDeletedItemView, but it returns LocalizedStringKey;
// can be refactored into a single function if functions calling these are changed to return same type
func markedDeletedText() -> String {
switch cItem.meta.itemDeleted {
case let .moderated(_, byGroupMember): String.localizedStringWithFormat(NSLocalizedString("moderated by %@", comment: "marked deleted chat item preview text"), byGroupMember.displayName)
case .blocked: NSLocalizedString("blocked", comment: "marked deleted chat item preview text")
case .blockedByAdmin: NSLocalizedString("blocked by admin", comment: "marked deleted chat item preview text")
case .deleted, nil: NSLocalizedString("marked deleted", comment: "marked deleted chat item preview text")
}
}
func attachment() -> String? {
switch cItem.content.msgContent {
case .file: return "doc.fill"
@@ -42,6 +42,25 @@ struct DeveloperView: View {
} footer: {
(developerTools ? Text("Show:") : Text("Hide:")) + Text(" ") + Text("Database IDs and Transport isolation option.")
}
// Section {
// settingsRow("arrow.up.doc") {
// Toggle("Send videos and files via XFTP", isOn: $xftpSendEnabled)
// .onChange(of: xftpSendEnabled) { _ in
// do {
// try setXFTPConfig(getXFTPCfg())
// } catch {
// logger.error("setXFTPConfig: cannot set XFTP config \(responseError(error))")
// }
// }
// }
// } header: {
// Text("Experimental")
// } footer: {
// if xftpSendEnabled {
// Text("v4.6.1+ is required to receive via XFTP.")
// }
// }
}
}
}
@@ -76,10 +76,6 @@ struct NotificationsView: View {
Text(m.notificationPreview.label)
}
}
if let server = m.notificationServer {
smpServers("Push server", [server])
}
} header: {
Text("Push notifications")
} footer: {
@@ -91,9 +87,6 @@ struct NotificationsView: View {
}
}
.disabled(legacyDatabase)
.onAppear {
(m.savedToken, m.tokenStatus, m.notificationMode, m.notificationServer) = apiGetNtfToken()
}
}
private func notificationAlert(_ alert: NotificationAlert, _ token: DeviceToken) -> Alert {
@@ -132,7 +125,6 @@ struct NotificationsView: View {
m.tokenStatus = .new
notificationMode = .off
m.notificationMode = .off
m.notificationServer = nil
}
} catch let error {
await MainActor.run {
@@ -143,13 +135,11 @@ struct NotificationsView: View {
}
default:
do {
let _ = try await apiRegisterToken(token: token, notificationMode: mode)
let (_, tknStatus, ntfMode, ntfServer) = apiGetNtfToken()
let status = try await apiRegisterToken(token: token, notificationMode: mode)
await MainActor.run {
m.tokenStatus = tknStatus
notificationMode = ntfMode
m.notificationMode = ntfMode
m.notificationServer = ntfServer
m.tokenStatus = status
notificationMode = mode
m.notificationMode = mode
}
} catch let error {
await MainActor.run {
@@ -219,7 +219,6 @@
</trans-unit>
<trans-unit id="%lld messages blocked by admin" xml:space="preserve">
<source>%lld messages blocked by admin</source>
<target>%lld Nachrichten wurden vom Administrator blockiert</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="%lld messages marked deleted" xml:space="preserve">
@@ -645,7 +644,6 @@
</trans-unit>
<trans-unit id="All messages will be deleted - this cannot be undone!" xml:space="preserve">
<source>All messages will be deleted - this cannot be undone!</source>
<target>Es werden alle Nachrichten gelöscht. Dieser Vorgang kann nicht rückgängig gemacht werden!</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">
@@ -655,7 +653,7 @@
</trans-unit>
<trans-unit id="All new messages from %@ will be hidden!" xml:space="preserve">
<source>All new messages from %@ will be hidden!</source>
<target>Von %@ werden alle neuen Nachrichten ausgeblendet!</target>
<target>Alle neuen Nachrichten von %@ werden verborgen!</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="All your contacts will remain connected." xml:space="preserve">
@@ -925,7 +923,6 @@
</trans-unit>
<trans-unit id="Block for all" xml:space="preserve">
<source>Block for all</source>
<target>Für Alle blockieren</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Block group members" xml:space="preserve">
@@ -940,7 +937,6 @@
</trans-unit>
<trans-unit id="Block member for all?" xml:space="preserve">
<source>Block member for all?</source>
<target>Mitglied für Alle blockieren?</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Block member?" xml:space="preserve">
@@ -950,7 +946,6 @@
</trans-unit>
<trans-unit id="Blocked by admin" xml:space="preserve">
<source>Blocked by admin</source>
<target>wurde vom Administrator blockiert</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Both you and your contact can add message reactions." xml:space="preserve">
@@ -1166,7 +1161,6 @@
</trans-unit>
<trans-unit id="Clear private notes?" xml:space="preserve">
<source>Clear private notes?</source>
<target>Private Notizen löschen?</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Clear verification" xml:space="preserve">
@@ -1465,12 +1459,10 @@ Das ist Ihr eigener Einmal-Link!</target>
</trans-unit>
<trans-unit id="Created at" xml:space="preserve">
<source>Created at</source>
<target>Erstellt um</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Created at: %@" xml:space="preserve">
<source>Created at: %@</source>
<target>Erstellt um: %@</target>
<note>copied message info</note>
</trans-unit>
<trans-unit id="Created on %@" xml:space="preserve">
@@ -2260,7 +2252,6 @@ Das kann nicht rückgängig gemacht werden!</target>
</trans-unit>
<trans-unit id="Error creating message" xml:space="preserve">
<source>Error creating message</source>
<target>Fehler beim Erstellen der Nachricht</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error creating profile!" xml:space="preserve">
@@ -2935,7 +2926,6 @@ Das kann nicht rückgängig gemacht werden!</target>
</trans-unit>
<trans-unit id="Improved message delivery" xml:space="preserve">
<source>Improved message delivery</source>
<target>Verbesserte Zustellung von Nachrichten</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Improved privacy and security" xml:space="preserve">
@@ -3163,7 +3153,6 @@ Das kann nicht rückgängig gemacht werden!</target>
</trans-unit>
<trans-unit id="Join group conversations" xml:space="preserve">
<source>Join group conversations</source>
<target>Gruppenunterhaltungen beitreten</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Join group?" xml:space="preserve">
@@ -3874,7 +3863,6 @@ Das ist Ihr Link für die Gruppe %@!</target>
</trans-unit>
<trans-unit id="Past member %@" xml:space="preserve">
<source>Past member %@</source>
<target>Ehemaliges Mitglied %@</target>
<note>past/unknown group member</note>
</trans-unit>
<trans-unit id="Paste desktop address" xml:space="preserve">
@@ -3889,7 +3877,6 @@ Das ist Ihr Link für die Gruppe %@!</target>
</trans-unit>
<trans-unit id="Paste link to connect!" xml:space="preserve">
<source>Paste link to connect!</source>
<target>Zum Verbinden den Link einfügen!</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Paste the link you received" xml:space="preserve">
@@ -4026,7 +4013,6 @@ Fehler: %@</target>
</trans-unit>
<trans-unit id="Private notes" xml:space="preserve">
<source>Private notes</source>
<target>Private Notizen</target>
<note>name of notes to self</note>
</trans-unit>
<trans-unit id="Profile and server connections" xml:space="preserve">
@@ -4211,7 +4197,6 @@ Fehler: %@</target>
</trans-unit>
<trans-unit id="Recent history and improved [directory bot](simplex:/contact#/?v=1-4&amp;smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FeXSPwqTkKyDO3px4fLf1wx3MvPdjdLW3%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAaiv6MkMH44L2TcYrt_CsX3ZvM11WgbMEUn0hkIKTOho%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion)." xml:space="preserve">
<source>Recent history and improved [directory bot](simplex:/contact#/?v=1-4&amp;smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FeXSPwqTkKyDO3px4fLf1wx3MvPdjdLW3%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAaiv6MkMH44L2TcYrt_CsX3ZvM11WgbMEUn0hkIKTOho%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion).</source>
<target>Aktueller Nachrichtenverlauf und verbesserter [Gruppenverzeichnis-Bot](simplex:/contact#/?v=1-4&amp;smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FeXSPwqTkKyDO3px4fLf1wx3MvPdjdLW3%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAaiv6MkMH44L2TcYrt_CsX3ZvM11WgbMEUn0hkIKTOho%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion).</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Recipients see updates as you type them." xml:space="preserve">
@@ -4501,7 +4486,6 @@ Fehler: %@</target>
</trans-unit>
<trans-unit id="Saved message" xml:space="preserve">
<source>Saved message</source>
<target>Gespeicherte Nachricht</target>
<note>message info title</note>
</trans-unit>
<trans-unit id="Scan QR code" xml:space="preserve">
@@ -4536,7 +4520,6 @@ Fehler: %@</target>
</trans-unit>
<trans-unit id="Search bar accepts invitation links." xml:space="preserve">
<source>Search bar accepts invitation links.</source>
<target>Von der Suchleiste werden Einladungslinks akzeptiert.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Search or paste SimpleX link" xml:space="preserve">
@@ -5375,7 +5358,6 @@ Sie werden aufgefordert, die Authentifizierung abzuschließen, bevor diese Funkt
</trans-unit>
<trans-unit id="Turkish interface" xml:space="preserve">
<source>Turkish interface</source>
<target>Türkische Bedienoberfläche</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Turn off" xml:space="preserve">
@@ -5400,7 +5382,6 @@ Sie werden aufgefordert, die Authentifizierung abzuschließen, bevor diese Funkt
</trans-unit>
<trans-unit id="Unblock for all" xml:space="preserve">
<source>Unblock for all</source>
<target>Für Alle freigeben</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Unblock member" xml:space="preserve">
@@ -5410,7 +5391,6 @@ Sie werden aufgefordert, die Authentifizierung abzuschließen, bevor diese Funkt
</trans-unit>
<trans-unit id="Unblock member for all?" xml:space="preserve">
<source>Unblock member for all?</source>
<target>Mitglied für Alle freigeben?</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Unblock member?" xml:space="preserve">
@@ -5777,7 +5757,6 @@ Bitten Sie Ihren Kontakt darum einen weiteren Verbindungs-Link zu erzeugen, um s
</trans-unit>
<trans-unit id="With encrypted files and media." xml:space="preserve">
<source>With encrypted files and media.</source>
<target>Mit verschlüsselten Dateien und Medien.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="With optional welcome message." xml:space="preserve">
@@ -5787,7 +5766,6 @@ Bitten Sie Ihren Kontakt darum einen weiteren Verbindungs-Link zu erzeugen, um s
</trans-unit>
<trans-unit id="With reduced battery usage." xml:space="preserve">
<source>With reduced battery usage.</source>
<target>Mit reduziertem Akkuverbrauch.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Wrong database passphrase" xml:space="preserve">
@@ -6290,17 +6268,15 @@ SimpleX-Server können Ihr Profil nicht einsehen.</target>
</trans-unit>
<trans-unit id="blocked" xml:space="preserve">
<source>blocked</source>
<target>Blockiert</target>
<target>blockiert</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="blocked %@" xml:space="preserve">
<source>blocked %@</source>
<target>%@ wurde blockiert</target>
<note>rcv group event chat item</note>
</trans-unit>
<trans-unit id="blocked by admin" xml:space="preserve">
<source>blocked by admin</source>
<target>wurde vom Administrator blockiert</target>
<note>blocked chat item</note>
</trans-unit>
<trans-unit id="bold" xml:space="preserve">
@@ -6425,7 +6401,6 @@ SimpleX-Server können Ihr Profil nicht einsehen.</target>
</trans-unit>
<trans-unit id="contact %@ changed to %@" xml:space="preserve">
<source>contact %1$@ changed to %2$@</source>
<target>Der Kontaktname %1$@ wurde auf %2$@ geändert</target>
<note>profile update event chat item</note>
</trans-unit>
<trans-unit id="contact has e2e encryption" xml:space="preserve">
@@ -6700,7 +6675,6 @@ SimpleX-Server können Ihr Profil nicht einsehen.</target>
</trans-unit>
<trans-unit id="member %@ changed to %@" xml:space="preserve">
<source>member %1$@ changed to %2$@</source>
<target>Der Mitgliedsname %1$@ wurde auf %2$@ geändert</target>
<note>profile update event chat item</note>
</trans-unit>
<trans-unit id="member connected" xml:space="preserve">
@@ -6827,12 +6801,10 @@ SimpleX-Server können Ihr Profil nicht einsehen.</target>
</trans-unit>
<trans-unit id="removed contact address" xml:space="preserve">
<source>removed contact address</source>
<target>Kontaktadresse wurde entfernt</target>
<note>profile update event chat item</note>
</trans-unit>
<trans-unit id="removed profile picture" xml:space="preserve">
<source>removed profile picture</source>
<target>Profil-Bild wurde entfernt</target>
<note>profile update event chat item</note>
</trans-unit>
<trans-unit id="removed you" xml:space="preserve">
@@ -6867,12 +6839,10 @@ SimpleX-Server können Ihr Profil nicht einsehen.</target>
</trans-unit>
<trans-unit id="set new contact address" xml:space="preserve">
<source>set new contact address</source>
<target>Neue Kontaktadresse wurde festgelegt</target>
<note>profile update event chat item</note>
</trans-unit>
<trans-unit id="set new profile picture" xml:space="preserve">
<source>set new profile picture</source>
<target>Neues Profil-Bild wurde festgelegt</target>
<note>profile update event chat item</note>
</trans-unit>
<trans-unit id="starting…" xml:space="preserve">
@@ -6892,7 +6862,6 @@ SimpleX-Server können Ihr Profil nicht einsehen.</target>
</trans-unit>
<trans-unit id="unblocked %@" xml:space="preserve">
<source>unblocked %@</source>
<target>%@ wurde freigegeben</target>
<note>rcv group event chat item</note>
</trans-unit>
<trans-unit id="unknown" xml:space="preserve">
@@ -6902,7 +6871,6 @@ SimpleX-Server können Ihr Profil nicht einsehen.</target>
</trans-unit>
<trans-unit id="unknown status" xml:space="preserve">
<source>unknown status</source>
<target>unbekannter Gruppenmitglieds-Status</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="updated group profile" xml:space="preserve">
@@ -6912,7 +6880,6 @@ SimpleX-Server können Ihr Profil nicht einsehen.</target>
</trans-unit>
<trans-unit id="updated profile" xml:space="preserve">
<source>updated profile</source>
<target>Das Profil wurde aktualisiert</target>
<note>profile update event chat item</note>
</trans-unit>
<trans-unit id="v%@" xml:space="preserve">
@@ -6987,7 +6954,6 @@ SimpleX-Server können Ihr Profil nicht einsehen.</target>
</trans-unit>
<trans-unit id="you blocked %@" xml:space="preserve">
<source>you blocked %@</source>
<target>Sie haben %@ blockiert</target>
<note>snd group event chat item</note>
</trans-unit>
<trans-unit id="you changed address" xml:space="preserve">
@@ -7032,7 +6998,6 @@ SimpleX-Server können Ihr Profil nicht einsehen.</target>
</trans-unit>
<trans-unit id="you unblocked %@" xml:space="preserve">
<source>you unblocked %@</source>
<target>Sie haben %@ freigegeben</target>
<note>snd group event chat item</note>
</trans-unit>
<trans-unit id="you: " xml:space="preserve">
@@ -219,7 +219,6 @@
</trans-unit>
<trans-unit id="%lld messages blocked by admin" xml:space="preserve">
<source>%lld messages blocked by admin</source>
<target>%lld messaggi bloccati dall'amministratore</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="%lld messages marked deleted" xml:space="preserve">
@@ -925,7 +924,6 @@
</trans-unit>
<trans-unit id="Block for all" xml:space="preserve">
<source>Block for all</source>
<target>Blocca per tutti</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Block group members" xml:space="preserve">
@@ -940,7 +938,6 @@
</trans-unit>
<trans-unit id="Block member for all?" xml:space="preserve">
<source>Block member for all?</source>
<target>Bloccare il membro per tutti?</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Block member?" xml:space="preserve">
@@ -950,7 +947,6 @@
</trans-unit>
<trans-unit id="Blocked by admin" xml:space="preserve">
<source>Blocked by admin</source>
<target>Bloccato dall'amministratore</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Both you and your contact can add message reactions." xml:space="preserve">
@@ -5400,7 +5396,6 @@ Ti verrà chiesto di completare l'autenticazione prima di attivare questa funzio
</trans-unit>
<trans-unit id="Unblock for all" xml:space="preserve">
<source>Unblock for all</source>
<target>Sblocca per tutti</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Unblock member" xml:space="preserve">
@@ -5410,7 +5405,6 @@ Ti verrà chiesto di completare l'autenticazione prima di attivare questa funzio
</trans-unit>
<trans-unit id="Unblock member for all?" xml:space="preserve">
<source>Unblock member for all?</source>
<target>Sbloccare il membro per tutti?</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Unblock member?" xml:space="preserve">
@@ -6295,12 +6289,10 @@ I server di SimpleX non possono vedere il tuo profilo.</target>
</trans-unit>
<trans-unit id="blocked %@" xml:space="preserve">
<source>blocked %@</source>
<target>ha bloccato %@</target>
<note>rcv group event chat item</note>
</trans-unit>
<trans-unit id="blocked by admin" xml:space="preserve">
<source>blocked by admin</source>
<target>bloccato dall'amministratore</target>
<note>blocked chat item</note>
</trans-unit>
<trans-unit id="bold" xml:space="preserve">
@@ -6892,7 +6884,6 @@ I server di SimpleX non possono vedere il tuo profilo.</target>
</trans-unit>
<trans-unit id="unblocked %@" xml:space="preserve">
<source>unblocked %@</source>
<target>ha sbloccato %@</target>
<note>rcv group event chat item</note>
</trans-unit>
<trans-unit id="unknown" xml:space="preserve">
@@ -6987,7 +6978,6 @@ I server di SimpleX non possono vedere il tuo profilo.</target>
</trans-unit>
<trans-unit id="you blocked %@" xml:space="preserve">
<source>you blocked %@</source>
<target>hai bloccato %@</target>
<note>snd group event chat item</note>
</trans-unit>
<trans-unit id="you changed address" xml:space="preserve">
@@ -7032,7 +7022,6 @@ I server di SimpleX non possono vedere il tuo profilo.</target>
</trans-unit>
<trans-unit id="you unblocked %@" xml:space="preserve">
<source>you unblocked %@</source>
<target>hai sbloccato %@</target>
<note>snd group event chat item</note>
</trans-unit>
<trans-unit id="you: " xml:space="preserve">
@@ -219,7 +219,6 @@
</trans-unit>
<trans-unit id="%lld messages blocked by admin" xml:space="preserve">
<source>%lld messages blocked by admin</source>
<target>%lld berichten geblokkeerd door beheerder</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="%lld messages marked deleted" xml:space="preserve">
@@ -925,7 +924,6 @@
</trans-unit>
<trans-unit id="Block for all" xml:space="preserve">
<source>Block for all</source>
<target>Blokkeren voor iedereen</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Block group members" xml:space="preserve">
@@ -940,7 +938,6 @@
</trans-unit>
<trans-unit id="Block member for all?" xml:space="preserve">
<source>Block member for all?</source>
<target>Lid voor iedereen blokkeren?</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Block member?" xml:space="preserve">
@@ -950,7 +947,6 @@
</trans-unit>
<trans-unit id="Blocked by admin" xml:space="preserve">
<source>Blocked by admin</source>
<target>Geblokkeerd door beheerder</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Both you and your contact can add message reactions." xml:space="preserve">
@@ -3163,7 +3159,7 @@ Dit kan niet ongedaan gemaakt worden!</target>
</trans-unit>
<trans-unit id="Join group conversations" xml:space="preserve">
<source>Join group conversations</source>
<target>Neem deel aan groepsgesprekken</target>
<target>Neem deel aan groep gesprekken</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Join group?" xml:space="preserve">
@@ -3889,7 +3885,7 @@ Dit is jouw link voor groep %@!</target>
</trans-unit>
<trans-unit id="Paste link to connect!" xml:space="preserve">
<source>Paste link to connect!</source>
<target>Plak een link om te verbinden!</target>
<target>Plak link om te verbinden!</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Paste the link you received" xml:space="preserve">
@@ -4541,7 +4537,7 @@ Fout: %@</target>
</trans-unit>
<trans-unit id="Search or paste SimpleX link" xml:space="preserve">
<source>Search or paste SimpleX link</source>
<target>Zoek of plak een SimpleX link</target>
<target>Zoek of plak de SimpleX link</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Secure queue" xml:space="preserve">
@@ -5400,7 +5396,6 @@ U wordt gevraagd de authenticatie te voltooien voordat deze functie wordt ingesc
</trans-unit>
<trans-unit id="Unblock for all" xml:space="preserve">
<source>Unblock for all</source>
<target>Deblokkeer voor iedereen</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Unblock member" xml:space="preserve">
@@ -5410,7 +5405,6 @@ U wordt gevraagd de authenticatie te voltooien voordat deze functie wordt ingesc
</trans-unit>
<trans-unit id="Unblock member for all?" xml:space="preserve">
<source>Unblock member for all?</source>
<target>Lid voor iedereen deblokkeren?</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Unblock member?" xml:space="preserve">
@@ -6295,12 +6289,10 @@ SimpleX servers kunnen uw profiel niet zien.</target>
</trans-unit>
<trans-unit id="blocked %@" xml:space="preserve">
<source>blocked %@</source>
<target>geblokkeerd %@</target>
<note>rcv group event chat item</note>
</trans-unit>
<trans-unit id="blocked by admin" xml:space="preserve">
<source>blocked by admin</source>
<target>geblokkeerd door beheerder</target>
<note>blocked chat item</note>
</trans-unit>
<trans-unit id="bold" xml:space="preserve">
@@ -6892,7 +6884,6 @@ SimpleX servers kunnen uw profiel niet zien.</target>
</trans-unit>
<trans-unit id="unblocked %@" xml:space="preserve">
<source>unblocked %@</source>
<target>gedeblokkeerd %@</target>
<note>rcv group event chat item</note>
</trans-unit>
<trans-unit id="unknown" xml:space="preserve">
@@ -6987,7 +6978,6 @@ SimpleX servers kunnen uw profiel niet zien.</target>
</trans-unit>
<trans-unit id="you blocked %@" xml:space="preserve">
<source>you blocked %@</source>
<target>je hebt %@ geblokkeerd</target>
<note>snd group event chat item</note>
</trans-unit>
<trans-unit id="you changed address" xml:space="preserve">
@@ -7032,7 +7022,6 @@ SimpleX servers kunnen uw profiel niet zien.</target>
</trans-unit>
<trans-unit id="you unblocked %@" xml:space="preserve">
<source>you unblocked %@</source>
<target>je hebt %@ gedeblokkeerd</target>
<note>snd group event chat item</note>
</trans-unit>
<trans-unit id="you: " xml:space="preserve">
@@ -219,7 +219,6 @@
</trans-unit>
<trans-unit id="%lld messages blocked by admin" xml:space="preserve">
<source>%lld messages blocked by admin</source>
<target>%lld wiadomości zablokowanych przez admina</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="%lld messages marked deleted" xml:space="preserve">
@@ -645,7 +644,6 @@
</trans-unit>
<trans-unit id="All messages will be deleted - this cannot be undone!" xml:space="preserve">
<source>All messages will be deleted - this cannot be undone!</source>
<target>Wszystkie wiadomości zostaną usunięte nie można tego cofnąć!</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">
@@ -925,7 +923,6 @@
</trans-unit>
<trans-unit id="Block for all" xml:space="preserve">
<source>Block for all</source>
<target>Zablokuj dla wszystkich</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Block group members" xml:space="preserve">
@@ -940,7 +937,6 @@
</trans-unit>
<trans-unit id="Block member for all?" xml:space="preserve">
<source>Block member for all?</source>
<target>Zablokować członka dla wszystkich?</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Block member?" xml:space="preserve">
@@ -950,7 +946,6 @@
</trans-unit>
<trans-unit id="Blocked by admin" xml:space="preserve">
<source>Blocked by admin</source>
<target>Zablokowany przez admina</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Both you and your contact can add message reactions." xml:space="preserve">
@@ -1166,7 +1161,6 @@
</trans-unit>
<trans-unit id="Clear private notes?" xml:space="preserve">
<source>Clear private notes?</source>
<target>Wyczyścić prywatne notatki?</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Clear verification" xml:space="preserve">
@@ -1465,12 +1459,10 @@ To jest twój jednorazowy link!</target>
</trans-unit>
<trans-unit id="Created at" xml:space="preserve">
<source>Created at</source>
<target>Utworzony o</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Created at: %@" xml:space="preserve">
<source>Created at: %@</source>
<target>Utworzony o: %@</target>
<note>copied message info</note>
</trans-unit>
<trans-unit id="Created on %@" xml:space="preserve">
@@ -1965,7 +1957,6 @@ To nie może być cofnięte!</target>
</trans-unit>
<trans-unit id="Do not send history to new members." xml:space="preserve">
<source>Do not send history to new members.</source>
<target>Nie wysyłaj historii do nowych członków.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Don't create address" xml:space="preserve">
@@ -2260,7 +2251,6 @@ To nie może być cofnięte!</target>
</trans-unit>
<trans-unit id="Error creating message" xml:space="preserve">
<source>Error creating message</source>
<target>Błąd tworzenia wiadomości</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error creating profile!" xml:space="preserve">
@@ -2840,7 +2830,6 @@ To nie może być cofnięte!</target>
</trans-unit>
<trans-unit id="History is not sent to new members." xml:space="preserve">
<source>History is not sent to new members.</source>
<target>Historia nie jest wysyłana do nowych członków.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="How SimpleX works" xml:space="preserve">
@@ -2935,7 +2924,6 @@ To nie może być cofnięte!</target>
</trans-unit>
<trans-unit id="Improved message delivery" xml:space="preserve">
<source>Improved message delivery</source>
<target>Ulepszona dostawa wiadomości</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Improved privacy and security" xml:space="preserve">
@@ -3052,7 +3040,6 @@ To nie może być cofnięte!</target>
</trans-unit>
<trans-unit id="Invalid display name!" xml:space="preserve">
<source>Invalid display name!</source>
<target>Nieprawidłowa nazwa wyświetlana!</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Invalid link" xml:space="preserve">
@@ -3163,7 +3150,6 @@ To nie może być cofnięte!</target>
</trans-unit>
<trans-unit id="Join group conversations" xml:space="preserve">
<source>Join group conversations</source>
<target>Dołącz do grupowej rozmowy</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Join group?" xml:space="preserve">
@@ -3874,7 +3860,6 @@ To jest twój link do grupy %@!</target>
</trans-unit>
<trans-unit id="Past member %@" xml:space="preserve">
<source>Past member %@</source>
<target>Były członek %@</target>
<note>past/unknown group member</note>
</trans-unit>
<trans-unit id="Paste desktop address" xml:space="preserve">
@@ -3889,7 +3874,6 @@ To jest twój link do grupy %@!</target>
</trans-unit>
<trans-unit id="Paste link to connect!" xml:space="preserve">
<source>Paste link to connect!</source>
<target>Wklej link, aby połączyć!</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Paste the link you received" xml:space="preserve">
@@ -4026,7 +4010,6 @@ Błąd: %@</target>
</trans-unit>
<trans-unit id="Private notes" xml:space="preserve">
<source>Private notes</source>
<target>Prywatne notatki</target>
<note>name of notes to self</note>
</trans-unit>
<trans-unit id="Profile and server connections" xml:space="preserve">
@@ -4211,7 +4194,6 @@ Błąd: %@</target>
</trans-unit>
<trans-unit id="Recent history and improved [directory bot](simplex:/contact#/?v=1-4&amp;smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FeXSPwqTkKyDO3px4fLf1wx3MvPdjdLW3%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAaiv6MkMH44L2TcYrt_CsX3ZvM11WgbMEUn0hkIKTOho%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion)." xml:space="preserve">
<source>Recent history and improved [directory bot](simplex:/contact#/?v=1-4&amp;smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FeXSPwqTkKyDO3px4fLf1wx3MvPdjdLW3%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAaiv6MkMH44L2TcYrt_CsX3ZvM11WgbMEUn0hkIKTOho%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion).</source>
<target>Ostania historia i ulepszony [bot adresowy](simplex:/contact#/?v=1-4&amp;smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FeXSPwqTkKyDO3px4fLf1wx3MvPdjdLW3%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAaiv6MkMH44L2TcYrt_CsX3ZvM11WgbMEUn0hkIKTOho%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion).</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Recipients see updates as you type them." xml:space="preserve">
@@ -4501,7 +4483,6 @@ Błąd: %@</target>
</trans-unit>
<trans-unit id="Saved message" xml:space="preserve">
<source>Saved message</source>
<target>Zachowano wiadomość</target>
<note>message info title</note>
</trans-unit>
<trans-unit id="Scan QR code" xml:space="preserve">
@@ -4536,7 +4517,6 @@ Błąd: %@</target>
</trans-unit>
<trans-unit id="Search bar accepts invitation links." xml:space="preserve">
<source>Search bar accepts invitation links.</source>
<target>Pasek wyszukiwania akceptuje linki zaproszenia.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Search or paste SimpleX link" xml:space="preserve">
@@ -4651,7 +4631,6 @@ Błąd: %@</target>
</trans-unit>
<trans-unit id="Send up to 100 last messages to new members." xml:space="preserve">
<source>Send up to 100 last messages to new members.</source>
<target>Wysyłaj do 100 ostatnich wiadomości do nowych członków.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Sender cancelled file transfer." xml:space="preserve">
@@ -5268,7 +5247,6 @@ Może się to zdarzyć z powodu jakiegoś błędu lub gdy połączenie jest skom
</trans-unit>
<trans-unit id="This display name is invalid. Please choose another name." xml:space="preserve">
<source>This display name is invalid. Please choose another name.</source>
<target>Nazwa wyświetlana jest nieprawidłowa. Proszę wybrać inną nazwę.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="This group has over %lld members, delivery receipts are not sent." xml:space="preserve">
@@ -5375,7 +5353,6 @@ Przed włączeniem tej funkcji zostanie wyświetlony monit uwierzytelniania.</ta
</trans-unit>
<trans-unit id="Turkish interface" xml:space="preserve">
<source>Turkish interface</source>
<target>Turecki interfejs</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Turn off" xml:space="preserve">
@@ -5400,7 +5377,6 @@ Przed włączeniem tej funkcji zostanie wyświetlony monit uwierzytelniania.</ta
</trans-unit>
<trans-unit id="Unblock for all" xml:space="preserve">
<source>Unblock for all</source>
<target>Odblokuj dla wszystkich</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Unblock member" xml:space="preserve">
@@ -5410,7 +5386,6 @@ Przed włączeniem tej funkcji zostanie wyświetlony monit uwierzytelniania.</ta
</trans-unit>
<trans-unit id="Unblock member for all?" xml:space="preserve">
<source>Unblock member for all?</source>
<target>Odblokować członka dla wszystkich?</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Unblock member?" xml:space="preserve">
@@ -5512,7 +5487,6 @@ Aby się połączyć, poproś Twój kontakt o utworzenie kolejnego linku połąc
</trans-unit>
<trans-unit id="Up to 100 last messages are sent to new members." xml:space="preserve">
<source>Up to 100 last messages are sent to new members.</source>
<target>Do nowych członków wysyłanych jest do 100 ostatnich wiadomości.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Update" xml:space="preserve">
@@ -5687,7 +5661,6 @@ Aby się połączyć, poproś Twój kontakt o utworzenie kolejnego linku połąc
</trans-unit>
<trans-unit id="Visible history" xml:space="preserve">
<source>Visible history</source>
<target>Widoczna historia</target>
<note>chat feature</note>
</trans-unit>
<trans-unit id="Voice messages" xml:space="preserve">
@@ -5777,7 +5750,6 @@ Aby się połączyć, poproś Twój kontakt o utworzenie kolejnego linku połąc
</trans-unit>
<trans-unit id="With encrypted files and media." xml:space="preserve">
<source>With encrypted files and media.</source>
<target>Z zaszyfrowanymi plikami i multimediami.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="With optional welcome message." xml:space="preserve">
@@ -5787,7 +5759,6 @@ Aby się połączyć, poproś Twój kontakt o utworzenie kolejnego linku połąc
</trans-unit>
<trans-unit id="With reduced battery usage." xml:space="preserve">
<source>With reduced battery usage.</source>
<target>Ze zmniejszonym zużyciem baterii.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Wrong database passphrase" xml:space="preserve">
@@ -6295,12 +6266,10 @@ Serwery SimpleX nie mogą zobaczyć Twojego profilu.</target>
</trans-unit>
<trans-unit id="blocked %@" xml:space="preserve">
<source>blocked %@</source>
<target>zablokowany %@</target>
<note>rcv group event chat item</note>
</trans-unit>
<trans-unit id="blocked by admin" xml:space="preserve">
<source>blocked by admin</source>
<target>zablokowany przez admina</target>
<note>blocked chat item</note>
</trans-unit>
<trans-unit id="bold" xml:space="preserve">
@@ -6425,7 +6394,6 @@ Serwery SimpleX nie mogą zobaczyć Twojego profilu.</target>
</trans-unit>
<trans-unit id="contact %@ changed to %@" xml:space="preserve">
<source>contact %1$@ changed to %2$@</source>
<target>kontakt %1$@ zmieniony na %2$@</target>
<note>profile update event chat item</note>
</trans-unit>
<trans-unit id="contact has e2e encryption" xml:space="preserve">
@@ -6700,7 +6668,6 @@ Serwery SimpleX nie mogą zobaczyć Twojego profilu.</target>
</trans-unit>
<trans-unit id="member %@ changed to %@" xml:space="preserve">
<source>member %1$@ changed to %2$@</source>
<target>członek %1$@ zmieniony na %2$@</target>
<note>profile update event chat item</note>
</trans-unit>
<trans-unit id="member connected" xml:space="preserve">
@@ -6827,12 +6794,10 @@ Serwery SimpleX nie mogą zobaczyć Twojego profilu.</target>
</trans-unit>
<trans-unit id="removed contact address" xml:space="preserve">
<source>removed contact address</source>
<target>usunięto adres kontaktu</target>
<note>profile update event chat item</note>
</trans-unit>
<trans-unit id="removed profile picture" xml:space="preserve">
<source>removed profile picture</source>
<target>usunięto zdjęcie profilu</target>
<note>profile update event chat item</note>
</trans-unit>
<trans-unit id="removed you" xml:space="preserve">
@@ -6867,12 +6832,10 @@ Serwery SimpleX nie mogą zobaczyć Twojego profilu.</target>
</trans-unit>
<trans-unit id="set new contact address" xml:space="preserve">
<source>set new contact address</source>
<target>ustaw nowy adres kontaktu</target>
<note>profile update event chat item</note>
</trans-unit>
<trans-unit id="set new profile picture" xml:space="preserve">
<source>set new profile picture</source>
<target>ustaw nowe zdjęcie profilu</target>
<note>profile update event chat item</note>
</trans-unit>
<trans-unit id="starting…" xml:space="preserve">
@@ -6892,7 +6855,6 @@ Serwery SimpleX nie mogą zobaczyć Twojego profilu.</target>
</trans-unit>
<trans-unit id="unblocked %@" xml:space="preserve">
<source>unblocked %@</source>
<target>odblokowano %@</target>
<note>rcv group event chat item</note>
</trans-unit>
<trans-unit id="unknown" xml:space="preserve">
@@ -6902,7 +6864,6 @@ Serwery SimpleX nie mogą zobaczyć Twojego profilu.</target>
</trans-unit>
<trans-unit id="unknown status" xml:space="preserve">
<source>unknown status</source>
<target>nieznany status</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="updated group profile" xml:space="preserve">
@@ -6912,7 +6873,6 @@ Serwery SimpleX nie mogą zobaczyć Twojego profilu.</target>
</trans-unit>
<trans-unit id="updated profile" xml:space="preserve">
<source>updated profile</source>
<target>zaktualizowano profil</target>
<note>profile update event chat item</note>
</trans-unit>
<trans-unit id="v%@" xml:space="preserve">
@@ -6987,7 +6947,6 @@ Serwery SimpleX nie mogą zobaczyć Twojego profilu.</target>
</trans-unit>
<trans-unit id="you blocked %@" xml:space="preserve">
<source>you blocked %@</source>
<target>zablokowałeś %@</target>
<note>snd group event chat item</note>
</trans-unit>
<trans-unit id="you changed address" xml:space="preserve">
@@ -7032,7 +6991,6 @@ Serwery SimpleX nie mogą zobaczyć Twojego profilu.</target>
</trans-unit>
<trans-unit id="you unblocked %@" xml:space="preserve">
<source>you unblocked %@</source>
<target>odblokowałeś %@</target>
<note>snd group event chat item</note>
</trans-unit>
<trans-unit id="you: " xml:space="preserve">
@@ -219,7 +219,6 @@
</trans-unit>
<trans-unit id="%lld messages blocked by admin" xml:space="preserve">
<source>%lld messages blocked by admin</source>
<target>%lld сообщений заблокировано администратором</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="%lld messages marked deleted" xml:space="preserve">
@@ -925,7 +924,6 @@
</trans-unit>
<trans-unit id="Block for all" xml:space="preserve">
<source>Block for all</source>
<target>Заблокировать для всех</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Block group members" xml:space="preserve">
@@ -940,7 +938,6 @@
</trans-unit>
<trans-unit id="Block member for all?" xml:space="preserve">
<source>Block member for all?</source>
<target>Заблокировать члена для всех?</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Block member?" xml:space="preserve">
@@ -950,7 +947,6 @@
</trans-unit>
<trans-unit id="Blocked by admin" xml:space="preserve">
<source>Blocked by admin</source>
<target>Заблокирован администратором</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Both you and your contact can add message reactions." xml:space="preserve">
@@ -5400,7 +5396,6 @@ You will be prompted to complete authentication before this feature is enabled.<
</trans-unit>
<trans-unit id="Unblock for all" xml:space="preserve">
<source>Unblock for all</source>
<target>Разблокировать для всех</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Unblock member" xml:space="preserve">
@@ -5410,7 +5405,6 @@ You will be prompted to complete authentication before this feature is enabled.<
</trans-unit>
<trans-unit id="Unblock member for all?" xml:space="preserve">
<source>Unblock member for all?</source>
<target>Разблокировать члена для всех?</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Unblock member?" xml:space="preserve">
@@ -6295,12 +6289,10 @@ SimpleX серверы не могут получить доступ к Ваше
</trans-unit>
<trans-unit id="blocked %@" xml:space="preserve">
<source>blocked %@</source>
<target>%@ заблокирован</target>
<note>rcv group event chat item</note>
</trans-unit>
<trans-unit id="blocked by admin" xml:space="preserve">
<source>blocked by admin</source>
<target>заблокировано администратором</target>
<note>blocked chat item</note>
</trans-unit>
<trans-unit id="bold" xml:space="preserve">
@@ -6892,7 +6884,6 @@ SimpleX серверы не могут получить доступ к Ваше
</trans-unit>
<trans-unit id="unblocked %@" xml:space="preserve">
<source>unblocked %@</source>
<target>%@ разблокирован</target>
<note>rcv group event chat item</note>
</trans-unit>
<trans-unit id="unknown" xml:space="preserve">
@@ -6987,7 +6978,6 @@ SimpleX серверы не могут получить доступ к Ваше
</trans-unit>
<trans-unit id="you blocked %@" xml:space="preserve">
<source>you blocked %@</source>
<target>Вы заблокировали %@</target>
<note>snd group event chat item</note>
</trans-unit>
<trans-unit id="you changed address" xml:space="preserve">
@@ -7032,7 +7022,6 @@ SimpleX серверы не могут получить доступ к Ваше
</trans-unit>
<trans-unit id="you unblocked %@" xml:space="preserve">
<source>you unblocked %@</source>
<target>Вы разблокировали %@</target>
<note>snd group event chat item</note>
</trans-unit>
<trans-unit id="you: " xml:space="preserve">
@@ -14,17 +14,17 @@
</trans-unit>
<trans-unit id=" " xml:space="preserve">
<source> </source>
<target> </target>
<target> . </target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id=" " xml:space="preserve">
<source> </source>
<target> </target>
<target> </target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id=" " xml:space="preserve">
<source> </source>
<target> </target>
<target> . </target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id=" (" xml:space="preserve">
@@ -453,6 +453,7 @@ var receiverStarted = false
let startLock = DispatchSemaphore(value: 1)
let suspendLock = DispatchSemaphore(value: 1)
var networkConfig: NetCfg = getNetCfg()
let xftpConfig: XFTPFileConfig? = getXFTPCfg()
// startChat uses semaphore startLock to ensure that only one didReceive thread can start chat controller
// Subsequent calls to didReceive will be waiting on semaphore and won't start chat again, as it will be .active
@@ -498,6 +499,7 @@ func doStartChat() -> DBMigrationResult? {
try setNetworkConfig(networkConfig)
try apiSetTempFolder(tempFolder: getTempFilesDirectory().path)
try apiSetFilesFolder(filesFolder: getAppFilesDirectory().path)
try setXFTPConfig(xftpConfig)
try apiSetEncryptLocalFiles(privacyEncryptLocalFilesGroupDefault.get())
// prevent suspension while starting chat
suspendLock.wait()
@@ -731,6 +733,12 @@ func apiSetFilesFolder(filesFolder: String) throws {
throw r
}
func setXFTPConfig(_ cfg: XFTPFileConfig?) throws {
let r = sendSimpleXCmd(.apiSetXFTPConfig(config: cfg))
if case .cmdOk = r { return }
throw r
}
func apiSetEncryptLocalFiles(_ enable: Bool) throws {
let r = sendSimpleXCmd(.apiSetEncryptLocalFiles(enable: enable))
if case .cmdOk = r { return }
+42 -32
View File
@@ -90,11 +90,6 @@
5CB0BA90282713D900B3292C /* SimpleXInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CB0BA8F282713D900B3292C /* SimpleXInfo.swift */; };
5CB0BA92282713FD00B3292C /* CreateProfile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CB0BA91282713FD00B3292C /* CreateProfile.swift */; };
5CB0BA9A2827FD8800B3292C /* HowItWorks.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CB0BA992827FD8800B3292C /* HowItWorks.swift */; };
5CB1CE9C2B8771DB00963938 /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CB1CE972B8771DB00963938 /* libffi.a */; };
5CB1CE9D2B8771DB00963938 /* libHSsimplex-chat-5.5.5.0-4Xh8aGOq2uNGBj7gMJTaKI.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CB1CE982B8771DB00963938 /* libHSsimplex-chat-5.5.5.0-4Xh8aGOq2uNGBj7gMJTaKI.a */; };
5CB1CE9E2B8771DB00963938 /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CB1CE992B8771DB00963938 /* libgmpxx.a */; };
5CB1CE9F2B8771DB00963938 /* libHSsimplex-chat-5.5.5.0-4Xh8aGOq2uNGBj7gMJTaKI-ghc9.6.3.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CB1CE9A2B8771DB00963938 /* libHSsimplex-chat-5.5.5.0-4Xh8aGOq2uNGBj7gMJTaKI-ghc9.6.3.a */; };
5CB1CEA02B8771DB00963938 /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CB1CE9B2B8771DB00963938 /* libgmp.a */; };
5CB2084F28DA4B4800D024EC /* RTCServers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CB2084E28DA4B4800D024EC /* RTCServers.swift */; };
5CB346E52868AA7F001FD2EF /* SuspendChat.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CB346E42868AA7F001FD2EF /* SuspendChat.swift */; };
5CB346E72868D76D001FD2EF /* NotificationsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CB346E62868D76D001FD2EF /* NotificationsView.swift */; };
@@ -117,6 +112,11 @@
5CC2C0FF2809BF11000C35E3 /* SimpleX--iOS--InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 5CC2C0FD2809BF11000C35E3 /* SimpleX--iOS--InfoPlist.strings */; };
5CC868F329EB540C0017BBFD /* CIRcvDecryptionError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CC868F229EB540C0017BBFD /* CIRcvDecryptionError.swift */; };
5CCB939C297EFCB100399E78 /* NavStackCompat.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CCB939B297EFCB100399E78 /* NavStackCompat.swift */; };
5CCD2C462B5C800E00F76440 /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CCD2C412B5C800E00F76440 /* libgmpxx.a */; };
5CCD2C472B5C800E00F76440 /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CCD2C422B5C800E00F76440 /* libgmp.a */; };
5CCD2C482B5C800E00F76440 /* libHSsimplex-chat-5.5.0.3-3tzCJyUgrnK8CcHdWjtxcl-ghc9.6.3.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CCD2C432B5C800E00F76440 /* libHSsimplex-chat-5.5.0.3-3tzCJyUgrnK8CcHdWjtxcl-ghc9.6.3.a */; };
5CCD2C492B5C800E00F76440 /* libHSsimplex-chat-5.5.0.3-3tzCJyUgrnK8CcHdWjtxcl.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CCD2C442B5C800E00F76440 /* libHSsimplex-chat-5.5.0.3-3tzCJyUgrnK8CcHdWjtxcl.a */; };
5CCD2C4A2B5C800E00F76440 /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CCD2C452B5C800E00F76440 /* libffi.a */; };
5CD67B8F2B0E858A00C510B1 /* hs_init.h in Headers */ = {isa = PBXBuildFile; fileRef = 5CD67B8D2B0E858A00C510B1 /* hs_init.h */; settings = {ATTRIBUTES = (Public, ); }; };
5CD67B902B0E858A00C510B1 /* hs_init.c in Sources */ = {isa = PBXBuildFile; fileRef = 5CD67B8E2B0E858A00C510B1 /* hs_init.c */; };
5CDCAD482818589900503DA2 /* NotificationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CDCAD472818589900503DA2 /* NotificationService.swift */; };
@@ -164,6 +164,11 @@
64466DC829FC2B3B00E3D48D /* CreateSimpleXAddress.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64466DC729FC2B3B00E3D48D /* CreateSimpleXAddress.swift */; };
64466DCC29FFE3E800E3D48D /* MailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64466DCB29FFE3E800E3D48D /* MailView.swift */; };
6448BBB628FA9D56000D2AB9 /* GroupLinkView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6448BBB528FA9D56000D2AB9 /* GroupLinkView.swift */; };
6449333A2AF8E51000AC506E /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 644933352AF8E51000AC506E /* libgmpxx.a */; };
6449333B2AF8E51000AC506E /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 644933362AF8E51000AC506E /* libgmp.a */; };
6449333C2AF8E51000AC506E /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 644933372AF8E51000AC506E /* libffi.a */; };
6449333D2AF8E51000AC506E /* libHSsimplex-chat-5.4.0.3-EnhmkSQK6HvJ11g1uZERg8-ghc9.6.3.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 644933382AF8E51000AC506E /* libHSsimplex-chat-5.4.0.3-EnhmkSQK6HvJ11g1uZERg8-ghc9.6.3.a */; };
6449333E2AF8E51000AC506E /* libHSsimplex-chat-5.4.0.3-EnhmkSQK6HvJ11g1uZERg8.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 644933392AF8E51000AC506E /* libHSsimplex-chat-5.4.0.3-EnhmkSQK6HvJ11g1uZERg8.a */; };
644EFFDE292BCD9D00525D5B /* ComposeVoiceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 644EFFDD292BCD9D00525D5B /* ComposeVoiceView.swift */; };
644EFFE0292CFD7F00525D5B /* CIVoiceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 644EFFDF292CFD7F00525D5B /* CIVoiceView.swift */; };
644EFFE2292D089800525D5B /* FramedCIVoiceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 644EFFE1292D089800525D5B /* FramedCIVoiceView.swift */; };
@@ -372,11 +377,6 @@
5CB0BA8F282713D900B3292C /* SimpleXInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SimpleXInfo.swift; sourceTree = "<group>"; };
5CB0BA91282713FD00B3292C /* CreateProfile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateProfile.swift; sourceTree = "<group>"; };
5CB0BA992827FD8800B3292C /* HowItWorks.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HowItWorks.swift; sourceTree = "<group>"; };
5CB1CE972B8771DB00963938 /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = "<group>"; };
5CB1CE982B8771DB00963938 /* libHSsimplex-chat-5.5.5.0-4Xh8aGOq2uNGBj7gMJTaKI.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-5.5.5.0-4Xh8aGOq2uNGBj7gMJTaKI.a"; sourceTree = "<group>"; };
5CB1CE992B8771DB00963938 /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = "<group>"; };
5CB1CE9A2B8771DB00963938 /* libHSsimplex-chat-5.5.5.0-4Xh8aGOq2uNGBj7gMJTaKI-ghc9.6.3.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-5.5.5.0-4Xh8aGOq2uNGBj7gMJTaKI-ghc9.6.3.a"; sourceTree = "<group>"; };
5CB1CE9B2B8771DB00963938 /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = "<group>"; };
5CB2084E28DA4B4800D024EC /* RTCServers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RTCServers.swift; sourceTree = "<group>"; };
5CB2085428DE647400D024EC /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = de; path = de.lproj/Localizable.strings; sourceTree = "<group>"; };
5CB346E42868AA7F001FD2EF /* SuspendChat.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SuspendChat.swift; sourceTree = "<group>"; };
@@ -403,6 +403,11 @@
5CC2C0FE2809BF11000C35E3 /* ru */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ru; path = "ru.lproj/SimpleX--iOS--InfoPlist.strings"; sourceTree = "<group>"; };
5CC868F229EB540C0017BBFD /* CIRcvDecryptionError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CIRcvDecryptionError.swift; sourceTree = "<group>"; };
5CCB939B297EFCB100399E78 /* NavStackCompat.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavStackCompat.swift; sourceTree = "<group>"; };
5CCD2C412B5C800E00F76440 /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = "<group>"; };
5CCD2C422B5C800E00F76440 /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = "<group>"; };
5CCD2C432B5C800E00F76440 /* libHSsimplex-chat-5.5.0.3-3tzCJyUgrnK8CcHdWjtxcl-ghc9.6.3.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-5.5.0.3-3tzCJyUgrnK8CcHdWjtxcl-ghc9.6.3.a"; sourceTree = "<group>"; };
5CCD2C442B5C800E00F76440 /* libHSsimplex-chat-5.5.0.3-3tzCJyUgrnK8CcHdWjtxcl.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-5.5.0.3-3tzCJyUgrnK8CcHdWjtxcl.a"; sourceTree = "<group>"; };
5CCD2C452B5C800E00F76440 /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = "<group>"; };
5CD67B8D2B0E858A00C510B1 /* hs_init.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = hs_init.h; sourceTree = "<group>"; };
5CD67B8E2B0E858A00C510B1 /* hs_init.c */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.c; path = hs_init.c; sourceTree = "<group>"; };
5CDCAD452818589900503DA2 /* SimpleX NSE.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = "SimpleX NSE.appex"; sourceTree = BUILT_PRODUCTS_DIR; };
@@ -450,6 +455,11 @@
64466DC729FC2B3B00E3D48D /* CreateSimpleXAddress.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateSimpleXAddress.swift; sourceTree = "<group>"; };
64466DCB29FFE3E800E3D48D /* MailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MailView.swift; sourceTree = "<group>"; };
6448BBB528FA9D56000D2AB9 /* GroupLinkView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupLinkView.swift; sourceTree = "<group>"; };
644933352AF8E51000AC506E /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = "<group>"; };
644933362AF8E51000AC506E /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = "<group>"; };
644933372AF8E51000AC506E /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = "<group>"; };
644933382AF8E51000AC506E /* libHSsimplex-chat-5.4.0.3-EnhmkSQK6HvJ11g1uZERg8-ghc9.6.3.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-5.4.0.3-EnhmkSQK6HvJ11g1uZERg8-ghc9.6.3.a"; sourceTree = "<group>"; };
644933392AF8E51000AC506E /* libHSsimplex-chat-5.4.0.3-EnhmkSQK6HvJ11g1uZERg8.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-5.4.0.3-EnhmkSQK6HvJ11g1uZERg8.a"; sourceTree = "<group>"; };
644EFFDD292BCD9D00525D5B /* ComposeVoiceView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeVoiceView.swift; sourceTree = "<group>"; };
644EFFDF292CFD7F00525D5B /* CIVoiceView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CIVoiceView.swift; sourceTree = "<group>"; };
644EFFE1292D089800525D5B /* FramedCIVoiceView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FramedCIVoiceView.swift; sourceTree = "<group>"; };
@@ -514,13 +524,13 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
5CCD2C462B5C800E00F76440 /* libgmpxx.a in Frameworks */,
5CCD2C492B5C800E00F76440 /* libHSsimplex-chat-5.5.0.3-3tzCJyUgrnK8CcHdWjtxcl.a in Frameworks */,
5CCD2C482B5C800E00F76440 /* libHSsimplex-chat-5.5.0.3-3tzCJyUgrnK8CcHdWjtxcl-ghc9.6.3.a in Frameworks */,
5CE2BA93284534B000EC33A6 /* libiconv.tbd in Frameworks */,
5CB1CEA02B8771DB00963938 /* libgmp.a in Frameworks */,
5CB1CE9E2B8771DB00963938 /* libgmpxx.a in Frameworks */,
5CB1CE9C2B8771DB00963938 /* libffi.a in Frameworks */,
5CCD2C4A2B5C800E00F76440 /* libffi.a in Frameworks */,
5CCD2C472B5C800E00F76440 /* libgmp.a in Frameworks */,
5CE2BA94284534BB00EC33A6 /* libz.tbd in Frameworks */,
5CB1CE9D2B8771DB00963938 /* libHSsimplex-chat-5.5.5.0-4Xh8aGOq2uNGBj7gMJTaKI.a in Frameworks */,
5CB1CE9F2B8771DB00963938 /* libHSsimplex-chat-5.5.5.0-4Xh8aGOq2uNGBj7gMJTaKI-ghc9.6.3.a in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -582,11 +592,11 @@
5C764E5C279C70B7000C6508 /* Libraries */ = {
isa = PBXGroup;
children = (
5CB1CE972B8771DB00963938 /* libffi.a */,
5CB1CE9B2B8771DB00963938 /* libgmp.a */,
5CB1CE992B8771DB00963938 /* libgmpxx.a */,
5CB1CE9A2B8771DB00963938 /* libHSsimplex-chat-5.5.5.0-4Xh8aGOq2uNGBj7gMJTaKI-ghc9.6.3.a */,
5CB1CE982B8771DB00963938 /* libHSsimplex-chat-5.5.5.0-4Xh8aGOq2uNGBj7gMJTaKI.a */,
5CCD2C452B5C800E00F76440 /* libffi.a */,
5CCD2C422B5C800E00F76440 /* libgmp.a */,
5CCD2C412B5C800E00F76440 /* libgmpxx.a */,
5CCD2C432B5C800E00F76440 /* libHSsimplex-chat-5.5.0.3-3tzCJyUgrnK8CcHdWjtxcl-ghc9.6.3.a */,
5CCD2C442B5C800E00F76440 /* libHSsimplex-chat-5.5.0.3-3tzCJyUgrnK8CcHdWjtxcl.a */,
);
path = Libraries;
sourceTree = "<group>";
@@ -1509,7 +1519,7 @@
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 199;
CURRENT_PROJECT_VERSION = 191;
DEVELOPMENT_TEAM = 5NN7GUYB6T;
ENABLE_BITCODE = NO;
ENABLE_PREVIEWS = YES;
@@ -1531,7 +1541,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 5.5.4;
MARKETING_VERSION = 5.5;
PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.app;
PRODUCT_NAME = SimpleX;
SDKROOT = iphoneos;
@@ -1552,7 +1562,7 @@
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 199;
CURRENT_PROJECT_VERSION = 191;
DEVELOPMENT_TEAM = 5NN7GUYB6T;
ENABLE_BITCODE = NO;
ENABLE_PREVIEWS = YES;
@@ -1574,7 +1584,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 5.5.4;
MARKETING_VERSION = 5.5;
PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.app;
PRODUCT_NAME = SimpleX;
SDKROOT = iphoneos;
@@ -1633,7 +1643,7 @@
CODE_SIGN_ENTITLEMENTS = "SimpleX NSE/SimpleX NSE.entitlements";
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 199;
CURRENT_PROJECT_VERSION = 191;
DEVELOPMENT_TEAM = 5NN7GUYB6T;
ENABLE_BITCODE = NO;
GENERATE_INFOPLIST_FILE = YES;
@@ -1646,7 +1656,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 5.5.4;
MARKETING_VERSION = 5.5;
PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.app.SimpleX-NSE";
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
@@ -1665,7 +1675,7 @@
CODE_SIGN_ENTITLEMENTS = "SimpleX NSE/SimpleX NSE.entitlements";
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 199;
CURRENT_PROJECT_VERSION = 191;
DEVELOPMENT_TEAM = 5NN7GUYB6T;
ENABLE_BITCODE = NO;
GENERATE_INFOPLIST_FILE = YES;
@@ -1678,7 +1688,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 5.5.4;
MARKETING_VERSION = 5.5;
PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.app.SimpleX-NSE";
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
@@ -1697,7 +1707,7 @@
APPLICATION_EXTENSION_API_ONLY = YES;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 199;
CURRENT_PROJECT_VERSION = 191;
DEFINES_MODULE = YES;
DEVELOPMENT_TEAM = 5NN7GUYB6T;
DYLIB_COMPATIBILITY_VERSION = 1;
@@ -1721,7 +1731,7 @@
"$(inherited)",
"$(PROJECT_DIR)/Libraries/sim",
);
MARKETING_VERSION = 5.5.4;
MARKETING_VERSION = 5.5;
PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.SimpleXChat;
PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
SDKROOT = iphoneos;
@@ -1743,7 +1753,7 @@
APPLICATION_EXTENSION_API_ONLY = YES;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 199;
CURRENT_PROJECT_VERSION = 191;
DEFINES_MODULE = YES;
DEVELOPMENT_TEAM = 5NN7GUYB6T;
DYLIB_COMPATIBILITY_VERSION = 1;
@@ -1767,7 +1777,7 @@
"$(inherited)",
"$(PROJECT_DIR)/Libraries/sim",
);
MARKETING_VERSION = 5.5.4;
MARKETING_VERSION = 5.5;
PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.SimpleXChat;
PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
SDKROOT = iphoneos;
-5
View File
@@ -105,11 +105,6 @@ public func parseSimpleXMarkdown(_ s: String) -> [FormattedText]? {
return nil
}
public func chatJsonLength(_ s: String) -> Int {
var c = s.cString(using: .utf8)!
return Int(chat_json_length(&c))
}
struct ParsedMarkdown: Decodable {
var formattedText: [FormattedText]?
}
+13 -2
View File
@@ -31,6 +31,7 @@ public enum ChatCommand {
case apiSuspendChat(timeoutMicroseconds: Int)
case setTempFolder(tempFolder: String)
case setFilesFolder(filesFolder: String)
case apiSetXFTPConfig(config: XFTPFileConfig?)
case apiSetEncryptLocalFiles(enable: Bool)
case apiExportArchive(config: ArchiveConfig)
case apiImportArchive(config: ArchiveConfig)
@@ -161,6 +162,11 @@ public enum ChatCommand {
case let .apiSuspendChat(timeoutMicroseconds): return "/_app suspend \(timeoutMicroseconds)"
case let .setTempFolder(tempFolder): return "/_temp_folder \(tempFolder)"
case let .setFilesFolder(filesFolder): return "/_files_folder \(filesFolder)"
case let .apiSetXFTPConfig(cfg): if let cfg = cfg {
return "/_xftp on \(encodeJSON(cfg))"
} else {
return "/_xftp off"
}
case let .apiSetEncryptLocalFiles(enable): return "/_files_encrypt \(onOff(enable))"
case let .apiExportArchive(cfg): return "/_db export \(encodeJSON(cfg))"
case let .apiImportArchive(cfg): return "/_db import \(encodeJSON(cfg))"
@@ -305,6 +311,7 @@ public enum ChatCommand {
case .apiSuspendChat: return "apiSuspendChat"
case .setTempFolder: return "setTempFolder"
case .setFilesFolder: return "setFilesFolder"
case .apiSetXFTPConfig: return "apiSetXFTPConfig"
case .apiSetEncryptLocalFiles: return "apiSetEncryptLocalFiles"
case .apiExportArchive: return "apiExportArchive"
case .apiImportArchive: return "apiImportArchive"
@@ -606,7 +613,7 @@ public enum ChatResponse: Decodable, Error {
case callEnded(user: UserRef, contact: Contact)
case callInvitations(callInvitations: [RcvCallInvitation])
case ntfTokenStatus(status: NtfTknStatus)
case ntfToken(token: DeviceToken, status: NtfTknStatus, ntfMode: NotificationsMode, ntfServer: String)
case ntfToken(token: DeviceToken, status: NtfTknStatus, ntfMode: NotificationsMode)
case ntfMessages(user_: User?, connEntity_: ConnectionEntity?, msgTs: Date?, ntfMessages: [NtfMsgInfo])
case ntfMessage(user: UserRef, connEntity: ConnectionEntity, ntfMessage: NtfMsgInfo)
case contactConnectionDeleted(user: UserRef, connection: PendingContactConnection)
@@ -905,7 +912,7 @@ public enum ChatResponse: Decodable, Error {
case let .callEnded(u, contact): return withUser(u, "contact: \(contact.id)")
case let .callInvitations(invs): return String(describing: invs)
case let .ntfTokenStatus(status): return String(describing: status)
case let .ntfToken(token, status, ntfMode, ntfServer): return "token: \(token)\nstatus: \(status.rawValue)\nntfMode: \(ntfMode.rawValue)\nntfServer: \(ntfServer)"
case let .ntfToken(token, status, ntfMode): return "token: \(token)\nstatus: \(status.rawValue)\nntfMode: \(ntfMode.rawValue)"
case let .ntfMessages(u, connEntity, msgTs, ntfMessages): return withUser(u, "connEntity: \(String(describing: connEntity))\nmsgTs: \(String(describing: msgTs))\nntfMessages: \(String(describing: ntfMessages))")
case let .ntfMessage(u, connEntity, ntfMessage): return withUser(u, "connEntity: \(String(describing: connEntity))\nntfMessage: \(String(describing: ntfMessage))")
case let .contactConnectionDeleted(u, connection): return withUser(u, String(describing: connection))
@@ -998,6 +1005,10 @@ struct ComposedMessage: Encodable {
var msgContent: MsgContent
}
public struct XFTPFileConfig: Encodable {
var minFileSize: Int64
}
public struct ArchiveConfig: Encodable {
var archivePath: String
var disableCompression: Bool?
+4
View File
@@ -265,6 +265,10 @@ public class Default<T> {
}
}
public func getXFTPCfg() -> XFTPFileConfig {
return XFTPFileConfig(minFileSize: 0)
}
public func getNetCfg() -> NetCfg {
let onionHosts = networkUseOnionHostsGroupDefault.get()
let (hostMode, requiredHostMode) = onionHosts.hostMode
+2 -17
View File
@@ -1367,17 +1367,6 @@ public enum ChatInfo: Identifiable, Decodable, NamedChat {
}
}
public var chatTs: Date {
switch self {
case let .direct(contact): return contact.chatTs ?? contact.updatedAt
case let .group(groupInfo): return groupInfo.chatTs ?? groupInfo.updatedAt
case let .local(noteFolder): return noteFolder.chatTs
case let .contactRequest(contactRequest): return contactRequest.updatedAt
case let .contactConnection(contactConnection): return contactConnection.updatedAt
case .invalidJSON: return .now
}
}
public struct SampleData {
public var direct: ChatInfo
public var group: ChatInfo
@@ -1436,7 +1425,6 @@ public struct Contact: Identifiable, Decodable, NamedChat {
public var mergedPreferences: ContactUserPreferences
var createdAt: Date
var updatedAt: Date
var chatTs: Date?
var contactGroupMemberId: Int64?
var contactGrpInvSent: Bool
@@ -1756,7 +1744,6 @@ public struct GroupInfo: Identifiable, Decodable, NamedChat {
public var chatSettings: ChatSettings
var createdAt: Date
var updatedAt: Date
var chatTs: Date?
public var id: ChatId { get { "#\(groupId)" } }
public var apiId: Int64 { get { groupId } }
@@ -2062,7 +2049,6 @@ public struct NoteFolder: Identifiable, Decodable, NamedChat {
public var unread: Bool
var createdAt: Date
public var updatedAt: Date
var chatTs: Date
public var id: ChatId { get { "*\(noteFolderId)" } }
public var apiId: Int64 { get { noteFolderId } }
@@ -2084,8 +2070,7 @@ public struct NoteFolder: Identifiable, Decodable, NamedChat {
favorite: false,
unread: false,
createdAt: .now,
updatedAt: .now,
chatTs: .now
updatedAt: .now
)
}
@@ -2268,7 +2253,7 @@ public struct ChatItem: Identifiable, Decodable {
case .rcvDirectEvent(rcvDirectEvent: let rcvDirectEvent):
switch rcvDirectEvent {
case .contactDeleted: return false
case .profileUpdated: return false
case .profileUpdated: return true
}
case .rcvGroupEvent(rcvGroupEvent: let rcvGroupEvent):
switch rcvGroupEvent {
-1
View File
@@ -25,7 +25,6 @@ extern char *chat_parse_markdown(char *str);
extern char *chat_parse_server(char *str);
extern char *chat_password_hash(char *pwd, char *salt);
extern char *chat_valid_name(char *name);
extern int chat_json_length(char *str);
extern char *chat_encrypt_media(chat_ctrl ctl, char *key, char *frame, int len);
extern char *chat_decrypt_media(char *key, char *frame, int len);
+2 -107
View File
@@ -202,9 +202,6 @@
/* No comment provided by engineer. */
"%lld messages blocked" = "%lld Nachrichten blockiert";
/* No comment provided by engineer. */
"%lld messages blocked by admin" = "%lld Nachrichten wurden vom Administrator blockiert";
/* No comment provided by engineer. */
"%lld messages marked deleted" = "%lld Nachrichten als gelöscht markiert";
@@ -401,14 +398,11 @@
/* 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!" = "Es werden alle Nachrichten gelöscht. Dieser Vorgang kann nicht rückgängig gemacht werden!";
/* 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.";
/* No comment provided by engineer. */
"All new messages from %@ will be hidden!" = "Von %@ werden alle neuen Nachrichten ausgeblendet!";
"All new messages from %@ will be hidden!" = "Alle neuen Nachrichten von %@ werden verborgen!";
/* No comment provided by engineer. */
"All your contacts will remain connected." = "Alle Ihre Kontakte bleiben verbunden.";
@@ -587,32 +581,17 @@
/* No comment provided by engineer. */
"Block" = "Blockieren";
/* No comment provided by engineer. */
"Block for all" = "Für Alle blockieren";
/* No comment provided by engineer. */
"Block group members" = "Gruppenmitglieder blockieren";
/* No comment provided by engineer. */
"Block member" = "Mitglied blockieren";
/* No comment provided by engineer. */
"Block member for all?" = "Mitglied für Alle blockieren?";
/* No comment provided by engineer. */
"Block member?" = "Mitglied blockieren?";
/* No comment provided by engineer. */
"blocked" = "Blockiert";
/* rcv group event chat item */
"blocked %@" = "%@ wurde blockiert";
/* blocked chat item */
"blocked by admin" = "wurde vom Administrator blockiert";
/* No comment provided by engineer. */
"Blocked by admin" = "wurde vom Administrator blockiert";
"blocked" = "blockiert";
/* No comment provided by engineer. */
"bold" = "fett";
@@ -771,9 +750,6 @@
/* No comment provided by engineer. */
"Clear conversation?" = "Unterhaltung löschen?";
/* No comment provided by engineer. */
"Clear private notes?" = "Private Notizen löschen?";
/* No comment provided by engineer. */
"Clear verification" = "Überprüfung zurücknehmen";
@@ -912,9 +888,6 @@
/* connection information */
"connection:%@" = "Verbindung:%@";
/* profile update event chat item */
"contact %@ changed to %@" = "Der Kontaktname %1$@ wurde auf %2$@ geändert";
/* No comment provided by engineer. */
"Contact allows" = "Der Kontakt erlaubt";
@@ -999,12 +972,6 @@
/* No comment provided by engineer. */
"Create your profile" = "Erstellen Sie Ihr Profil";
/* No comment provided by engineer. */
"Created at" = "Erstellt um";
/* copied message info */
"Created at: %@" = "Erstellt um: %@";
/* No comment provided by engineer. */
"Created on %@" = "Erstellt am %@";
@@ -1557,9 +1524,6 @@
/* No comment provided by engineer. */
"Error creating member contact" = "Fehler beim Anlegen eines Mitglied-Kontaktes";
/* No comment provided by engineer. */
"Error creating message" = "Fehler beim Erstellen der Nachricht";
/* No comment provided by engineer. */
"Error creating profile!" = "Fehler beim Erstellen des Profils!";
@@ -1974,9 +1938,6 @@
/* No comment provided by engineer. */
"Import database" = "Datenbank importieren";
/* No comment provided by engineer. */
"Improved message delivery" = "Verbesserte Zustellung von Nachrichten";
/* No comment provided by engineer. */
"Improved privacy and security" = "Verbesserte Privatsphäre und Sicherheit";
@@ -2154,9 +2115,6 @@
/* No comment provided by engineer. */
"Join group" = "Treten Sie der Gruppe bei";
/* No comment provided by engineer. */
"Join group conversations" = "Gruppenunterhaltungen beitreten";
/* No comment provided by engineer. */
"Join group?" = "Der Gruppe beitreten?";
@@ -2292,9 +2250,6 @@
/* No comment provided by engineer. */
"Member" = "Mitglied";
/* profile update event chat item */
"member %@ changed to %@" = "Der Mitgliedsname %1$@ wurde auf %2$@ geändert";
/* rcv group event chat item */
"member connected" = "ist der Gruppe beigetreten";
@@ -2639,18 +2594,12 @@
/* No comment provided by engineer. */
"Password to show" = "Passwort anzeigen";
/* past/unknown group member */
"Past member %@" = "Ehemaliges Mitglied %@";
/* No comment provided by engineer. */
"Paste desktop address" = "Desktop-Adresse einfügen";
/* No comment provided by engineer. */
"Paste image" = "Bild einfügen";
/* No comment provided by engineer. */
"Paste link to connect!" = "Zum Verbinden den Link einfügen!";
/* No comment provided by engineer. */
"Paste the link you received" = "Fügen Sie den erhaltenen Link ein";
@@ -2738,9 +2687,6 @@
/* No comment provided by engineer. */
"Private filenames" = "Neutrale Dateinamen";
/* name of notes to self */
"Private notes" = "Private Notizen";
/* No comment provided by engineer. */
"Profile and server connections" = "Profil und Serververbindungen";
@@ -2855,9 +2801,6 @@
/* No comment provided by engineer. */
"Receiving via" = "Empfangen über";
/* No comment provided by engineer. */
"Recent history and improved [directory bot](simplex:/contact#/?v=1-4&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FeXSPwqTkKyDO3px4fLf1wx3MvPdjdLW3%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAaiv6MkMH44L2TcYrt_CsX3ZvM11WgbMEUn0hkIKTOho%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion)." = "Aktueller Nachrichtenverlauf und verbesserter [Gruppenverzeichnis-Bot](simplex:/contact#/?v=1-4&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FeXSPwqTkKyDO3px4fLf1wx3MvPdjdLW3%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAaiv6MkMH44L2TcYrt_CsX3ZvM11WgbMEUn0hkIKTOho%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion).";
/* No comment provided by engineer. */
"Recipients see updates as you type them." = "Die Empfänger sehen Nachrichtenaktualisierungen, während Sie sie eingeben.";
@@ -2912,12 +2855,6 @@
/* rcv group event chat item */
"removed %@" = "hat %@ aus der Gruppe entfernt";
/* profile update event chat item */
"removed contact address" = "Kontaktadresse wurde entfernt";
/* profile update event chat item */
"removed profile picture" = "Profil-Bild wurde entfernt";
/* rcv group event chat item */
"removed you" = "hat Sie aus der Gruppe entfernt";
@@ -3041,9 +2978,6 @@
/* No comment provided by engineer. */
"Save welcome message?" = "Begrüßungsmeldung speichern?";
/* message info title */
"Saved message" = "Gespeicherte Nachricht";
/* No comment provided by engineer. */
"Saved WebRTC ICE servers will be removed" = "Gespeicherte WebRTC ICE-Server werden entfernt";
@@ -3065,9 +2999,6 @@
/* No comment provided by engineer. */
"Search" = "Suche";
/* No comment provided by engineer. */
"Search bar accepts invitation links." = "Von der Suchleiste werden Einladungslinks akzeptiert.";
/* No comment provided by engineer. */
"Search or paste SimpleX link" = "Suchen oder fügen Sie den SimpleX-Link ein";
@@ -3224,12 +3155,6 @@
/* No comment provided by engineer. */
"Set it instead of system authentication." = "Anstelle der System-Authentifizierung festlegen.";
/* profile update event chat item */
"set new contact address" = "Neue Kontaktadresse wurde festgelegt";
/* profile update event chat item */
"set new profile picture" = "Neues Profil-Bild wurde festgelegt";
/* No comment provided by engineer. */
"Set passcode" = "Zugangscode einstellen";
@@ -3599,9 +3524,6 @@
/* No comment provided by engineer. */
"Trying to connect to the server used to receive messages from this contact." = "Versuche die Verbindung mit dem Server aufzunehmen, der für den Empfang von Nachrichten mit diesem Kontakt genutzt wird.";
/* No comment provided by engineer. */
"Turkish interface" = "Türkische Bedienoberfläche";
/* No comment provided by engineer. */
"Turn off" = "Abschalten";
@@ -3614,21 +3536,12 @@
/* No comment provided by engineer. */
"Unblock" = "Freigeben";
/* No comment provided by engineer. */
"Unblock for all" = "Für Alle freigeben";
/* No comment provided by engineer. */
"Unblock member" = "Mitglied freigeben";
/* No comment provided by engineer. */
"Unblock member for all?" = "Mitglied für Alle freigeben?";
/* No comment provided by engineer. */
"Unblock member?" = "Mitglied freigeben?";
/* rcv group event chat item */
"unblocked %@" = "%@ wurde freigegeben";
/* item status description */
"Unexpected error: %@" = "Unerwarteter Fehler: %@";
@@ -3662,9 +3575,6 @@
/* No comment provided by engineer. */
"Unknown error" = "Unbekannter Fehler";
/* No comment provided by engineer. */
"unknown status" = "unbekannter Gruppenmitglieds-Status";
/* No comment provided by engineer. */
"Unless you use iOS call interface, enable Do Not Disturb mode to avoid interruptions." = "Aktivieren Sie den Modus \"Bitte nicht stören\", um Unterbrechungen zu vermeiden, es sei denn, Sie verwenden die iOS Anrufschnittstelle.";
@@ -3710,9 +3620,6 @@
/* rcv group event chat item */
"updated group profile" = "Aktualisiertes Gruppenprofil";
/* profile update event chat item */
"updated profile" = "Das Profil wurde aktualisiert";
/* No comment provided by engineer. */
"Updating settings will re-connect the client to all servers." = "Die Aktualisierung der Einstellungen wird den Client wieder mit allen Servern verbinden.";
@@ -3887,15 +3794,9 @@
/* No comment provided by engineer. */
"When you share an incognito profile with somebody, this profile will be used for the groups they invite you to." = "Wenn Sie ein Inkognito-Profil mit Jemandem teilen, wird dieses Profil auch für die Gruppen verwendet, für die Sie von diesem Kontakt eingeladen werden.";
/* No comment provided by engineer. */
"With encrypted files and media." = "Mit verschlüsselten Dateien und Medien.";
/* No comment provided by engineer. */
"With optional welcome message." = "Mit optionaler Begrüßungsmeldung.";
/* No comment provided by engineer. */
"With reduced battery usage." = "Mit reduziertem Akkuverbrauch.";
/* No comment provided by engineer. */
"Wrong database passphrase" = "Falsches Datenbank-Passwort";
@@ -3956,9 +3857,6 @@
/* No comment provided by engineer. */
"you are observer" = "Sie sind Beobachter";
/* snd group event chat item */
"you blocked %@" = "Sie haben %@ blockiert";
/* No comment provided by engineer. */
"You can accept calls from lock screen, without device and app authentication." = "Sie können Anrufe ohne Geräte- und App-Authentifizierung vom Sperrbildschirm aus annehmen.";
@@ -4070,9 +3968,6 @@
/* chat list item description */
"you shared one-time link incognito" = "Sie haben Inkognito einen Einmal-Link geteilt";
/* snd group event chat item */
"you unblocked %@" = "Sie haben %@ freigegeben";
/* No comment provided by engineer. */
"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!";
-33
View File
@@ -202,9 +202,6 @@
/* No comment provided by engineer. */
"%lld messages blocked" = "%lld messaggi bloccati";
/* No comment provided by engineer. */
"%lld messages blocked by admin" = "%lld messaggi bloccati dall'amministratore";
/* No comment provided by engineer. */
"%lld messages marked deleted" = "%lld messaggi contrassegnati eliminati";
@@ -587,33 +584,18 @@
/* No comment provided by engineer. */
"Block" = "Blocca";
/* No comment provided by engineer. */
"Block for all" = "Blocca per tutti";
/* No comment provided by engineer. */
"Block group members" = "Blocca i membri dei gruppi";
/* No comment provided by engineer. */
"Block member" = "Blocca membro";
/* No comment provided by engineer. */
"Block member for all?" = "Bloccare il membro per tutti?";
/* No comment provided by engineer. */
"Block member?" = "Bloccare il membro?";
/* No comment provided by engineer. */
"blocked" = "bloccato";
/* rcv group event chat item */
"blocked %@" = "ha bloccato %@";
/* blocked chat item */
"blocked by admin" = "bloccato dall'amministratore";
/* No comment provided by engineer. */
"Blocked by admin" = "Bloccato dall'amministratore";
/* No comment provided by engineer. */
"bold" = "grassetto";
@@ -3614,21 +3596,12 @@
/* No comment provided by engineer. */
"Unblock" = "Sblocca";
/* No comment provided by engineer. */
"Unblock for all" = "Sblocca per tutti";
/* No comment provided by engineer. */
"Unblock member" = "Sblocca membro";
/* No comment provided by engineer. */
"Unblock member for all?" = "Sbloccare il membro per tutti?";
/* No comment provided by engineer. */
"Unblock member?" = "Sbloccare il membro?";
/* rcv group event chat item */
"unblocked %@" = "ha sbloccato %@";
/* item status description */
"Unexpected error: %@" = "Errore imprevisto: % @";
@@ -3956,9 +3929,6 @@
/* No comment provided by engineer. */
"you are observer" = "sei un osservatore";
/* snd group event chat item */
"you blocked %@" = "hai bloccato %@";
/* No comment provided by engineer. */
"You can accept calls from lock screen, without device and app authentication." = "Puoi accettare chiamate dalla schermata di blocco, senza l'autenticazione del dispositivo e dell'app.";
@@ -4070,9 +4040,6 @@
/* chat list item description */
"you shared one-time link incognito" = "hai condiviso un link incognito una tantum";
/* snd group event chat item */
"you unblocked %@" = "hai sbloccato %@";
/* No comment provided by engineer. */
"You will be connected to group when the group host's device is online, please wait or check later!" = "Verrai connesso/a al gruppo quando il dispositivo dell'host del gruppo sarà in linea, attendi o controlla più tardi!";
+3 -36
View File
@@ -202,9 +202,6 @@
/* No comment provided by engineer. */
"%lld messages blocked" = "%lld berichten geblokkeerd";
/* No comment provided by engineer. */
"%lld messages blocked by admin" = "%lld berichten geblokkeerd door beheerder";
/* No comment provided by engineer. */
"%lld messages marked deleted" = "%lld berichten gemarkeerd als verwijderd";
@@ -587,33 +584,18 @@
/* No comment provided by engineer. */
"Block" = "Blokkeren";
/* No comment provided by engineer. */
"Block for all" = "Blokkeren voor iedereen";
/* No comment provided by engineer. */
"Block group members" = "Groepsleden blokkeren";
/* No comment provided by engineer. */
"Block member" = "Lid blokkeren";
/* No comment provided by engineer. */
"Block member for all?" = "Lid voor iedereen blokkeren?";
/* No comment provided by engineer. */
"Block member?" = "Lid blokkeren?";
/* No comment provided by engineer. */
"blocked" = "geblokkeerd";
/* rcv group event chat item */
"blocked %@" = "geblokkeerd %@";
/* blocked chat item */
"blocked by admin" = "geblokkeerd door beheerder";
/* No comment provided by engineer. */
"Blocked by admin" = "Geblokkeerd door beheerder";
/* No comment provided by engineer. */
"bold" = "vetgedrukt";
@@ -2155,7 +2137,7 @@
"Join group" = "Word lid van groep";
/* No comment provided by engineer. */
"Join group conversations" = "Neem deel aan groepsgesprekken";
"Join group conversations" = "Neem deel aan groep gesprekken";
/* No comment provided by engineer. */
"Join group?" = "Deelnemen aan groep?";
@@ -2649,7 +2631,7 @@
"Paste image" = "Afbeelding plakken";
/* No comment provided by engineer. */
"Paste link to connect!" = "Plak een link om te verbinden!";
"Paste link to connect!" = "Plak link om te verbinden!";
/* No comment provided by engineer. */
"Paste the link you received" = "Plak de link die je hebt ontvangen";
@@ -3069,7 +3051,7 @@
"Search bar accepts invitation links." = "Zoekbalk accepteert uitnodigingslinks.";
/* No comment provided by engineer. */
"Search or paste SimpleX link" = "Zoek of plak een SimpleX link";
"Search or paste SimpleX link" = "Zoek of plak de SimpleX link";
/* network option */
"sec" = "sec";
@@ -3614,21 +3596,12 @@
/* No comment provided by engineer. */
"Unblock" = "Deblokkeren";
/* No comment provided by engineer. */
"Unblock for all" = "Deblokkeer voor iedereen";
/* No comment provided by engineer. */
"Unblock member" = "Lid deblokkeren";
/* No comment provided by engineer. */
"Unblock member for all?" = "Lid voor iedereen deblokkeren?";
/* No comment provided by engineer. */
"Unblock member?" = "Lid deblokkeren?";
/* rcv group event chat item */
"unblocked %@" = "gedeblokkeerd %@";
/* item status description */
"Unexpected error: %@" = "Onverwachte fout: %@";
@@ -3956,9 +3929,6 @@
/* No comment provided by engineer. */
"you are observer" = "jij bent waarnemer";
/* snd group event chat item */
"you blocked %@" = "je hebt %@ geblokkeerd";
/* No comment provided by engineer. */
"You can accept calls from lock screen, without device and app authentication." = "U kunt oproepen van het vergrendelingsscherm accepteren, zonder apparaat- en app-verificatie.";
@@ -4070,9 +4040,6 @@
/* chat list item description */
"you shared one-time link incognito" = "je hebt een eenmalige link incognito gedeeld";
/* snd group event chat item */
"you unblocked %@" = "je hebt %@ gedeblokkeerd";
/* No comment provided by engineer. */
"You will be connected to group when the group host's device is online, please wait or check later!" = "Je wordt verbonden met de groep wanneer het apparaat van de groep host online is, even geduld a.u.b. of controleer het later!";
-126
View File
@@ -202,9 +202,6 @@
/* No comment provided by engineer. */
"%lld messages blocked" = "%lld wiadomości zablokowanych";
/* No comment provided by engineer. */
"%lld messages blocked by admin" = "%lld wiadomości zablokowanych przez admina";
/* No comment provided by engineer. */
"%lld messages marked deleted" = "%lld wiadomości oznaczonych do usunięcia";
@@ -401,9 +398,6 @@
/* No comment provided by engineer. */
"All group members will remain connected." = "Wszyscy członkowie grupy pozostaną połączeni.";
/* No comment provided by engineer. */
"All messages will be deleted - this cannot be undone!" = "Wszystkie wiadomości zostaną usunięte nie można tego cofnąć!";
/* No comment provided by engineer. */
"All messages will be deleted - this cannot be undone! The messages will be deleted ONLY for you." = "Wszystkie wiadomości zostaną usunięte - nie można tego cofnąć! Wiadomości zostaną usunięte TYLKO dla Ciebie.";
@@ -587,33 +581,18 @@
/* No comment provided by engineer. */
"Block" = "Zablokuj";
/* No comment provided by engineer. */
"Block for all" = "Zablokuj dla wszystkich";
/* No comment provided by engineer. */
"Block group members" = "Blokuj członków grupy";
/* No comment provided by engineer. */
"Block member" = "Zablokuj członka";
/* No comment provided by engineer. */
"Block member for all?" = "Zablokować członka dla wszystkich?";
/* No comment provided by engineer. */
"Block member?" = "Zablokować członka?";
/* No comment provided by engineer. */
"blocked" = "zablokowany";
/* rcv group event chat item */
"blocked %@" = "zablokowany %@";
/* blocked chat item */
"blocked by admin" = "zablokowany przez admina";
/* No comment provided by engineer. */
"Blocked by admin" = "Zablokowany przez admina";
/* No comment provided by engineer. */
"bold" = "pogrubiona";
@@ -771,9 +750,6 @@
/* No comment provided by engineer. */
"Clear conversation?" = "Wyczyścić rozmowę?";
/* No comment provided by engineer. */
"Clear private notes?" = "Wyczyścić prywatne notatki?";
/* No comment provided by engineer. */
"Clear verification" = "Wyczyść weryfikację";
@@ -912,9 +888,6 @@
/* connection information */
"connection:%@" = "połączenie: %@";
/* profile update event chat item */
"contact %@ changed to %@" = "kontakt %1$@ zmieniony na %2$@";
/* No comment provided by engineer. */
"Contact allows" = "Kontakt pozwala";
@@ -999,12 +972,6 @@
/* No comment provided by engineer. */
"Create your profile" = "Utwórz swój profil";
/* No comment provided by engineer. */
"Created at" = "Utworzony o";
/* copied message info */
"Created at: %@" = "Utworzony o: %@";
/* No comment provided by engineer. */
"Created on %@" = "Utworzony w dniu %@";
@@ -1329,9 +1296,6 @@
/* No comment provided by engineer. */
"Do it later" = "Zrób to później";
/* No comment provided by engineer. */
"Do not send history to new members." = "Nie wysyłaj historii do nowych członków.";
/* No comment provided by engineer. */
"Do NOT use SimpleX for emergency calls." = "NIE używaj SimpleX do połączeń alarmowych.";
@@ -1557,9 +1521,6 @@
/* No comment provided by engineer. */
"Error creating member contact" = "Błąd tworzenia kontaktu członka";
/* No comment provided by engineer. */
"Error creating message" = "Błąd tworzenia wiadomości";
/* No comment provided by engineer. */
"Error creating profile!" = "Błąd tworzenia profilu!";
@@ -1914,9 +1875,6 @@
/* No comment provided by engineer. */
"History" = "Historia";
/* No comment provided by engineer. */
"History is not sent to new members." = "Historia nie jest wysyłana do nowych członków.";
/* time unit */
"hours" = "godziny";
@@ -1974,9 +1932,6 @@
/* No comment provided by engineer. */
"Import database" = "Importuj bazę danych";
/* No comment provided by engineer. */
"Improved message delivery" = "Ulepszona dostawa wiadomości";
/* No comment provided by engineer. */
"Improved privacy and security" = "Zwiększona prywatność i bezpieczeństwo";
@@ -2061,9 +2016,6 @@
/* invalid chat item */
"invalid data" = "nieprawidłowe dane";
/* No comment provided by engineer. */
"Invalid display name!" = "Nieprawidłowa nazwa wyświetlana!";
/* No comment provided by engineer. */
"Invalid link" = "Nieprawidłowy link";
@@ -2154,9 +2106,6 @@
/* No comment provided by engineer. */
"Join group" = "Dołącz do grupy";
/* No comment provided by engineer. */
"Join group conversations" = "Dołącz do grupowej rozmowy";
/* No comment provided by engineer. */
"Join group?" = "Dołączyć do grupy?";
@@ -2292,9 +2241,6 @@
/* No comment provided by engineer. */
"Member" = "Członek";
/* profile update event chat item */
"member %@ changed to %@" = "członek %1$@ zmieniony na %2$@";
/* rcv group event chat item */
"member connected" = "połączony";
@@ -2639,18 +2585,12 @@
/* No comment provided by engineer. */
"Password to show" = "Hasło do wyświetlenia";
/* past/unknown group member */
"Past member %@" = "Były członek %@";
/* No comment provided by engineer. */
"Paste desktop address" = "Wklej adres komputera";
/* No comment provided by engineer. */
"Paste image" = "Wklej obraz";
/* No comment provided by engineer. */
"Paste link to connect!" = "Wklej link, aby połączyć!";
/* No comment provided by engineer. */
"Paste the link you received" = "Wklej link, który otrzymałeś";
@@ -2738,9 +2678,6 @@
/* No comment provided by engineer. */
"Private filenames" = "Prywatne nazwy plików";
/* name of notes to self */
"Private notes" = "Prywatne notatki";
/* No comment provided by engineer. */
"Profile and server connections" = "Profil i połączenia z serwerem";
@@ -2855,9 +2792,6 @@
/* No comment provided by engineer. */
"Receiving via" = "Odbieranie przez";
/* No comment provided by engineer. */
"Recent history and improved [directory bot](simplex:/contact#/?v=1-4&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FeXSPwqTkKyDO3px4fLf1wx3MvPdjdLW3%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAaiv6MkMH44L2TcYrt_CsX3ZvM11WgbMEUn0hkIKTOho%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion)." = "Ostania historia i ulepszony [bot adresowy](simplex:/contact#/?v=1-4&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FeXSPwqTkKyDO3px4fLf1wx3MvPdjdLW3%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAaiv6MkMH44L2TcYrt_CsX3ZvM11WgbMEUn0hkIKTOho%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion).";
/* No comment provided by engineer. */
"Recipients see updates as you type them." = "Odbiorcy widzą aktualizacje podczas ich wpisywania.";
@@ -2912,12 +2846,6 @@
/* rcv group event chat item */
"removed %@" = "usunięto %@";
/* profile update event chat item */
"removed contact address" = "usunięto adres kontaktu";
/* profile update event chat item */
"removed profile picture" = "usunięto zdjęcie profilu";
/* rcv group event chat item */
"removed you" = "usunął cię";
@@ -3041,9 +2969,6 @@
/* No comment provided by engineer. */
"Save welcome message?" = "Zapisać wiadomość powitalną?";
/* message info title */
"Saved message" = "Zachowano wiadomość";
/* No comment provided by engineer. */
"Saved WebRTC ICE servers will be removed" = "Zapisane serwery WebRTC ICE zostaną usunięte";
@@ -3065,9 +2990,6 @@
/* No comment provided by engineer. */
"Search" = "Szukaj";
/* No comment provided by engineer. */
"Search bar accepts invitation links." = "Pasek wyszukiwania akceptuje linki zaproszenia.";
/* No comment provided by engineer. */
"Search or paste SimpleX link" = "Wyszukaj lub wklej link SimpleX";
@@ -3149,9 +3071,6 @@
/* No comment provided by engineer. */
"Send them from gallery or custom keyboards." = "Wyślij je z galerii lub niestandardowych klawiatur.";
/* No comment provided by engineer. */
"Send up to 100 last messages to new members." = "Wysyłaj do 100 ostatnich wiadomości do nowych członków.";
/* No comment provided by engineer. */
"Sender cancelled file transfer." = "Nadawca anulował transfer pliku.";
@@ -3224,12 +3143,6 @@
/* No comment provided by engineer. */
"Set it instead of system authentication." = "Ustaw go zamiast uwierzytelniania systemowego.";
/* profile update event chat item */
"set new contact address" = "ustaw nowy adres kontaktu";
/* profile update event chat item */
"set new profile picture" = "ustaw nowe zdjęcie profilu";
/* No comment provided by engineer. */
"Set passcode" = "Ustaw pin";
@@ -3536,9 +3449,6 @@
/* No comment provided by engineer. */
"This device name" = "Nazwa tego urządzenia";
/* No comment provided by engineer. */
"This display name is invalid. Please choose another name." = "Nazwa wyświetlana jest nieprawidłowa. Proszę wybrać inną nazwę.";
/* No comment provided by engineer. */
"This group has over %lld members, delivery receipts are not sent." = "Ta grupa ma ponad %lld członków, potwierdzenia dostawy nie są wysyłane.";
@@ -3599,9 +3509,6 @@
/* No comment provided by engineer. */
"Trying to connect to the server used to receive messages from this contact." = "Próbowanie połączenia z serwerem używanym do odbierania wiadomości od tego kontaktu.";
/* No comment provided by engineer. */
"Turkish interface" = "Turecki interfejs";
/* No comment provided by engineer. */
"Turn off" = "Wyłącz";
@@ -3614,21 +3521,12 @@
/* No comment provided by engineer. */
"Unblock" = "Odblokuj";
/* No comment provided by engineer. */
"Unblock for all" = "Odblokuj dla wszystkich";
/* No comment provided by engineer. */
"Unblock member" = "Odblokuj członka";
/* No comment provided by engineer. */
"Unblock member for all?" = "Odblokować członka dla wszystkich?";
/* No comment provided by engineer. */
"Unblock member?" = "Odblokować członka?";
/* rcv group event chat item */
"unblocked %@" = "odblokowano %@";
/* item status description */
"Unexpected error: %@" = "Nieoczekiwany błąd: %@";
@@ -3662,9 +3560,6 @@
/* No comment provided by engineer. */
"Unknown error" = "Nieznany błąd";
/* No comment provided by engineer. */
"unknown status" = "nieznany status";
/* No comment provided by engineer. */
"Unless you use iOS call interface, enable Do Not Disturb mode to avoid interruptions." = "O ile nie korzystasz z interfejsu połączeń systemu iOS, włącz tryb Nie przeszkadzać, aby uniknąć przerywania.";
@@ -3689,9 +3584,6 @@
/* No comment provided by engineer. */
"Unread" = "Nieprzeczytane";
/* No comment provided by engineer. */
"Up to 100 last messages are sent to new members." = "Do nowych członków wysyłanych jest do 100 ostatnich wiadomości.";
/* No comment provided by engineer. */
"Update" = "Aktualizuj";
@@ -3710,9 +3602,6 @@
/* rcv group event chat item */
"updated group profile" = "zaktualizowano profil grupy";
/* profile update event chat item */
"updated profile" = "zaktualizowano profil";
/* No comment provided by engineer. */
"Updating settings will re-connect the client to all servers." = "Aktualizacja ustawień spowoduje ponowne połączenie klienta ze wszystkimi serwerami.";
@@ -3821,9 +3710,6 @@
/* No comment provided by engineer. */
"View security code" = "Pokaż kod bezpieczeństwa";
/* chat feature */
"Visible history" = "Widoczna historia";
/* No comment provided by engineer. */
"Voice message…" = "Wiadomość głosowa…";
@@ -3887,15 +3773,9 @@
/* No comment provided by engineer. */
"When you share an incognito profile with somebody, this profile will be used for the groups they invite you to." = "Gdy udostępnisz komuś profil incognito, będzie on używany w grupach, do których Cię zaprosi.";
/* No comment provided by engineer. */
"With encrypted files and media." = "Z zaszyfrowanymi plikami i multimediami.";
/* No comment provided by engineer. */
"With optional welcome message." = "Z opcjonalną wiadomością powitalną.";
/* No comment provided by engineer. */
"With reduced battery usage." = "Ze zmniejszonym zużyciem baterii.";
/* No comment provided by engineer. */
"Wrong database passphrase" = "Nieprawidłowe hasło bazy danych";
@@ -3956,9 +3836,6 @@
/* No comment provided by engineer. */
"you are observer" = "jesteś obserwatorem";
/* snd group event chat item */
"you blocked %@" = "zablokowałeś %@";
/* No comment provided by engineer. */
"You can accept calls from lock screen, without device and app authentication." = "Możesz przyjmować połączenia z ekranu blokady, bez uwierzytelniania urządzenia i aplikacji.";
@@ -4070,9 +3947,6 @@
/* chat list item description */
"you shared one-time link incognito" = "udostępniłeś jednorazowy link incognito";
/* snd group event chat item */
"you unblocked %@" = "odblokowałeś %@";
/* No comment provided by engineer. */
"You will be connected to group when the group host's device is online, please wait or check later!" = "Zostaniesz połączony do grupy, gdy urządzenie gospodarza grupy będzie online, proszę czekać lub sprawdzić później!";
-33
View File
@@ -202,9 +202,6 @@
/* No comment provided by engineer. */
"%lld messages blocked" = "%lld сообщений заблокировано";
/* No comment provided by engineer. */
"%lld messages blocked by admin" = "%lld сообщений заблокировано администратором";
/* No comment provided by engineer. */
"%lld messages marked deleted" = "%lld сообщений помечено удалёнными";
@@ -587,33 +584,18 @@
/* No comment provided by engineer. */
"Block" = "Заблокировать";
/* No comment provided by engineer. */
"Block for all" = "Заблокировать для всех";
/* No comment provided by engineer. */
"Block group members" = "Блокируйте членов группы";
/* No comment provided by engineer. */
"Block member" = "Заблокировать члена группы";
/* No comment provided by engineer. */
"Block member for all?" = "Заблокировать члена для всех?";
/* No comment provided by engineer. */
"Block member?" = "Заблокировать члена группы?";
/* No comment provided by engineer. */
"blocked" = "заблокировано";
/* rcv group event chat item */
"blocked %@" = "%@ заблокирован";
/* blocked chat item */
"blocked by admin" = "заблокировано администратором";
/* No comment provided by engineer. */
"Blocked by admin" = "Заблокирован администратором";
/* No comment provided by engineer. */
"bold" = "жирный";
@@ -3614,21 +3596,12 @@
/* No comment provided by engineer. */
"Unblock" = "Разблокировать";
/* No comment provided by engineer. */
"Unblock for all" = "Разблокировать для всех";
/* No comment provided by engineer. */
"Unblock member" = "Разблокировать члена группы";
/* No comment provided by engineer. */
"Unblock member for all?" = "Разблокировать члена для всех?";
/* No comment provided by engineer. */
"Unblock member?" = "Разблокировать члена группы?";
/* rcv group event chat item */
"unblocked %@" = "%@ разблокирован";
/* item status description */
"Unexpected error: %@" = "Неожиданная ошибка: %@";
@@ -3956,9 +3929,6 @@
/* No comment provided by engineer. */
"you are observer" = "только чтение сообщений";
/* snd group event chat item */
"you blocked %@" = "Вы заблокировали %@";
/* No comment provided by engineer. */
"You can accept calls from lock screen, without device and app authentication." = "Вы можете принимать звонки на экране блокировки, без аутентификации.";
@@ -4070,9 +4040,6 @@
/* chat list item description */
"you shared one-time link incognito" = "Вы создали ссылку инкогнито";
/* snd group event chat item */
"you unblocked %@" = "Вы разблокировали %@";
/* No comment provided by engineer. */
"You will be connected to group when the group host's device is online, please wait or check later!" = "Соединение с группой будет установлено, когда хост группы будет онлайн. Пожалуйста, подождите или проверьте позже!";
+3 -3
View File
@@ -2,13 +2,13 @@
"\n" = "\n";
/* No comment provided by engineer. */
" " = " ";
" " = " . ";
/* No comment provided by engineer. */
" " = " ";
" " = " ";
/* No comment provided by engineer. */
" " = " ";
" " = " . ";
/* No comment provided by engineer. */
" (" = " (";
@@ -103,14 +103,11 @@
</intent-filter>
</activity-alias>
<activity android:name=".views.call.CallActivity"
<activity android:name=".views.call.IncomingCallActivity"
android:showOnLockScreen="true"
android:exported="false"
android:launchMode="singleInstance"
android:supportsPictureInPicture="true"
android:autoRemoveFromRecents="true"
android:screenOrientation="portrait"
android:configChanges="screenSize|smallestScreenSize|screenLayout|orientation"/>
android:launchMode="singleTask"/>
<provider
android:name="androidx.core.content.FileProvider"
@@ -136,18 +133,6 @@
android:stopWithTask="false"></service>
<!-- SimplexService restart on reboot -->
<service
android:name=".CallService"
android:enabled="true"
android:exported="false"
android:stopWithTask="false"/>
<receiver
android:name=".CallService$CallActionReceiver"
android:enabled="true"
android:exported="false" />
<receiver
android:name=".SimplexService$StartReceiver"
android:enabled="true"
@@ -1,176 +0,0 @@
package chat.simplex.app
import android.app.*
import android.content.*
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.os.*
import androidx.compose.ui.graphics.asAndroidBitmap
import androidx.core.app.NotificationCompat
import androidx.core.content.ContextCompat
import chat.simplex.app.model.NtfManager.EndCallAction
import chat.simplex.app.views.call.CallActivity
import chat.simplex.common.model.NotificationPreviewMode
import chat.simplex.common.platform.*
import chat.simplex.common.views.call.CallState
import chat.simplex.common.views.helpers.*
import chat.simplex.res.MR
import kotlinx.datetime.Instant
class CallService: Service() {
private var wakeLock: PowerManager.WakeLock? = null
private var notificationManager: NotificationManager? = null
private var serviceNotification: Notification? = null
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
Log.d(TAG, "onStartCommand startId: $startId")
if (intent != null) {
val action = intent.action
Log.d(TAG, "intent action $action")
when (action) {
Action.START.name -> startService()
else -> Log.e(TAG, "No action in the intent")
}
} else {
Log.d(TAG, "null intent. Probably restarted by the system.")
}
startForeground(CALL_SERVICE_ID, serviceNotification)
return START_STICKY
}
override fun onCreate() {
super.onCreate()
Log.d(TAG, "Call service created")
notificationManager = createNotificationChannel()
updateNotification()
startForeground(CALL_SERVICE_ID, serviceNotification)
}
override fun onDestroy() {
Log.d(TAG, "Call service destroyed")
try {
wakeLock?.let {
while (it.isHeld) it.release() // release all, in case acquired more than once
}
wakeLock = null
} catch (e: Exception) {
Log.d(TAG, "Exception while releasing wakelock: ${e.message}")
}
super.onDestroy()
}
private fun startService() {
Log.d(TAG, "CallService startService")
if (wakeLock != null) return
wakeLock = (getSystemService(Context.POWER_SERVICE) as PowerManager).run {
newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, WAKE_LOCK_TAG).apply {
acquire()
}
}
}
fun updateNotification() {
val call = chatModel.activeCall.value
val previewMode = appPreferences.notificationPreviewMode.get()
val title = if (previewMode == NotificationPreviewMode.HIDDEN.name)
generalGetString(MR.strings.notification_preview_somebody)
else
call?.contact?.profile?.displayName ?: ""
val text = generalGetString(if (call?.supportsVideo() == true) MR.strings.call_service_notification_video_call else MR.strings.call_service_notification_audio_call)
val image = call?.contact?.image
val largeIcon = if (image == null || previewMode == NotificationPreviewMode.HIDDEN.name)
BitmapFactory.decodeResource(resources, R.drawable.icon)
else
base64ToBitmap(image).asAndroidBitmap()
serviceNotification = createNotification(title, text, largeIcon, call?.connectedAt)
startForeground(CALL_SERVICE_ID, serviceNotification)
}
private fun createNotificationChannel(): NotificationManager? {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
val channel = NotificationChannel(CALL_NOTIFICATION_CHANNEL_ID, CALL_NOTIFICATION_CHANNEL_NAME, NotificationManager.IMPORTANCE_DEFAULT)
notificationManager.createNotificationChannel(channel)
return notificationManager
}
return null
}
private fun createNotification(title: String, text: String, icon: Bitmap, connectedAt: Instant? = null): Notification {
val pendingIntent: PendingIntent = Intent(this, CallActivity::class.java).let { notificationIntent ->
PendingIntent.getActivity(this, 0, notificationIntent, PendingIntent.FLAG_IMMUTABLE)
}
val endCallPendingIntent: PendingIntent = Intent(this, CallActionReceiver::class.java).let { notificationIntent ->
notificationIntent.setAction(EndCallAction)
PendingIntent.getBroadcast(this, 1, notificationIntent, PendingIntent.FLAG_CANCEL_CURRENT or PendingIntent.FLAG_IMMUTABLE)
}
val builder = NotificationCompat.Builder(this, CALL_NOTIFICATION_CHANNEL_ID)
.setSmallIcon(R.drawable.ntf_icon)
.setLargeIcon(icon.clipToCircle())
.setColor(0x88FFFF)
.setContentTitle(title)
.setContentText(text)
.setContentIntent(pendingIntent)
.setSilent(true)
.addAction(R.drawable.ntf_icon, generalGetString(MR.strings.call_service_notification_end_call), endCallPendingIntent)
if (connectedAt != null) {
builder.setUsesChronometer(true)
builder.setWhen(connectedAt.epochSeconds * 1000)
}
return builder.build()
}
override fun onBind(intent: Intent): IBinder {
return CallServiceBinder()
}
inner class CallServiceBinder : Binder() {
fun getService() = this@CallService
}
enum class Action {
START,
}
class CallActionReceiver: BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
when (intent?.action) {
EndCallAction -> {
val call = chatModel.activeCall.value
if (call != null) {
withBGApi {
chatModel.callManager.endCall(call)
}
}
}
else -> {
Log.e(TAG, "Unknown action. Make sure you provided an action")
}
}
}
}
companion object {
const val TAG = "CALL_SERVICE"
const val CALL_NOTIFICATION_CHANNEL_ID = "chat.simplex.app.CALL_SERVICE_NOTIFICATION"
const val CALL_NOTIFICATION_CHANNEL_NAME = "SimpleX Chat call service"
const val CALL_SERVICE_ID = 6788
const val WAKE_LOCK_TAG = "CallService::lock"
fun startService(): Intent {
Log.d(TAG, "CallService start")
return Intent(androidAppContext, CallService::class.java).also {
it.action = Action.START.name
ContextCompat.startForegroundService(androidAppContext, it)
}
}
fun stopService() {
androidAppContext.stopService(Intent(androidAppContext, CallService::class.java))
}
}
}
@@ -1,12 +1,11 @@
package chat.simplex.app
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.*
import android.view.WindowManager
import androidx.activity.compose.setContent
import androidx.compose.ui.platform.ClipboardManager
import androidx.appcompat.app.AppCompatDelegate
import androidx.fragment.app.FragmentActivity
import chat.simplex.app.model.NtfManager
import chat.simplex.app.model.NtfManager.getUserIdFromIntent
@@ -59,17 +58,6 @@ class MainActivity: FragmentActivity() {
override fun onResume() {
super.onResume()
AppLock.recheckAuthState()
withApi {
delay(1000)
if (!isAppOnForeground) return@withApi
/**
* When the app calls [ClipboardManager.shareText] and a user copies text in clipboard, Android denies
* access to clipboard because the app considered in background.
* This will ensure that the app will get the event on resume
* */
val service = getSystemService(Context.CLIPBOARD_SERVICE) as android.content.ClipboardManager
chatModel.clipboardHasText.value = service.hasPrimaryClip()
}
}
override fun onPause() {
@@ -1,15 +1,14 @@
package chat.simplex.app
import android.app.*
import android.app.Application
import android.content.Context
import androidx.compose.ui.platform.ClipboardManager
import chat.simplex.common.platform.Log
import android.content.Intent
import android.app.UiModeManager
import android.os.*
import androidx.lifecycle.*
import androidx.work.*
import chat.simplex.app.model.NtfManager
import chat.simplex.app.model.NtfManager.AcceptCallAction
import chat.simplex.app.views.call.CallActivity
import chat.simplex.common.helpers.APPLICATION_ID
import chat.simplex.common.helpers.requiresIgnoringBattery
import chat.simplex.common.model.*
@@ -19,7 +18,6 @@ import chat.simplex.common.platform.*
import chat.simplex.common.ui.theme.CurrentColors
import chat.simplex.common.ui.theme.DefaultTheme
import chat.simplex.common.views.call.RcvCallInvitation
import chat.simplex.common.views.call.activeCallDestroyWebView
import chat.simplex.common.views.helpers.*
import chat.simplex.common.views.onboarding.OnboardingStage
import com.jakewharton.processphoenix.ProcessPhoenix
@@ -73,7 +71,7 @@ class SimplexApp: Application(), LifecycleEventObserver {
override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) {
Log.d(TAG, "onStateChanged: $event")
withLongRunningApi {
withBGApi {
when (event) {
Lifecycle.Event.ON_START -> {
isAppOnForeground = true
@@ -99,6 +97,13 @@ class SimplexApp: Application(), LifecycleEventObserver {
}
Lifecycle.Event.ON_RESUME -> {
isAppOnForeground = true
/**
* When the app calls [ClipboardManager.shareText] and a user copies text in clipboard, Android denies
* access to clipboard because the app considered in background.
* This will ensure that the app will get the event on resume
* */
val service = androidAppContext.getSystemService(Context.CLIPBOARD_SERVICE) as android.content.ClipboardManager
chatModel.clipboardHasText.value = service.hasPrimaryClip()
if (chatModel.controller.appPrefs.onboardingStage.get() == OnboardingStage.OnboardingComplete && chatModel.currentUser.value != null) {
SimplexService.showBackgroundServiceNoticeIfNeeded()
}
@@ -186,28 +191,16 @@ class SimplexApp: Application(), LifecycleEventObserver {
SimplexService.safeStopService()
}
override fun androidCallServiceSafeStop() {
CallService.stopService()
}
override fun androidNotificationsModeChanged(mode: NotificationsMode) {
if (mode.requiresIgnoringBattery && !SimplexService.isBackgroundAllowed()) {
appPrefs.backgroundServiceNoticeShown.set(false)
}
SimplexService.StartReceiver.toggleReceiver(mode == NotificationsMode.SERVICE)
CoroutineScope(Dispatchers.Default).launch {
if (mode == NotificationsMode.SERVICE) {
if (mode == NotificationsMode.SERVICE)
SimplexService.start()
// Sometimes, when we change modes fast from one to another, system destroys the service after start.
// We can wait a little and restart the service, and it will work in 100% of cases
delay(2000)
if (!SimplexService.isServiceStarted && appPrefs.notificationsMode.get() == NotificationsMode.SERVICE) {
Log.i(TAG, "Service tried to start but destroyed by system, repeating once more")
SimplexService.start()
}
} else {
else
SimplexService.safeStopService()
}
}
if (mode != NotificationsMode.PERIODIC) {
@@ -260,28 +253,6 @@ class SimplexApp: Application(), LifecycleEventObserver {
uiModeManager.setApplicationNightMode(mode)
}
override fun androidStartCallActivity(acceptCall: Boolean, remoteHostId: Long?, chatId: ChatId?) {
val context = mainActivity.get() ?: return
val intent = Intent(context, CallActivity::class.java)
.addFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION)
if (acceptCall) {
intent.setAction(AcceptCallAction)
.putExtra("remoteHostId", remoteHostId)
.putExtra("chatId", chatId)
}
intent.flags += Intent.FLAG_ACTIVITY_BROUGHT_TO_FRONT
context.startActivity(intent)
}
override fun androidPictureInPictureAllowed(): Boolean {
val appOps = androidAppContext.getSystemService(Context.APP_OPS_SERVICE) as AppOpsManager
return appOps.checkOpNoThrow(AppOpsManager.OPSTR_PICTURE_IN_PICTURE, Process.myUid(), packageName) == AppOpsManager.MODE_ALLOWED
}
override fun androidCallEnded() {
activeCallDestroyWebView()
}
override suspend fun androidAskToAllowBackgroundCalls(): Boolean {
if (SimplexService.isBackgroundRestricted()) {
val userChoice: CompletableDeferred<Boolean> = CompletableDeferred()
@@ -34,13 +34,12 @@ import kotlin.system.exitProcess
class SimplexService: Service() {
private var wakeLock: PowerManager.WakeLock? = null
private var isCheckingNewMessages = false
private var isStartingService = false
private var notificationManager: NotificationManager? = null
private var serviceNotification: Notification? = null
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
Log.d(TAG, "onStartCommand startId: $startId")
isServiceStarting = false
if (intent != null) {
val action = intent.action
Log.d(TAG, "intent action $action")
@@ -72,7 +71,6 @@ class SimplexService: Service() {
stopForeground(true)
stopSelf()
} else {
isServiceStarting = false
isServiceStarted = true
// In case of self-destruct is enabled the initialization process will not start in SimplexApp, Let's start it here
if (DatabaseUtils.ksSelfDestructPassword.get() != null && chatModel.chatDbStatus.value == null) {
@@ -91,7 +89,6 @@ class SimplexService: Service() {
} catch (e: Exception) {
Log.d(TAG, "Exception while releasing wakelock: ${e.message}")
}
isServiceStarting = false
isServiceStarted = false
stopAfterStart = false
saveServiceState(this, ServiceState.STOPPED)
@@ -104,10 +101,10 @@ class SimplexService: Service() {
private fun startService() {
Log.d(TAG, "SimplexService startService")
if (wakeLock != null || isCheckingNewMessages) return
if (wakeLock != null || isStartingService) return
val self = this
isCheckingNewMessages = true
withLongRunningApi {
isStartingService = true
withLongRunningApi(slow = 30_000, deadlock = 60_000) {
val chatController = ChatController
waitDbMigrationEnds(chatController)
try {
@@ -126,7 +123,7 @@ class SimplexService: Service() {
}
}
} finally {
isCheckingNewMessages = false
isStartingService = false
}
}
}
@@ -265,8 +262,7 @@ class SimplexService: Service() {
private const val SHARED_PREFS_SERVICE_STATE = "SIMPLEX_SERVICE_STATE"
private const val WORK_NAME_ONCE = "ServiceStartWorkerOnce"
var isServiceStarting = false
var isServiceStarted = false
private var isServiceStarted = false
private var stopAfterStart = false
fun scheduleStart(context: Context) {
@@ -285,7 +281,7 @@ class SimplexService: Service() {
fun safeStopService() {
if (isServiceStarted) {
androidAppContext.stopService(Intent(androidAppContext, SimplexService::class.java))
} else if (isServiceStarting) {
} else {
stopAfterStart = true
}
}
@@ -295,7 +291,6 @@ class SimplexService: Service() {
withContext(Dispatchers.IO) {
Intent(androidAppContext, SimplexService::class.java).also {
it.action = action.name
isServiceStarting = true
ContextCompat.startForegroundService(androidAppContext, it)
}
}
@@ -13,7 +13,7 @@ import androidx.compose.ui.graphics.asAndroidBitmap
import androidx.core.app.*
import chat.simplex.app.*
import chat.simplex.app.TAG
import chat.simplex.app.views.call.CallActivity
import chat.simplex.app.views.call.IncomingCallActivity
import chat.simplex.app.views.call.getKeyguardManager
import chat.simplex.common.views.helpers.*
import chat.simplex.common.model.*
@@ -33,7 +33,6 @@ object NtfManager {
const val CallChannel: String = "chat.simplex.app.CALL_NOTIFICATION_2"
const val AcceptCallAction: String = "chat.simplex.app.ACCEPT_CALL"
const val RejectCallAction: String = "chat.simplex.app.REJECT_CALL"
const val EndCallAction: String = "chat.simplex.app.END_CALL"
const val CallNotificationId: Int = -1
private const val UserIdKey: String = "userId"
private const val ChatIdKey: String = "chatId"
@@ -158,7 +157,7 @@ object NtfManager {
val screenOff = displayManager.displays.all { it.state != Display.STATE_ON }
var ntfBuilder =
if ((keyguardManager.isKeyguardLocked || screenOff) && appPreferences.callOnLockScreen.get() != CallOnLockScreen.DISABLE) {
val fullScreenIntent = Intent(context, CallActivity::class.java)
val fullScreenIntent = Intent(context, IncomingCallActivity::class.java)
val fullScreenPendingIntent = PendingIntent.getActivity(context, 0, fullScreenIntent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)
NotificationCompat.Builder(context, CallChannel)
.setFullScreenIntent(fullScreenPendingIntent, true)
@@ -1,18 +1,17 @@
package chat.simplex.app.views.call
import android.app.*
import android.content.*
import android.content.res.Configuration
import android.graphics.Rect
import android.os.*
import android.util.Rational
import android.view.*
import android.app.Activity
import android.app.KeyguardManager
import android.content.Context
import android.content.Intent
import android.os.Build
import android.os.Bundle
import chat.simplex.common.platform.Log
import android.view.WindowManager
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.trackPipAnimationHintView
import androidx.compose.desktop.ui.tooling.preview.Preview
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.*
@@ -23,115 +22,33 @@ import androidx.compose.ui.draw.scale
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.lifecycle.Lifecycle
import chat.simplex.app.*
import chat.simplex.app.R
import chat.simplex.app.TAG
import chat.simplex.app.model.NtfManager
import chat.simplex.app.model.NtfManager.AcceptCallAction
import chat.simplex.common.model.*
import chat.simplex.common.platform.*
import chat.simplex.app.model.NtfManager.OpenChatAction
import chat.simplex.common.platform.ntfManager
import chat.simplex.common.ui.theme.*
import chat.simplex.common.views.call.*
import chat.simplex.common.views.helpers.*
import chat.simplex.res.MR
import dev.icerock.moko.resources.compose.stringResource
import kotlinx.coroutines.launch
import kotlinx.datetime.Clock
import java.lang.ref.WeakReference
import chat.simplex.common.platform.chatModel as m
class CallActivity: ComponentActivity(), ServiceConnection {
var boundService: CallService? = null
class IncomingCallActivity: ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
callActivity = WeakReference(this)
when (intent?.action) {
AcceptCallAction -> {
val remoteHostId = intent.getLongExtra("remoteHostId", -1).takeIf { it != -1L }
val chatId = intent.getStringExtra("chatId")
val invitation = (m.callInvitations.values + m.activeCallInvitation.value).lastOrNull {
it?.remoteHostId == remoteHostId && it?.contact?.id == chatId
}
if (invitation != null) {
m.callManager.acceptIncomingCall(invitation = invitation)
}
}
}
setContent { CallActivityView() }
if (isOnLockScreenNow()) {
unlockForIncomingCall()
}
setContent { IncomingCallActivityView(ChatModel) }
unlockForIncomingCall()
}
override fun onDestroy() {
super.onDestroy()
if (isOnLockScreenNow()) {
lockAfterIncomingCall()
}
try {
unbindService(this)
} catch (e: Exception) {
Log.i(TAG, "Unable to unbind service: " + e.stackTraceToString())
}
}
private fun isOnLockScreenNow() = getKeyguardManager(this).isKeyguardLocked
fun setPipParams(video: Boolean, sourceRectHint: Rect? = null, viewRatio: Rational? = null) {
// By manually specifying source rect we exclude empty background while toggling PiP
val builder = PictureInPictureParams.Builder()
.setAspectRatio(viewRatio)
.setSourceRectHint(sourceRectHint)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
builder.setAutoEnterEnabled(video)
}
setPictureInPictureParams(builder.build())
}
override fun onPictureInPictureModeChanged(isInPictureInPictureMode: Boolean, newConfig: Configuration) {
super.onPictureInPictureModeChanged(isInPictureInPictureMode, newConfig)
m.activeCallViewIsCollapsed.value = isInPictureInPictureMode
val layoutType = if (!isInPictureInPictureMode) {
LayoutType.Default
} else {
LayoutType.RemoteVideo
}
m.callCommand.add(WCallCommand.Layout(layoutType))
}
override fun onBackPressed() {
if (isOnLockScreenNow()) {
super.onBackPressed()
} else {
m.activeCallViewIsCollapsed.value = true
}
}
override fun onPictureInPictureRequested(): Boolean {
Log.d(TAG, "Requested picture-in-picture from the system")
return super.onPictureInPictureRequested()
}
override fun onUserLeaveHint() {
// On Android 12+ PiP is enabled automatically when a user hides the app
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.R && callSupportsVideo() && platform.androidPictureInPictureAllowed()) {
enterPictureInPictureMode()
}
}
override fun onResume() {
super.onResume()
m.activeCallViewIsCollapsed.value = false
lockAfterIncomingCall()
}
private fun unlockForIncomingCall() {
@@ -155,23 +72,6 @@ class CallActivity: ComponentActivity(), ServiceConnection {
}
}
fun startServiceAndBind() {
/**
* On Android 12 there is a bug that prevents starting activity after pressing back button
* (the error says that it denies to start activity in background).
* Workaround is to bind to a service
* */
bindService(CallService.startService(), this, 0)
}
override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
boundService = (service as CallService.CallServiceBinder).getService()
}
override fun onServiceDisconnected(name: ComponentName?) {
boundService = null
}
companion object {
const val activityFlags = WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON or WindowManager.LayoutParams.FLAG_ALLOW_LOCK_WHILE_SCREEN_ON
}
@@ -180,96 +80,38 @@ class CallActivity: ComponentActivity(), ServiceConnection {
fun getKeyguardManager(context: Context): KeyguardManager =
context.getSystemService(Context.KEYGUARD_SERVICE) as KeyguardManager
private fun callSupportsVideo() = m.activeCall.value?.supportsVideo() == true || m.activeCallInvitation.value?.callType?.media == CallMediaType.Video
@Composable
fun CallActivityView() {
fun IncomingCallActivityView(m: ChatModel) {
val switchingCall = m.switchingCall.value
val invitation = m.activeCallInvitation.value
val call = remember { m.activeCall }.value
val call = m.activeCall.value
val showCallView = m.showCallView.value
val activity = LocalContext.current as CallActivity
LaunchedEffect(Unit) {
snapshotFlow { m.activeCallViewIsCollapsed.value }
.collect { collapsed ->
when {
collapsed -> {
if (!platform.androidPictureInPictureAllowed() || !callSupportsVideo()) {
activity.moveTaskToBack(true)
activity.startActivity(Intent(activity, MainActivity::class.java))
} else if (!activity.isInPictureInPictureMode && activity.lifecycle.currentState == Lifecycle.State.RESUMED) {
// User pressed back button, show MainActivity
activity.startActivity(Intent(activity, MainActivity::class.java))
activity.enterPictureInPictureMode()
}
}
callSupportsVideo() && !platform.androidPictureInPictureAllowed() -> {
// PiP disabled by user
platform.androidStartCallActivity(false)
}
activity.isInPictureInPictureMode -> {
platform.androidStartCallActivity(false)
}
}
}
}
SimpleXTheme {
var prevCall by remember { mutableStateOf(call) }
KeyChangeEffect(m.activeCall.value) {
if (m.activeCall.value != null) {
prevCall = m.activeCall.value
activity.boundService?.updateNotification()
}
}
Box(Modifier.background(Color.Black)) {
if (call != null) {
val view = LocalView.current
ActiveCallView()
if (callSupportsVideo()) {
val scope = rememberCoroutineScope()
LaunchedEffect(Unit) {
scope.launch {
activity.setPipParams(callSupportsVideo(), viewRatio = Rational(view.width, view.height))
activity.trackPipAnimationHintView(view)
}
}
}
} else if (prevCall != null) {
prevCall?.let { ActiveCallOverlayDisabled(it) }
}
if (invitation != null) {
if (call == null) {
Surface(
Modifier
.fillMaxSize(),
color = MaterialTheme.colors.background,
contentColor = LocalContentColor.current
) {
IncomingCallLockScreenAlert(invitation, m)
}
} else {
IncomingCallAlertView(invitation, m)
}
}
}
}
LaunchedEffect(call == null) {
if (call != null) {
activity.startServiceAndBind()
}
}
val activity = LocalContext.current as Activity
LaunchedEffect(invitation, call, switchingCall, showCallView) {
if (!switchingCall && invitation == null && (!showCallView || call == null)) {
Log.d(TAG, "CallActivityView: finishing activity")
Log.d(TAG, "IncomingCallActivityView: finishing activity")
activity.finish()
}
}
SimpleXTheme {
Surface(
Modifier
.fillMaxSize(),
color = MaterialTheme.colors.background,
contentColor = LocalContentColor.current
) {
if (showCallView) {
Box {
ActiveCallView()
if (invitation != null) IncomingCallAlertView(invitation, m)
}
} else if (invitation != null) {
IncomingCallLockScreenAlert(invitation, m)
}
}
}
}
/**
* Related to lockscreen
* */
@Composable
fun IncomingCallLockScreenAlert(invitation: RcvCallInvitation, chatModel: ChatModel) {
val cm = chatModel.callManager
@@ -293,7 +135,7 @@ fun IncomingCallLockScreenAlert(invitation: RcvCallInvitation, chatModel: ChatMo
acceptCall = { cm.acceptIncomingCall(invitation = invitation) },
openApp = {
val intent = Intent(context, MainActivity::class.java)
.setAction(NtfManager.OpenChatAction)
.setAction(OpenChatAction)
.putExtra("userId", invitation.user.userId)
.putExtra("chatId", invitation.contact.id)
context.startActivity(intent)
@@ -4,7 +4,6 @@ import android.annotation.SuppressLint
import android.content.Context
import android.net.LocalServerSocket
import android.util.Log
import androidx.activity.ComponentActivity
import androidx.fragment.app.FragmentActivity
import chat.simplex.common.*
import chat.simplex.common.platform.*
@@ -26,8 +25,7 @@ val defaultLocale: Locale = Locale.getDefault()
@SuppressLint("StaticFieldLeak")
lateinit var androidAppContext: Context
var mainActivity: WeakReference<FragmentActivity> = WeakReference(null)
var callActivity: WeakReference<ComponentActivity> = WeakReference(null)
lateinit var mainActivity: WeakReference<FragmentActivity>
fun initHaskell() {
val socketName = "chat.simplex.app.local.socket.address.listen.native.cmd2" + Random.nextLong(100000)
@@ -61,16 +61,6 @@ actual fun cropToSquare(image: ImageBitmap): ImageBitmap {
return Bitmap.createBitmap(image.asAndroidBitmap(), xOffset, yOffset, side, side).asImageBitmap()
}
fun Bitmap.clipToCircle(): Bitmap {
val circle = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
val path = android.graphics.Path()
path.addCircle(width / 2f, height / 2f, min(width, height) / 2f, android.graphics.Path.Direction.CCW)
val canvas = android.graphics.Canvas(circle)
canvas.clipPath(path)
canvas.drawBitmap(this, 0f, 0f, null)
return circle
}
actual fun compressImageStr(bitmap: ImageBitmap): String {
val usePng = bitmap.hasAlpha()
val ext = if (usePng) "png" else "jpg"
@@ -14,27 +14,17 @@ import chat.simplex.common.views.helpers.*
import java.io.BufferedOutputStream
import java.io.File
import chat.simplex.res.MR
import kotlin.math.min
actual fun ClipboardManager.shareText(text: String) {
var text = text
for (i in 10 downTo 1) {
try {
val sendIntent: Intent = Intent().apply {
action = Intent.ACTION_SEND
putExtra(Intent.EXTRA_TEXT, text)
type = "text/plain"
flags = FLAG_ACTIVITY_NEW_TASK
}
val shareIntent = Intent.createChooser(sendIntent, null)
shareIntent.addFlags(FLAG_ACTIVITY_NEW_TASK)
androidAppContext.startActivity(shareIntent)
break
} catch (e: Exception) {
Log.e(TAG, "Failed to share text: ${e.stackTraceToString()}")
text = text.substring(0, min(i * 1000, text.length))
}
val sendIntent: Intent = Intent().apply {
action = Intent.ACTION_SEND
putExtra(Intent.EXTRA_TEXT, text)
type = "text/plain"
flags = FLAG_ACTIVITY_NEW_TASK
}
val shareIntent = Intent.createChooser(sendIntent, null)
shareIntent.addFlags(FLAG_ACTIVITY_NEW_TASK)
androidAppContext.startActivity(shareIntent)
}
actual fun shareFile(text: String, fileSource: CryptoFile) {
@@ -12,8 +12,6 @@ import androidx.activity.compose.setContent
import androidx.compose.runtime.*
import androidx.compose.ui.platform.LocalView
import chat.simplex.common.AppScreen
import chat.simplex.common.model.clear
import chat.simplex.common.ui.theme.SimpleXTheme
import chat.simplex.common.views.helpers.*
import androidx.compose.ui.platform.LocalContext as LocalContext1
import chat.simplex.res.MR
@@ -114,8 +112,7 @@ actual class GlobalExceptionsHandler: Thread.UncaughtExceptionHandler {
Handler(Looper.getMainLooper()).post {
AlertManager.shared.showAlertMsg(
title = generalGetString(MR.strings.app_was_crashed),
text = e.stackTraceToString(),
shareText = true
text = e.stackTraceToString()
)
}
}
@@ -28,7 +28,6 @@ import androidx.compose.ui.platform.LocalLifecycleOwner
import dev.icerock.moko.resources.compose.painterResource
import dev.icerock.moko.resources.compose.stringResource
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.viewinterop.AndroidView
import androidx.lifecycle.Lifecycle
@@ -51,30 +50,20 @@ import kotlinx.datetime.Clock
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.encodeToString
// Should be destroy()'ed and set as null when call is ended. Otherwise, it will be a leak
@SuppressLint("StaticFieldLeak")
private var staticWebView: WebView? = null
// WebView methods must be called on Main thread
fun activeCallDestroyWebView() = withApi {
// Stop it when call ended
platform.androidCallServiceSafeStop()
staticWebView?.destroy()
staticWebView = null
Log.d(TAG, "CallView: webview was destroyed")
}
@SuppressLint("SourceLockedOrientationActivity")
@Composable
actual fun ActiveCallView() {
val chatModel = ChatModel
BackHandler(onBack = {
val call = chatModel.activeCall.value
if (call != null) withBGApi { chatModel.callManager.endCall(call) }
})
val audioViaBluetooth = rememberSaveable { mutableStateOf(false) }
val proximityLock = remember {
val pm = (androidAppContext.getSystemService(Context.POWER_SERVICE) as PowerManager)
if (pm.isWakeLockLevelSupported(PROXIMITY_SCREEN_OFF_WAKE_LOCK)) {
pm.newWakeLock(PROXIMITY_SCREEN_OFF_WAKE_LOCK, androidAppContext.packageName + ":proximityLock")
} else {
null
}
val ntfModeService = remember { chatModel.controller.appPrefs.notificationsMode.get() == NotificationsMode.SERVICE }
LaunchedEffect(Unit) {
// Start service when call happening since it's not already started.
// It's needed to prevent Android from shutting down a microphone after a minute or so when screen is off
if (!ntfModeService) platform.androidServiceStart()
}
DisposableEffect(Unit) {
val am = androidAppContext.getSystemService(Context.AUDIO_SERVICE) as AudioManager
@@ -104,24 +93,22 @@ actual fun ActiveCallView() {
}
}
am.registerAudioDeviceCallback(audioCallback, null)
val pm = (androidAppContext.getSystemService(Context.POWER_SERVICE) as PowerManager)
val proximityLock = if (pm.isWakeLockLevelSupported(PROXIMITY_SCREEN_OFF_WAKE_LOCK)) {
pm.newWakeLock(PROXIMITY_SCREEN_OFF_WAKE_LOCK, androidAppContext.packageName + ":proximityLock")
} else {
null
}
proximityLock?.acquire()
onDispose {
// Stop it when call ended
if (!ntfModeService) platform.androidServiceSafeStop()
dropAudioManagerOverrides()
am.unregisterAudioDeviceCallback(audioCallback)
if (proximityLock?.isHeld == true) {
proximityLock.release()
}
}
}
LaunchedEffect(chatModel.activeCallViewIsCollapsed.value) {
if (chatModel.activeCallViewIsCollapsed.value) {
if (proximityLock?.isHeld == true) proximityLock.release()
} else {
delay(1000)
if (proximityLock?.isHeld == false) proximityLock.acquire()
proximityLock?.release()
}
}
val scope = rememberCoroutineScope()
val call = chatModel.activeCall.value
Box(Modifier.fillMaxSize()) {
WebRTCView(chatModel.callCommand) { apiMsg ->
Log.d(TAG, "received from WebRTCView: $apiMsg")
@@ -133,15 +120,15 @@ actual fun ActiveCallView() {
is WCallResponse.Capabilities -> withBGApi {
val callType = CallType(call.localMedia, r.capabilities)
chatModel.controller.apiSendCallInvitation(callRh, call.contact, callType)
updateActiveCall(call) { it.copy(callState = CallState.InvitationSent, localCapabilities = r.capabilities) }
chatModel.activeCall.value = call.copy(callState = CallState.InvitationSent, localCapabilities = r.capabilities)
}
is WCallResponse.Offer -> withBGApi {
chatModel.controller.apiSendCallOffer(callRh, call.contact, r.offer, r.iceCandidates, call.localMedia, r.capabilities)
updateActiveCall(call) { it.copy(callState = CallState.OfferSent, localCapabilities = r.capabilities) }
chatModel.activeCall.value = call.copy(callState = CallState.OfferSent, localCapabilities = r.capabilities)
}
is WCallResponse.Answer -> withBGApi {
chatModel.controller.apiSendCallAnswer(callRh, call.contact, r.answer, r.iceCandidates)
updateActiveCall(call) { it.copy(callState = CallState.Negotiated) }
chatModel.activeCall.value = call.copy(callState = CallState.Negotiated)
}
is WCallResponse.Ice -> withBGApi {
chatModel.controller.apiSendCallExtraInfo(callRh, call.contact, r.iceCandidates)
@@ -150,7 +137,7 @@ actual fun ActiveCallView() {
try {
val callStatus = json.decodeFromString<WebRTCCallStatus>("\"${r.state.connectionState}\"")
if (callStatus == WebRTCCallStatus.Connected) {
updateActiveCall(call) { it.copy(callState = CallState.Connected, connectedAt = Clock.System.now()) }
chatModel.activeCall.value = call.copy(callState = CallState.Connected, connectedAt = Clock.System.now())
setCallSound(call.soundSpeaker, audioViaBluetooth)
}
withBGApi { chatModel.controller.apiCallStatus(callRh, call.contact, callStatus) }
@@ -158,7 +145,7 @@ actual fun ActiveCallView() {
Log.d(TAG,"call status ${r.state.connectionState} not used")
}
is WCallResponse.Connected -> {
updateActiveCall(call) { it.copy(callState = CallState.Connected, connectionInfo = r.connectionInfo) }
chatModel.activeCall.value = call.copy(callState = CallState.Connected, connectionInfo = r.connectionInfo)
scope.launch {
setCallSound(call.soundSpeaker, audioViaBluetooth)
}
@@ -167,29 +154,27 @@ actual fun ActiveCallView() {
withBGApi { chatModel.callManager.endCall(call) }
}
is WCallResponse.Ended -> {
updateActiveCall(call) { it.copy(callState = CallState.Ended) }
chatModel.activeCall.value = call.copy(callState = CallState.Ended)
withBGApi { chatModel.callManager.endCall(call) }
chatModel.showCallView.value = false
}
is WCallResponse.Ok -> when (val cmd = apiMsg.command) {
is WCallCommand.Answer ->
updateActiveCall(call) { it.copy(callState = CallState.Negotiated) }
chatModel.activeCall.value = call.copy(callState = CallState.Negotiated)
is WCallCommand.Media -> {
updateActiveCall(call) {
when (cmd.media) {
CallMediaType.Video -> it.copy(videoEnabled = cmd.enable)
CallMediaType.Audio -> it.copy(audioEnabled = cmd.enable)
}
when (cmd.media) {
CallMediaType.Video -> chatModel.activeCall.value = call.copy(videoEnabled = cmd.enable)
CallMediaType.Audio -> chatModel.activeCall.value = call.copy(audioEnabled = cmd.enable)
}
}
is WCallCommand.Camera -> {
updateActiveCall(call) { it.copy(localCamera = cmd.camera) }
chatModel.activeCall.value = call.copy(localCamera = cmd.camera)
if (!call.audioEnabled) {
chatModel.callCommand.add(WCallCommand.Media(CallMediaType.Audio, enable = false))
}
}
is WCallCommand.End -> {
withBGApi { chatModel.callManager.endCall(call) }
}
is WCallCommand.End ->
chatModel.showCallView.value = false
else -> {}
}
is WCallResponse.Error -> {
@@ -198,16 +183,8 @@ actual fun ActiveCallView() {
}
}
}
val showOverlay = when {
call == null -> false
!platform.androidPictureInPictureAllowed() -> true
!call.supportsVideo() -> true
!chatModel.activeCallViewIsCollapsed.value -> true
else -> false
}
if (call != null && showOverlay) {
ActiveCallOverlay(call, chatModel, audioViaBluetooth)
}
val call = chatModel.activeCall.value
if (call != null) ActiveCallOverlay(call, chatModel, audioViaBluetooth)
}
val context = LocalContext.current
@@ -252,20 +229,6 @@ private fun ActiveCallOverlay(call: Call, chatModel: ChatModel, audioViaBluetoot
)
}
@Composable
fun ActiveCallOverlayDisabled(call: Call) {
ActiveCallOverlayLayout(
call = call,
speakerCanBeEnabled = false,
enabled = false,
dismiss = {},
toggleAudio = {},
toggleVideo = {},
toggleSound = {},
flipCamera = {}
)
}
private fun setCallSound(speaker: Boolean, audioViaBluetooth: MutableState<Boolean>) {
val am = androidAppContext.getSystemService(Context.AUDIO_SERVICE) as AudioManager
Log.d(TAG, "setCallSound: set audio mode, speaker enabled: $speaker")
@@ -308,69 +271,59 @@ private fun dropAudioManagerOverrides() {
private fun ActiveCallOverlayLayout(
call: Call,
speakerCanBeEnabled: Boolean,
enabled: Boolean = true,
dismiss: () -> Unit,
toggleAudio: () -> Unit,
toggleVideo: () -> Unit,
toggleSound: () -> Unit,
flipCamera: () -> Unit
) {
Column {
val media = call.peerMedia ?: call.localMedia
CloseSheetBar({ chatModel.activeCallViewIsCollapsed.value = true }, true, tintColor = Color(0xFFFFFFD8)) {
if (media == CallMediaType.Video) {
Text(call.contact.chatViewName, Modifier.fillMaxWidth().padding(end = DEFAULT_PADDING), color = Color(0xFFFFFFD8), style = MaterialTheme.typography.h2, overflow = TextOverflow.Ellipsis, maxLines = 1)
}
}
Column(Modifier.padding(horizontal = DEFAULT_PADDING)) {
when (media) {
CallMediaType.Video -> {
VideoCallInfoView(call)
Box(Modifier.fillMaxWidth().fillMaxHeight().weight(1f), contentAlignment = Alignment.BottomCenter) {
DisabledBackgroundCallsButton()
Column(Modifier.padding(DEFAULT_PADDING)) {
when (call.peerMedia ?: call.localMedia) {
CallMediaType.Video -> {
CallInfoView(call, alignment = Alignment.Start)
Box(Modifier.fillMaxWidth().fillMaxHeight().weight(1f), contentAlignment = Alignment.BottomCenter) {
DisabledBackgroundCallsButton()
}
Row(Modifier.fillMaxWidth().padding(horizontal = 6.dp), horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically) {
ToggleAudioButton(call, toggleAudio)
Spacer(Modifier.size(40.dp))
IconButton(onClick = dismiss) {
Icon(painterResource(MR.images.ic_call_end_filled), stringResource(MR.strings.icon_descr_hang_up), tint = Color.Red, modifier = Modifier.size(64.dp))
}
Row(Modifier.fillMaxWidth().padding(horizontal = 6.dp), horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically) {
ToggleAudioButton(call, enabled, toggleAudio)
Spacer(Modifier.size(40.dp))
IconButton(onClick = dismiss, enabled = enabled) {
Icon(painterResource(MR.images.ic_call_end_filled), stringResource(MR.strings.icon_descr_hang_up), tint = if (enabled) Color.Red else MaterialTheme.colors.secondary, modifier = Modifier.size(64.dp))
}
if (call.videoEnabled) {
ControlButton(call, painterResource(MR.images.ic_flip_camera_android_filled), MR.strings.icon_descr_flip_camera, enabled, flipCamera)
ControlButton(call, painterResource(MR.images.ic_videocam_filled), MR.strings.icon_descr_video_off, enabled, toggleVideo)
} else {
Spacer(Modifier.size(48.dp))
ControlButton(call, painterResource(MR.images.ic_videocam_off), MR.strings.icon_descr_video_on, enabled, toggleVideo)
}
if (call.videoEnabled) {
ControlButton(call, painterResource(MR.images.ic_flip_camera_android_filled), MR.strings.icon_descr_flip_camera, flipCamera)
ControlButton(call, painterResource(MR.images.ic_videocam_filled), MR.strings.icon_descr_video_off, toggleVideo)
} else {
Spacer(Modifier.size(48.dp))
ControlButton(call, painterResource(MR.images.ic_videocam_off), MR.strings.icon_descr_video_on, toggleVideo)
}
}
CallMediaType.Audio -> {
Spacer(Modifier.fillMaxHeight().weight(1f))
Column(
Modifier.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
ProfileImage(size = 192.dp, image = call.contact.profile.image)
AudioCallInfoView(call)
}
Box(Modifier.fillMaxWidth().fillMaxHeight().weight(1f), contentAlignment = Alignment.BottomCenter) {
DisabledBackgroundCallsButton()
}
Box(Modifier.fillMaxWidth().padding(bottom = DEFAULT_BOTTOM_PADDING), contentAlignment = Alignment.CenterStart) {
Box(Modifier.fillMaxWidth(), contentAlignment = Alignment.Center) {
IconButton(onClick = dismiss, enabled = enabled) {
Icon(painterResource(MR.images.ic_call_end_filled), stringResource(MR.strings.icon_descr_hang_up), tint = if (enabled) Color.Red else MaterialTheme.colors.secondary, modifier = Modifier.size(64.dp))
}
}
CallMediaType.Audio -> {
Spacer(Modifier.fillMaxHeight().weight(1f))
Column(
Modifier.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
ProfileImage(size = 192.dp, image = call.contact.profile.image)
CallInfoView(call, alignment = Alignment.CenterHorizontally)
}
Box(Modifier.fillMaxWidth().fillMaxHeight().weight(1f), contentAlignment = Alignment.BottomCenter) {
DisabledBackgroundCallsButton()
}
Box(Modifier.fillMaxWidth().padding(bottom = DEFAULT_BOTTOM_PADDING), contentAlignment = Alignment.CenterStart) {
Box(Modifier.fillMaxWidth(), contentAlignment = Alignment.Center) {
IconButton(onClick = dismiss) {
Icon(painterResource(MR.images.ic_call_end_filled), stringResource(MR.strings.icon_descr_hang_up), tint = Color.Red, modifier = Modifier.size(64.dp))
}
Box(Modifier.padding(start = 32.dp)) {
ToggleAudioButton(call, enabled, toggleAudio)
}
Box(Modifier.fillMaxWidth(), contentAlignment = Alignment.CenterEnd) {
Box(Modifier.padding(end = 32.dp)) {
ToggleSoundButton(call, speakerCanBeEnabled && enabled, toggleSound)
}
}
Box(Modifier.padding(start = 32.dp)) {
ToggleAudioButton(call, toggleAudio)
}
Box(Modifier.fillMaxWidth(), contentAlignment = Alignment.CenterEnd) {
Box(Modifier.padding(end = 32.dp)) {
ToggleSoundButton(call, speakerCanBeEnabled, toggleSound)
}
}
}
@@ -380,7 +333,7 @@ private fun ActiveCallOverlayLayout(
}
@Composable
private fun ControlButton(call: Call, icon: Painter, iconText: StringResource, enabled: Boolean = true, action: () -> Unit) {
private fun ControlButton(call: Call, icon: Painter, iconText: StringResource, action: () -> Unit, enabled: Boolean = true) {
if (call.hasMedia) {
IconButton(onClick = action, enabled = enabled) {
Icon(icon, stringResource(iconText), tint = if (enabled) Color(0xFFFFFFD8) else MaterialTheme.colors.secondary, modifier = Modifier.size(40.dp))
@@ -391,26 +344,28 @@ private fun ControlButton(call: Call, icon: Painter, iconText: StringResource, e
}
@Composable
private fun ToggleAudioButton(call: Call, enabled: Boolean = true, toggleAudio: () -> Unit) {
private fun ToggleAudioButton(call: Call, toggleAudio: () -> Unit) {
if (call.audioEnabled) {
ControlButton(call, painterResource(MR.images.ic_mic), MR.strings.icon_descr_audio_off, enabled, toggleAudio)
ControlButton(call, painterResource(MR.images.ic_mic), MR.strings.icon_descr_audio_off, toggleAudio)
} else {
ControlButton(call, painterResource(MR.images.ic_mic_off), MR.strings.icon_descr_audio_on, enabled, toggleAudio)
ControlButton(call, painterResource(MR.images.ic_mic_off), MR.strings.icon_descr_audio_on, toggleAudio)
}
}
@Composable
private fun ToggleSoundButton(call: Call, enabled: Boolean, toggleSound: () -> Unit) {
if (call.soundSpeaker) {
ControlButton(call, painterResource(MR.images.ic_volume_up), MR.strings.icon_descr_speaker_off, enabled, toggleSound)
ControlButton(call, painterResource(MR.images.ic_volume_up), MR.strings.icon_descr_speaker_off, toggleSound, enabled)
} else {
ControlButton(call, painterResource(MR.images.ic_volume_down), MR.strings.icon_descr_speaker_on, enabled, toggleSound)
ControlButton(call, painterResource(MR.images.ic_volume_down), MR.strings.icon_descr_speaker_on, toggleSound, enabled)
}
}
@Composable
fun AudioCallInfoView(call: Call) {
Column(horizontalAlignment = Alignment.CenterHorizontally) {
fun CallInfoView(call: Call, alignment: Alignment.Horizontal) {
@Composable fun InfoText(text: String, style: TextStyle = MaterialTheme.typography.body2) =
Text(text, color = Color(0xFFFFFFD8), style = style)
Column(horizontalAlignment = alignment) {
InfoText(call.contact.chatViewName, style = MaterialTheme.typography.h2)
InfoText(call.callState.text)
@@ -420,21 +375,6 @@ fun AudioCallInfoView(call: Call) {
}
}
@Composable
fun VideoCallInfoView(call: Call) {
Column(horizontalAlignment = Alignment.Start) {
InfoText(call.callState.text)
val connInfo = call.connectionInfo
val connInfoText = if (connInfo == null) "" else " (${connInfo.text})"
InfoText(call.encryptionStatus + connInfoText)
}
}
@Composable
fun InfoText(text: String, modifier: Modifier = Modifier, style: TextStyle = MaterialTheme.typography.body2) =
Text(text, modifier, color = Color(0xFFFFFFD8), style = style)
@Composable
private fun DisabledBackgroundCallsButton() {
var show by remember { mutableStateOf(!platform.androidIsBackgroundCallAllowed()) }
@@ -512,6 +452,7 @@ private fun DisabledBackgroundCallsButton() {
@Composable
fun WebRTCView(callCommand: SnapshotStateList<WCallCommand>, onResponse: (WVAPIMessage) -> Unit) {
val scope = rememberCoroutineScope()
val webView = remember { mutableStateOf<WebView?>(null) }
val permissionsState = rememberMultiplePermissionsState(
permissions = listOf(
@@ -534,10 +475,10 @@ fun WebRTCView(callCommand: SnapshotStateList<WCallCommand>, onResponse: (WVAPIM
}
lifecycleOwner.lifecycle.addObserver(observer)
onDispose {
val wv = webView.value
if (wv != null) processCommand(wv, WCallCommand.End)
lifecycleOwner.lifecycle.removeObserver(observer)
// val wv = webView.value
// if (wv != null) processCommand(wv, WCallCommand.End)
// webView.value?.destroy()
webView.value?.destroy()
webView.value = null
}
}
@@ -564,7 +505,7 @@ fun WebRTCView(callCommand: SnapshotStateList<WCallCommand>, onResponse: (WVAPIM
Box(Modifier.fillMaxSize()) {
AndroidView(
factory = { AndroidViewContext ->
(staticWebView ?: WebView(androidAppContext)).apply {
WebView(AndroidViewContext).apply {
layoutParams = ViewGroup.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT,
@@ -589,11 +530,7 @@ fun WebRTCView(callCommand: SnapshotStateList<WCallCommand>, onResponse: (WVAPIM
webViewSettings.javaScriptEnabled = true
webViewSettings.mediaPlaybackRequiresUserGesture = false
webViewSettings.cacheMode = WebSettings.LOAD_NO_CACHE
if (staticWebView == null) {
this.loadUrl("file:android_asset/www/android/call.html")
} else {
webView.value = this
}
this.loadUrl("file:android_asset/www/android/call.html")
}
}
) { /* WebView */ }
@@ -617,15 +554,6 @@ class WebRTCInterface(private val onResponse: (WVAPIMessage) -> Unit) {
}
}
private fun updateActiveCall(initial: Call, transform: (Call) -> Call) {
val activeCall = chatModel.activeCall.value
if (activeCall != null && activeCall.contact.apiId == initial.contact.apiId) {
chatModel.activeCall.value = transform(activeCall)
} else {
Log.d(TAG, "withActiveCall: ignoring, not in call with the contact ${activeCall?.contact?.id}")
}
}
private class LocalContentWebViewClient(val webView: MutableState<WebView?>, private val assetLoader: WebViewAssetLoader) : WebViewClientCompat() {
override fun shouldInterceptRequest(
view: WebView,
@@ -638,7 +566,6 @@ private class LocalContentWebViewClient(val webView: MutableState<WebView?>, pri
super.onPageFinished(view, url)
view.evaluateJavascript("sendMessageToNative = (msg) => WebRTCInterface.postMessage(JSON.stringify(msg))", null)
webView.value = view
staticWebView = view
Log.d(TAG, "WebRTCView: webview ready")
// for debugging
// view.evaluateJavascript("sendMessageToNative = ({resp}) => WebRTCInterface.postMessage(JSON.stringify({command: resp}))", null)
@@ -652,7 +579,6 @@ fun PreviewActiveCallOverlayVideo() {
ActiveCallOverlayLayout(
call = Call(
remoteHostId = null,
userProfile = Profile.sampleData,
contact = Contact.sampleData,
callState = CallState.Negotiated,
localMedia = CallMediaType.Video,
@@ -679,7 +605,6 @@ fun PreviewActiveCallOverlayAudio() {
ActiveCallOverlayLayout(
call = Call(
remoteHostId = null,
userProfile = Profile.sampleData,
contact = Contact.sampleData,
callState = CallState.Negotiated,
localMedia = CallMediaType.Audio,
@@ -1,112 +1,8 @@
package chat.simplex.common.views.chatlist
import android.app.Activity
import androidx.compose.foundation.*
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.*
import androidx.compose.material.ripple.rememberRipple
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.platform.*
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import chat.simplex.common.ANDROID_CALL_TOP_PADDING
import chat.simplex.common.model.durationText
import chat.simplex.common.platform.*
import chat.simplex.common.ui.theme.*
import chat.simplex.common.views.call.*
import chat.simplex.common.views.helpers.*
import chat.simplex.res.MR
import dev.icerock.moko.resources.compose.painterResource
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.datetime.Clock
private val CALL_INTERACTIVE_AREA_HEIGHT = 74.dp
private val CALL_TOP_OFFSET = (-10).dp
private val CALL_TOP_GREEN_LINE_HEIGHT = ANDROID_CALL_TOP_PADDING - CALL_TOP_OFFSET
private val CALL_BOTTOM_ICON_OFFSET = (-15).dp
private val CALL_BOTTOM_ICON_HEIGHT = CALL_INTERACTIVE_AREA_HEIGHT + CALL_BOTTOM_ICON_OFFSET
@Composable
actual fun ActiveCallInteractiveArea(call: Call, newChatSheetState: MutableStateFlow<AnimatedViewState>) {
val onClick = { platform.androidStartCallActivity(false) }
Box(Modifier.offset(y = CALL_TOP_OFFSET).height(CALL_INTERACTIVE_AREA_HEIGHT)) {
val source = remember { MutableInteractionSource() }
val indication = rememberRipple(bounded = true, 3000.dp)
Box(Modifier.height(CALL_TOP_GREEN_LINE_HEIGHT).clickable(onClick = onClick, indication = indication, interactionSource = source)) {
GreenLine(call)
}
Box(
Modifier
.offset(y = CALL_BOTTOM_ICON_OFFSET)
.size(CALL_BOTTOM_ICON_HEIGHT)
.background(SimplexGreen, CircleShape)
.clip(CircleShape)
.clickable(onClick = onClick, indication = indication, interactionSource = source)
.align(Alignment.BottomCenter),
contentAlignment = Alignment.Center
) {
val media = call.peerMedia ?: call.localMedia
if (media == CallMediaType.Video) {
Icon(painterResource(MR.images.ic_videocam_filled), null, Modifier.size(27.dp).offset(x = 2.5.dp, y = 2.dp), tint = Color.White)
} else {
Icon(painterResource(MR.images.ic_call_filled), null, Modifier.size(27.dp).offset(x = -0.5.dp, y = 2.dp), tint = Color.White)
}
}
}
}
@Composable
private fun GreenLine(call: Call) {
Row(
Modifier
.fillMaxSize()
.background(SimplexGreen)
.padding(top = -CALL_TOP_OFFSET)
.padding(horizontal = DEFAULT_PADDING),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Center
) {
ContactName(call.contact.displayName)
Spacer(Modifier.weight(1f))
CallDuration(call)
}
val window = (LocalContext.current as Activity).window
DisposableEffect(Unit) {
window.statusBarColor = SimplexGreen.toArgb()
onDispose {
window.statusBarColor = Color.Black.toArgb()
}
}
}
@Composable
private fun ContactName(name: String) {
Text(name, Modifier.width(windowWidth() * 0.35f), color = Color.White, maxLines = 1, overflow = TextOverflow.Ellipsis)
}
@Composable
private fun CallDuration(call: Call) {
val connectedAt = call.connectedAt
if (connectedAt != null) {
val time = remember { mutableStateOf(durationText(0)) }
LaunchedEffect(Unit) {
while (true) {
time.value = durationText((Clock.System.now() - connectedAt).inWholeSeconds.toInt())
delay(250)
}
}
val text = time.value
val sp40Or50 = with(LocalDensity.current) { if (text.length >= 6) 60.sp.toDp() else 42.sp.toDp() }
val offset = with(LocalDensity.current) { 7.sp.toDp() }
Text(text, Modifier.offset(x = offset).widthIn(min = sp40Or50), color = Color.White)
}
}
actual fun DesktopActiveCallOverlayLayout(newChatSheetState: MutableStateFlow<AnimatedViewState>) {}
@@ -1,106 +0,0 @@
package chat.simplex.common.views.helpers
import androidx.compose.foundation.layout.*
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import chat.simplex.common.model.CustomTimeUnit
import chat.simplex.common.ui.theme.DEFAULT_PADDING
import com.sd.lib.compose.wheel_picker.*
@Composable
actual fun CustomTimePicker(
selection: MutableState<Int>,
timeUnitsLimits: List<TimeUnitLimits>
) {
fun getUnitValues(unit: CustomTimeUnit, selectedValue: Int): List<Int> {
val unitLimits = timeUnitsLimits.firstOrNull { it.timeUnit == unit } ?: TimeUnitLimits.defaultUnitLimits(unit)
val regularUnitValues = (unitLimits.minValue..unitLimits.maxValue).toList()
return regularUnitValues + if (regularUnitValues.contains(selectedValue)) emptyList() else listOf(selectedValue)
}
val (unit, duration) = CustomTimeUnit.toTimeUnit(selection.value)
val selectedUnit: MutableState<CustomTimeUnit> = remember { mutableStateOf(unit) }
val selectedDuration = remember { mutableStateOf(duration) }
val selectedUnitValues = remember { mutableStateOf(getUnitValues(selectedUnit.value, selectedDuration.value)) }
val isTriggered = remember { mutableStateOf(false) }
LaunchedEffect(selectedUnit.value) {
// on initial composition, if passed selection doesn't fit into picker bounds, so that selectedDuration is bigger than selectedUnit maxValue
// (e.g., for selection = 121 seconds: selectedUnit would be Second, selectedDuration would be 121 > selectedUnit maxValue of 120),
// selectedDuration would've been replaced by maxValue - isTriggered check prevents this by skipping LaunchedEffect on initial composition
if (isTriggered.value) {
val maxValue = timeUnitsLimits.firstOrNull { it.timeUnit == selectedUnit.value }?.maxValue
if (maxValue != null && selectedDuration.value > maxValue) {
selectedDuration.value = maxValue
selectedUnitValues.value = getUnitValues(selectedUnit.value, selectedDuration.value)
} else {
selectedUnitValues.value = getUnitValues(selectedUnit.value, selectedDuration.value)
selection.value = selectedUnit.value.toSeconds * selectedDuration.value
}
} else {
isTriggered.value = true
}
}
LaunchedEffect(selectedDuration.value) {
selection.value = selectedUnit.value.toSeconds * selectedDuration.value
}
Row(
Modifier
.fillMaxWidth()
.padding(horizontal = DEFAULT_PADDING),
horizontalArrangement = Arrangement.spacedBy(0.dp)
) {
Column(Modifier.weight(1f)) {
val durationPickerState = rememberFWheelPickerState(selectedUnitValues.value.indexOf(selectedDuration.value))
FVerticalWheelPicker(
count = selectedUnitValues.value.count(),
state = durationPickerState,
unfocusedCount = 2,
focus = {
FWheelPickerFocusVertical(dividerColor = MaterialTheme.colors.primary)
}
) { index ->
Text(
selectedUnitValues.value[index].toString(),
fontSize = 18.sp,
color = MaterialTheme.colors.primary
)
}
LaunchedEffect(durationPickerState) {
snapshotFlow { durationPickerState.currentIndex }
.collect {
selectedDuration.value = selectedUnitValues.value[it]
}
}
}
Column(Modifier.weight(1f)) {
val unitPickerState = rememberFWheelPickerState(timeUnitsLimits.indexOfFirst { it.timeUnit == selectedUnit.value })
FVerticalWheelPicker(
count = timeUnitsLimits.count(),
state = unitPickerState,
unfocusedCount = 2,
focus = {
FWheelPickerFocusVertical(dividerColor = MaterialTheme.colors.primary)
}
) { index ->
Text(
timeUnitsLimits[index].timeUnit.text,
fontSize = 18.sp,
color = MaterialTheme.colors.primary
)
}
LaunchedEffect(unitPickerState) {
snapshotFlow { unitPickerState.currentIndex }
.collect {
selectedUnit.value = timeUnitsLimits[it].timeUnit
}
}
}
}
}
@@ -66,7 +66,6 @@ extern char *chat_parse_markdown(const char *str);
extern char *chat_parse_server(const char *str);
extern char *chat_password_hash(const char *pwd, const char *salt);
extern char *chat_valid_name(const char *name);
extern int chat_json_length(const char *str);
extern char *chat_write_file(chat_ctrl ctrl, const char *path, char *ptr, int length);
extern char *chat_read_file(const char *path, const char *key, const char *nonce);
extern char *chat_encrypt_file(chat_ctrl ctrl, const char *from_path, const char *to_path);
@@ -164,14 +163,6 @@ Java_chat_simplex_common_platform_CoreKt_chatValidName(JNIEnv *env, jclass clazz
return res;
}
JNIEXPORT int JNICALL
Java_chat_simplex_common_platform_CoreKt_chatJsonLength(JNIEnv *env, jclass clazz, jstring str) {
const char *_str = (*env)->GetStringUTFChars(env, str, JNI_FALSE);
int res = chat_json_length(_str);
(*env)->ReleaseStringUTFChars(env, str, _str);
return res;
}
JNIEXPORT jstring JNICALL
Java_chat_simplex_common_platform_CoreKt_chatWriteFile(JNIEnv *env, jclass clazz, jlong controller, jstring path, jobject buffer) {
const char *_path = (*env)->GetStringUTFChars(env, path, JNI_FALSE);
@@ -71,7 +71,7 @@ if(NOT APPLE)
else()
# Without direct linking it can't find hs_init in linking step
add_library( rts SHARED IMPORTED )
FILE(GLOB RTSLIB ${CMAKE_SOURCE_DIR}/libs/${OS_LIB_PATH}-${OS_LIB_ARCH}/libHSrts*_thr-*.${OS_LIB_EXT})
FILE(GLOB RTSLIB ${CMAKE_SOURCE_DIR}/libs/${OS_LIB_PATH}-${OS_LIB_ARCH}/deps/libHSrts*_thr-*.${OS_LIB_EXT})
set_target_properties( rts PROPERTIES IMPORTED_LOCATION ${RTSLIB})
target_link_libraries(app-lib rts simplex)
@@ -8,34 +8,13 @@ void hs_init_with_rtsopts(int * argc, char **argv[]);
JNIEXPORT void JNICALL
Java_chat_simplex_common_platform_CoreKt_initHS(JNIEnv *env, jclass clazz) {
// setup static arena for bump allocation and passing to RTS
char *argv[32] = {NULL,};
int argc = 0; // number of arguments used so far, always stands at the first NULL in argv
// common args
argv[argc++] = "simplex"; // fake program name
argv[argc++] = "+RTS"; // start adding RTS options
argv[argc++] = "-T"; // make GC counters available from inside the program
argv[argc++] = "-A16m"; // chunk size for new allocations (less frequent GC)
argv[argc++] = "-H64m"; // larger heap size on start (faster boot)
argv[argc++] = "-M8G"; // keep memory usage under 8G, collecting more aggressively when approaching it (and crashing sooner rather than taking down the whole system)
int eventlog = 1; // TODO: geto option flag from app (pointer for -ol?)
if (eventlog) {
argv[argc++] = "-olsimplex.eventlog"; // produce simplex.eventlog in "current" directory
argv[argc++] = "-l-agu"; // collect GC and user events
}
int profiling = 1; // TODO: get option flag from app (pointer for -po?)
if (profiling) {
argv[argc++] = "-posimplex"; // produce simplex.hp in "current" directory
argv[argc++] = "-hT"; // emit heap profile by closure type
}
int non_moving_gc = !profiling;
#ifdef _WIN32
// non-moving GC is broken on windows with GHC 9.4-9.6.3
non_moving_gc = 0;
int argc = 4;
char *argv[] = {"simplex", "+RTS", "-A16m", "-H64m", NULL}; // non-moving GC is broken on windows with GHC 9.4-9.6.3
#else
int argc = 5;
char *argv[] = {"simplex", "+RTS", "-A16m", "-H64m", "-xn", NULL}; // see android/simplex-api.c for details
#endif
if (non_moving_gc)
argv[argc++] = "-xn";
// wrap args as expected by RTS
char **pargv = argv;
hs_init_with_rtsopts(&argc, &pargv);
}
@@ -60,7 +39,6 @@ extern char *chat_parse_markdown(const char *str);
extern char *chat_parse_server(const char *str);
extern char *chat_password_hash(const char *pwd, const char *salt);
extern char *chat_valid_name(const char *name);
extern int chat_json_length(const char *str);
extern char *chat_write_file(chat_ctrl ctrl, const char *path, char *ptr, int length);
extern char *chat_read_file(const char *path, const char *key, const char *nonce);
extern char *chat_encrypt_file(chat_ctrl ctrl, const char *from_path, const char *to_path);
@@ -195,14 +173,6 @@ Java_chat_simplex_common_platform_CoreKt_chatValidName(JNIEnv *env, jclass clazz
return res;
}
JNIEXPORT int JNICALL
Java_chat_simplex_common_platform_CoreKt_chatJsonLength(JNIEnv *env, jclass clazz, jstring str) {
const char *_str = encode_to_utf8_chars(env, str);
int res = chat_json_length(_str);
(*env)->ReleaseStringUTFChars(env, str, _str);
return res;
}
JNIEXPORT jstring JNICALL
Java_chat_simplex_common_platform_CoreKt_chatWriteFile(JNIEnv *env, jclass clazz, jlong controller, jstring path, jobject buffer) {
const char *_path = encode_to_utf8_chars(env, path);
@@ -1,19 +1,16 @@
package chat.simplex.common
import androidx.compose.animation.core.Animatable
import androidx.compose.foundation.*
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.*
import androidx.compose.material.ripple.rememberRipple
import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.rememberSaveable
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.graphicsLayer
import androidx.compose.ui.unit.dp
import chat.simplex.common.views.usersettings.SetDeliveryReceiptsView
@@ -23,7 +20,8 @@ import chat.simplex.common.ui.theme.*
import chat.simplex.common.views.CreateFirstProfile
import chat.simplex.common.views.helpers.SimpleButton
import chat.simplex.common.views.SplashView
import chat.simplex.common.views.call.*
import chat.simplex.common.views.call.ActiveCallView
import chat.simplex.common.views.call.IncomingCallAlertView
import chat.simplex.common.views.chat.ChatView
import chat.simplex.common.views.chatlist.*
import chat.simplex.common.views.database.DatabaseErrorView
@@ -110,7 +108,6 @@ fun MainScreen() {
val localUserCreated = chatModel.localUserCreated.value
var showInitializationView by remember { mutableStateOf(false) }
when {
chatModel.dbMigrationInProgress.value -> DefaultProgressView(stringResource(MR.strings.database_migration_in_progress))
chatModel.chatDbStatus.value == null && showInitializationView -> DefaultProgressView(stringResource(MR.strings.opening_database))
showChatDatabaseError -> {
// Prevent showing keyboard on Android when: passcode enabled and database password not saved
@@ -171,17 +168,7 @@ fun MainScreen() {
}
} else {
if (chatModel.showCallView.value) {
if (appPlatform.isAndroid) {
LaunchedEffect(Unit) {
// This if prevents running the activity in the following condition:
// - the activity already started before and was destroyed by collapsing active call (start audio call, press back button, go to a launcher)
if (!chatModel.activeCallViewIsCollapsed.value) {
platform.androidStartCallActivity(false)
}
}
} else {
ActiveCallView()
}
ActiveCallView()
} else {
// It's needed for privacy settings toggle, so it can be shown even if the app is passcode unlocked
ModalManager.fullscreen.showPasscodeInView()
@@ -218,13 +205,9 @@ fun MainScreen() {
}
}
val ANDROID_CALL_TOP_PADDING = 40.dp
@Composable
fun AndroidScreen(settingsState: SettingsViewState) {
BoxWithConstraints {
val call = remember { chatModel.activeCall} .value
val showCallArea = call != null && call.callState != CallState.WaitCapabilities && call.callState != CallState.InvitationAccepted
var currentChatId by rememberSaveable { mutableStateOf(chatModel.chatId.value) }
val offset = remember { Animatable(if (chatModel.chatId.value == null) 0f else maxWidth.value) }
Box(
@@ -232,7 +215,6 @@ fun AndroidScreen(settingsState: SettingsViewState) {
.graphicsLayer {
translationX = -offset.value.dp.toPx()
}
.padding(top = if (showCallArea) ANDROID_CALL_TOP_PADDING else 0.dp)
) {
StartPartOfScreen(settingsState)
}
@@ -259,17 +241,11 @@ fun AndroidScreen(settingsState: SettingsViewState) {
}
}
}
Box(Modifier
.graphicsLayer { translationX = maxWidth.toPx() - offset.value.dp.toPx() }
.padding(top = if (showCallArea) ANDROID_CALL_TOP_PADDING else 0.dp)
) Box2@{
Box(Modifier.graphicsLayer { translationX = maxWidth.toPx() - offset.value.dp.toPx() }) Box2@{
currentChatId?.let {
ChatView(it, chatModel, onComposed)
}
}
if (call != null && showCallArea) {
ActiveCallInteractiveArea(call, remember { MutableStateFlow(AnimatedViewState.GONE) })
}
}
}
@@ -2,7 +2,6 @@ package chat.simplex.common.model
import androidx.compose.material.*
import androidx.compose.runtime.*
import androidx.compose.runtime.snapshots.SnapshotStateList
import androidx.compose.runtime.snapshots.SnapshotStateMap
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.SpanStyle
@@ -49,7 +48,6 @@ object ChatModel {
val chatDbEncrypted = mutableStateOf<Boolean?>(false)
val chatDbStatus = mutableStateOf<DBMigrationResult?>(null)
val ctrlInitInProgress = mutableStateOf(false)
val dbMigrationInProgress = mutableStateOf(false)
val chats = mutableStateListOf<Chat>()
// map of connections network statuses, key is agent connection id
val networkStatuses = mutableStateMapOf<String, NetworkStatus>()
@@ -57,7 +55,7 @@ object ChatModel {
// current chat
val chatId = mutableStateOf<String?>(null)
val chatItems = mutableStateOf(SnapshotStateList<ChatItem>())
val chatItems = mutableStateListOf<ChatItem>()
// rhId, chatId
val deletedChats = mutableStateOf<List<Pair<Long?, String>>>(emptyList())
val chatItemStatuses = mutableMapOf<Long, CIStatus>()
@@ -65,6 +63,8 @@ object ChatModel {
val terminalItems = mutableStateOf<List<TerminalItem>>(listOf())
val userAddress = mutableStateOf<UserContactLinkRec?>(null)
// Allows to temporary save servers that are being edited on multiple screens
val userSMPServersUnsaved = mutableStateOf<(List<ServerCfg>)?>(null)
val chatItemTTL = mutableStateOf<ChatItemTTL>(ChatItemTTL.None)
// set when app opened from external intent
@@ -96,7 +96,6 @@ object ChatModel {
val activeCallInvitation = mutableStateOf<RcvCallInvitation?>(null)
val activeCall = mutableStateOf<Call?>(null)
val activeCallViewIsVisible = mutableStateOf<Boolean>(false)
val activeCallViewIsCollapsed = mutableStateOf<Boolean>(false)
val callCommand = mutableStateListOf<WCallCommand>()
val showCallView = mutableStateOf(false)
val switchingCall = mutableStateOf(false)
@@ -270,15 +269,18 @@ object ChatModel {
} else {
addChat(Chat(remoteHostId = rhId, chatInfo = cInfo, chatItems = arrayListOf(cItem)))
}
Log.d(TAG, "TODOCHAT: addChatItem: adding to chat ${chatId.value} from ${cInfo.id} ${cItem.id}, size ${chatItems.size}")
withContext(Dispatchers.Main) {
// add to current chat
if (chatId.value == cInfo.id) {
Log.d(TAG, "TODOCHAT: addChatItem: chatIds are equal, size ${chatItems.size}")
// Prevent situation when chat item already in the list received from backend
if (chatItems.value.none { it.id == cItem.id }) {
if (chatItems.value.lastOrNull()?.id == ChatItem.TEMP_LIVE_CHAT_ITEM_ID) {
chatItems.add(kotlin.math.max(0, chatItems.value.lastIndex), cItem)
if (chatItems.none { it.id == cItem.id }) {
if (chatItems.lastOrNull()?.id == ChatItem.TEMP_LIVE_CHAT_ITEM_ID) {
chatItems.add(kotlin.math.max(0, chatItems.lastIndex), cItem)
} else {
chatItems.add(cItem)
Log.d(TAG, "TODOCHAT: addChatItem: added to chat ${chatId.value} from ${cInfo.id} ${cItem.id}, size ${chatItems.size}")
}
}
}
@@ -305,13 +307,14 @@ object ChatModel {
addChat(Chat(remoteHostId = rhId, chatInfo = cInfo, chatItems = arrayListOf(cItem)))
res = true
}
Log.d(TAG, "TODOCHAT: upsertChatItem: upserting to chat ${chatId.value} from ${cInfo.id} ${cItem.id}, size ${chatItems.size}")
return withContext(Dispatchers.Main) {
// update current chat
if (chatId.value == cInfo.id) {
val items = chatItems.value
val itemIndex = items.indexOfFirst { it.id == cItem.id }
val itemIndex = chatItems.indexOfFirst { it.id == cItem.id }
if (itemIndex >= 0) {
items[itemIndex] = cItem
chatItems[itemIndex] = cItem
Log.d(TAG, "TODOCHAT: upsertChatItem: updated in chat $chatId from ${cInfo.id} ${cItem.id}, size ${chatItems.size}")
false
} else {
val status = chatItemStatuses.remove(cItem.id)
@@ -321,6 +324,7 @@ object ChatModel {
cItem
}
chatItems.add(ci)
Log.d(TAG, "TODOCHAT: upsertChatItem: added to chat $chatId from ${cInfo.id} ${cItem.id}, size ${chatItems.size}")
true
}
} else {
@@ -332,10 +336,9 @@ object ChatModel {
suspend fun updateChatItem(cInfo: ChatInfo, cItem: ChatItem, status: CIStatus? = null) {
withContext(Dispatchers.Main) {
if (chatId.value == cInfo.id) {
val items = chatItems.value
val itemIndex = items.indexOfFirst { it.id == cItem.id }
val itemIndex = chatItems.indexOfFirst { it.id == cItem.id }
if (itemIndex >= 0) {
items[itemIndex] = cItem
chatItems[itemIndex] = cItem
}
} else if (status != null) {
chatItemStatuses[cItem.id] = status
@@ -359,10 +362,10 @@ object ChatModel {
}
// remove from current chat
if (chatId.value == cInfo.id) {
chatItems.removeAll {
val remove = it.id == cItem.id
if (remove) { AudioPlayer.stop(it) }
remove
val itemIndex = chatItems.indexOfFirst { it.id == cItem.id }
if (itemIndex >= 0) {
AudioPlayer.stop(chatItems[itemIndex])
chatItems.removeAt(itemIndex)
}
}
}
@@ -403,7 +406,7 @@ object ChatModel {
}
fun removeLiveDummy() {
if (chatItems.value.lastOrNull()?.id == ChatItem.TEMP_LIVE_CHAT_ITEM_ID) {
if (chatItems.lastOrNull()?.id == ChatItem.TEMP_LIVE_CHAT_ITEM_ID) {
chatItems.removeLast()
}
}
@@ -435,14 +438,14 @@ object ChatModel {
var markedRead = 0
if (chatId.value == cInfo.id) {
var i = 0
val items = chatItems.value
while (i < items.size) {
val item = items[i]
Log.d(TAG, "TODOCHAT: markItemsReadInCurrentChat: marking read ${cInfo.id}, current chatId ${chatId.value}, size was ${chatItems.size}")
while (i < chatItems.count()) {
val item = chatItems[i]
if (item.meta.itemStatus is CIStatus.RcvNew && (range == null || (range.from <= item.id && item.id <= range.to))) {
val newItem = item.withStatus(CIStatus.RcvRead())
items[i] = newItem
chatItems[i] = newItem
if (newItem.meta.itemLive != true && newItem.meta.itemTimed?.ttl != null) {
items[i] = newItem.copy(meta = newItem.meta.copy(itemTimed = newItem.meta.itemTimed.copy(
chatItems[i] = newItem.copy(meta = newItem.meta.copy(itemTimed = newItem.meta.itemTimed.copy(
deleteAt = Clock.System.now() + newItem.meta.itemTimed.ttl.toDuration(DurationUnit.SECONDS)))
)
}
@@ -450,6 +453,7 @@ object ChatModel {
}
i += 1
}
Log.d(TAG, "TODOCHAT: markItemsReadInCurrentChat: marked read ${cInfo.id}, current chatId ${chatId.value}, size now ${chatItems.size}")
}
return markedRead
}
@@ -640,8 +644,7 @@ object ChatModel {
}
fun addTerminalItem(item: TerminalItem) {
val maxItems = if (appPreferences.developerTools.get()) 500 else 200
if (terminalItems.value.size >= maxItems) {
if (terminalItems.value.size >= 500) {
terminalItems.value = terminalItems.value.subList(1, terminalItems.value.size)
}
terminalItems.value += item
@@ -966,16 +969,6 @@ sealed class ChatInfo: SomeChat, NamedChat {
is Group -> groupInfo.chatSettings
else -> null
}
val chatTs: Instant
get() = when(this) {
is Direct -> contact.chatTs ?: contact.updatedAt
is Group -> groupInfo.chatTs ?: groupInfo.updatedAt
is Local -> noteFolder.chatTs
is ContactRequest -> contactRequest.updatedAt
is ContactConnection -> contactConnection.updatedAt
is InvalidJSON -> updatedAt
}
}
@Serializable
@@ -1016,7 +1009,6 @@ data class Contact(
val mergedPreferences: ContactUserPreferences,
override val createdAt: Instant,
override val updatedAt: Instant,
val chatTs: Instant?,
val contactGroupMemberId: Long? = null,
val contactGrpInvSent: Boolean
): SomeChat, NamedChat {
@@ -1085,7 +1077,6 @@ data class Contact(
mergedPreferences = ContactUserPreferences.sampleData,
createdAt = Clock.System.now(),
updatedAt = Clock.System.now(),
chatTs = Clock.System.now(),
contactGrpInvSent = false
)
}
@@ -1213,8 +1204,7 @@ data class GroupInfo (
val hostConnCustomUserProfileId: Long? = null,
val chatSettings: ChatSettings,
override val createdAt: Instant,
override val updatedAt: Instant,
val chatTs: Instant?
override val updatedAt: Instant
): SomeChat, NamedChat {
override val chatType get() = ChatType.Group
override val id get() = "#$groupId"
@@ -1255,8 +1245,7 @@ data class GroupInfo (
hostConnCustomUserProfileId = null,
chatSettings = ChatSettings(enableNtfs = MsgFilter.All, sendRcpts = null, favorite = false),
createdAt = Clock.System.now(),
updatedAt = Clock.System.now(),
chatTs = Clock.System.now()
updatedAt = Clock.System.now()
)
}
}
@@ -1518,8 +1507,7 @@ class NoteFolder(
val favorite: Boolean,
val unread: Boolean,
override val createdAt: Instant,
override val updatedAt: Instant,
val chatTs: Instant
override val updatedAt: Instant
): SomeChat, NamedChat {
override val chatType get() = ChatType.Local
override val id get() = "*$noteFolderId"
@@ -1542,8 +1530,7 @@ class NoteFolder(
favorite = false,
unread = false,
createdAt = Clock.System.now(),
updatedAt = Clock.System.now(),
chatTs = Clock.System.now()
updatedAt = Clock.System.now()
)
}
}
@@ -1822,7 +1809,7 @@ data class ChatItem (
is CIContent.SndGroupInvitation -> false
is CIContent.RcvDirectEventContent -> when (content.rcvDirectEvent) {
is RcvDirectEvent.ContactDeleted -> false
is RcvDirectEvent.ProfileUpdated -> false
is RcvDirectEvent.ProfileUpdated -> true
}
is CIContent.RcvGroupEventContent -> when (content.rcvGroupEvent) {
is RcvGroupEvent.MemberAdded -> false
@@ -2003,46 +1990,6 @@ data class ChatItem (
}
}
fun MutableState<SnapshotStateList<ChatItem>>.add(index: Int, chatItem: ChatItem) {
value = SnapshotStateList<ChatItem>().apply { addAll(value); add(index, chatItem) }
}
fun MutableState<SnapshotStateList<ChatItem>>.add(chatItem: ChatItem) {
value = SnapshotStateList<ChatItem>().apply { addAll(value); add(chatItem) }
}
fun MutableState<SnapshotStateList<ChatItem>>.addAll(index: Int, chatItems: List<ChatItem>) {
value = SnapshotStateList<ChatItem>().apply { addAll(value); addAll(index, chatItems) }
}
fun MutableState<SnapshotStateList<ChatItem>>.addAll(chatItems: List<ChatItem>) {
value = SnapshotStateList<ChatItem>().apply { addAll(value); addAll(chatItems) }
}
fun MutableState<SnapshotStateList<ChatItem>>.removeAll(block: (ChatItem) -> Boolean) {
value = SnapshotStateList<ChatItem>().apply { addAll(value); removeAll(block) }
}
fun MutableState<SnapshotStateList<ChatItem>>.removeAt(index: Int) {
value = SnapshotStateList<ChatItem>().apply { addAll(value); removeAt(index) }
}
fun MutableState<SnapshotStateList<ChatItem>>.removeLast() {
value = SnapshotStateList<ChatItem>().apply { addAll(value); removeLast() }
}
fun MutableState<SnapshotStateList<ChatItem>>.replaceAll(chatItems: List<ChatItem>) {
value = SnapshotStateList<ChatItem>().apply { addAll(chatItems) }
}
fun MutableState<SnapshotStateList<ChatItem>>.clear() {
value = SnapshotStateList<ChatItem>()
}
fun State<SnapshotStateList<ChatItem>>.asReversed(): MutableList<ChatItem> = value.asReversed()
val State<List<ChatItem>>.size: Int get() = value.size
enum class CIMergeCategory {
MemberConnected,
RcvGroupEvent,
@@ -451,21 +451,7 @@ object ChatController {
}
try {
val msg = recvMsg(ctrl)
if (msg != null) {
val finishedWithoutTimeout = withTimeoutOrNull(60_000L) {
processReceivedMsg(msg)
}
if (finishedWithoutTimeout == null) {
Log.e(TAG, "Timeout reached while processing received message: " + msg.resp.responseType)
if (appPreferences.developerTools.get() && appPreferences.showSlowApiCalls.get()) {
AlertManager.shared.showAlertMsg(
title = generalGetString(MR.strings.possible_slow_function_title),
text = generalGetString(MR.strings.possible_slow_function_desc).format(60, msg.resp.responseType + "\n" + Exception().stackTraceToString()),
shareText = true
)
}
}
}
if (msg != null) processReceivedMsg(msg)
} catch (e: Exception) {
Log.e(TAG, "ChatController recvMsg/processReceivedMsg exception: " + e.stackTraceToString());
} catch (e: Throwable) {
@@ -631,6 +617,12 @@ object ChatController {
throw Error("failed to set remote hosts folder: ${r.responseType} ${r.details}")
}
suspend fun apiSetXFTPConfig(cfg: XFTPFileConfig?) {
val r = sendCmd(null, CC.ApiSetXFTPConfig(cfg))
if (r is CR.CmdOk) return
throw Error("apiSetXFTPConfig bad response: ${r.responseType} ${r.details}")
}
suspend fun apiSetEncryptLocalFiles(enable: Boolean) = sendCommandOkResp(null, CC.ApiSetEncryptLocalFiles(enable))
suspend fun apiExportArchive(config: ArchiveConfig) {
@@ -1693,7 +1685,7 @@ object ChatController {
chatModel.networkStatuses[s.agentConnId] = s.networkStatus
}
}
is CR.NewChatItem -> withBGApi {
is CR.NewChatItem -> {
val cInfo = r.chatItem.chatInfo
val cItem = r.chatItem.chatItem
if (active(r.user)) {
@@ -1708,7 +1700,7 @@ object ChatController {
((mc is MsgContent.MCImage && file.fileSize <= MAX_IMAGE_SIZE_AUTO_RCV)
|| (mc is MsgContent.MCVideo && file.fileSize <= MAX_VIDEO_SIZE_AUTO_RCV)
|| (mc is MsgContent.MCVoice && file.fileSize <= MAX_VOICE_SIZE_AUTO_RCV && file.fileStatus !is CIFileStatus.RcvAccepted))) {
receiveFile(rhId, r.user, file.fileId, auto = true)
withBGApi { receiveFile(rhId, r.user, file.fileId, auto = true) }
}
if (cItem.showNotification && (allowedToShowNotification() || chatModel.chatId.value != cInfo.id || chatModel.remoteHostId() != rhId)) {
ntfManager.notifyMessageReceived(r.user, cInfo, cItem)
@@ -1908,8 +1900,10 @@ object ChatController {
if (invitation != null) {
chatModel.callManager.reportCallRemoteEnded(invitation = invitation)
}
withCall(r, r.contact) { call ->
withBGApi { chatModel.callManager.endCall(call) }
withCall(r, r.contact) { _ ->
chatModel.callCommand.add(WCallCommand.End)
chatModel.activeCall.value = null
chatModel.showCallView.value = false
}
}
is CR.ContactSwitch ->
@@ -2165,6 +2159,10 @@ object ChatController {
}
}
fun getXFTPCfg(): XFTPFileConfig {
return XFTPFileConfig(minFileSize = 0)
}
fun getNetCfg(): NetCfg {
val useSocksProxy = appPrefs.networkUseSocksProxy.get()
val proxyHostPort = appPrefs.networkProxyHostPort.get()
@@ -2273,6 +2271,7 @@ sealed class CC {
class SetTempFolder(val tempFolder: String): CC()
class SetFilesFolder(val filesFolder: String): CC()
class SetRemoteHostsFolder(val remoteHostsFolder: String): CC()
class ApiSetXFTPConfig(val config: XFTPFileConfig?): CC()
class ApiSetEncryptLocalFiles(val enable: Boolean): CC()
class ApiExportArchive(val config: ArchiveConfig): CC()
class ApiImportArchive(val config: ArchiveConfig): CC()
@@ -2402,6 +2401,7 @@ sealed class CC {
is SetTempFolder -> "/_temp_folder $tempFolder"
is SetFilesFolder -> "/_files_folder $filesFolder"
is SetRemoteHostsFolder -> "/remote_hosts_folder $remoteHostsFolder"
is ApiSetXFTPConfig -> if (config != null) "/_xftp on ${json.encodeToString(config)}" else "/_xftp off"
is ApiSetEncryptLocalFiles -> "/_files_encrypt ${onOff(enable)}"
is ApiExportArchive -> "/_db export ${json.encodeToString(config)}"
is ApiImportArchive -> "/_db import ${json.encodeToString(config)}"
@@ -2536,6 +2536,7 @@ sealed class CC {
is SetTempFolder -> "setTempFolder"
is SetFilesFolder -> "setFilesFolder"
is SetRemoteHostsFolder -> "setRemoteHostsFolder"
is ApiSetXFTPConfig -> "apiSetXFTPConfig"
is ApiSetEncryptLocalFiles -> "apiSetEncryptLocalFiles"
is ApiExportArchive -> "apiExportArchive"
is ApiImportArchive -> "apiImportArchive"
@@ -2701,6 +2702,9 @@ sealed class ChatPagination {
@Serializable
class ComposedMessage(val fileSource: CryptoFile?, val quotedItemId: Long?, val msgContent: MsgContent)
@Serializable
class XFTPFileConfig(val minFileSize: Long)
@Serializable
class ArchiveConfig(val archivePath: String, val disableCompression: Boolean? = null, val parentTempDirectory: String? = null)
@@ -28,7 +28,6 @@ external fun chatParseMarkdown(str: String): String
external fun chatParseServer(str: String): String
external fun chatPasswordHash(pwd: String, salt: String): String
external fun chatValidName(name: String): String
external fun chatJsonLength(str: String): Int
external fun chatWriteFile(ctrl: ChatCtrl, path: String, buffer: ByteBuffer): String
external fun chatReadFile(path: String, key: String, nonce: String): Array<Any>
external fun chatEncryptFile(ctrl: ChatCtrl, fromPath: String, toPath: String): String
@@ -43,7 +42,7 @@ val appPreferences: AppPreferences
val chatController: ChatController = ChatController
fun initChatControllerAndRunMigrations() {
withLongRunningApi {
withLongRunningApi(slow = 30_000, deadlock = 60_000) {
if (appPreferences.chatStopped.get() && appPreferences.storeDBPassphrase.get() && ksDatabasePassword.get() != null) {
initChatController(startChat = ::showStartChatAfterRestartAlert)
} else {
@@ -59,23 +58,10 @@ suspend fun initChatController(useKey: String? = null, confirmMigrations: Migrat
chatModel.ctrlInitInProgress.value = true
val dbKey = useKey ?: DatabaseUtils.useDatabaseKey()
val confirm = confirmMigrations ?: if (appPreferences.developerTools.get() && appPreferences.confirmDBUpgrades.get()) MigrationConfirmation.Error else MigrationConfirmation.YesUp
var migrated: Array<Any> = chatMigrateInit(dbAbsolutePrefixPath, dbKey, MigrationConfirmation.Error.value)
var res: DBMigrationResult = runCatching {
val migrated: Array<Any> = chatMigrateInit(dbAbsolutePrefixPath, dbKey, confirm.value)
val res: DBMigrationResult = kotlin.runCatching {
json.decodeFromString<DBMigrationResult>(migrated[0] as String)
}.getOrElse { DBMigrationResult.Unknown(migrated[0] as String) }
val rerunMigration = res is DBMigrationResult.ErrorMigration && when (res.migrationError) {
// we don't allow to run down migrations without confirmation in UI, so currently it won't be YesUpDown
is MigrationError.Upgrade -> confirm == MigrationConfirmation.YesUp || confirm == MigrationConfirmation.YesUpDown
is MigrationError.Downgrade -> confirm == MigrationConfirmation.YesUpDown
is MigrationError.Error -> false
}
if (rerunMigration) {
chatModel.dbMigrationInProgress.value = true
migrated = chatMigrateInit(dbAbsolutePrefixPath, dbKey, confirm.value)
res = runCatching {
json.decodeFromString<DBMigrationResult>(migrated[0] as String)
}.getOrElse { DBMigrationResult.Unknown(migrated[0] as String) }
}
val ctrl = if (res is DBMigrationResult.OK) {
migrated[1] as Long
} else null
@@ -91,6 +77,7 @@ suspend fun initChatController(useKey: String? = null, confirmMigrations: Migrat
if (appPlatform.isDesktop) {
controller.apiSetRemoteHostsFolder(remoteHostsDir.absolutePath)
}
controller.apiSetXFTPConfig(controller.getXFTPCfg())
controller.apiSetEncryptLocalFiles(controller.appPrefs.privacyEncryptLocalFiles.get())
// If we migrated successfully means previous re-encryption process on database level finished successfully too
if (appPreferences.encryptionStartedAt.get() != null) appPreferences.encryptionStartedAt.set(null)
@@ -132,7 +119,6 @@ suspend fun initChatController(useKey: String? = null, confirmMigrations: Migrat
}
} finally {
chatModel.ctrlInitInProgress.value = false
chatModel.dbMigrationInProgress.value = false
}
}
@@ -55,7 +55,7 @@ abstract class NtfManager {
}
fun openChatAction(userId: Long?, chatId: ChatId) {
withLongRunningApi {
withLongRunningApi(slow = 30_000, deadlock = 60_000) {
awaitChatStartedIfNeeded(chatModel)
if (userId != null && userId != chatModel.currentUser.value?.userId && chatModel.currentUser.value != null) {
// TODO include remote host ID in desktop notifications?
@@ -70,7 +70,7 @@ abstract class NtfManager {
}
fun showChatsAction(userId: Long?) {
withLongRunningApi {
withLongRunningApi(slow = 30_000, deadlock = 60_000) {
awaitChatStartedIfNeeded(chatModel)
if (userId != null && userId != chatModel.currentUser.value?.userId && chatModel.currentUser.value != null) {
// TODO include remote host ID in desktop notifications?
@@ -1,21 +1,16 @@
package chat.simplex.common.platform
import chat.simplex.common.model.ChatId
import chat.simplex.common.model.NotificationsMode
interface PlatformInterface {
suspend fun androidServiceStart() {}
fun androidServiceSafeStop() {}
fun androidCallServiceSafeStop() {}
fun androidNotificationsModeChanged(mode: NotificationsMode) {}
fun androidChatStartedAfterBeingOff() {}
fun androidChatStopped() {}
fun androidChatInitializedAndStarted() {}
fun androidIsBackgroundCallAllowed(): Boolean = true
fun androidSetNightModeIfSupported() {}
fun androidStartCallActivity(acceptCall: Boolean, remoteHostId: Long? = null, chatId: ChatId? = null) {}
fun androidPictureInPictureAllowed(): Boolean = true
fun androidCallEnded() {}
suspend fun androidAskToAllowBackgroundCalls(): Boolean = true
}
/**
@@ -1,6 +1,6 @@
package chat.simplex.common.views.call
import chat.simplex.common.model.*
import chat.simplex.common.model.ChatModel
import chat.simplex.common.platform.*
import chat.simplex.common.views.helpers.withBGApi
import kotlinx.datetime.Clock
@@ -23,29 +23,27 @@ class CallManager(val chatModel: ChatModel) {
}
}
fun acceptIncomingCall(invitation: RcvCallInvitation) = withBGApi {
fun acceptIncomingCall(invitation: RcvCallInvitation) {
val call = chatModel.activeCall.value
val contactInfo = chatModel.controller.apiContactInfo(invitation.remoteHostId, invitation.contact.contactId)
val profile = contactInfo?.second ?: invitation.user.profile.toProfile()
// In case the same contact calling while previous call didn't end yet (abnormal ending of call from the other side)
if (call == null || (call.remoteHostId == invitation.remoteHostId && call.contact.id == invitation.contact.id)) {
justAcceptIncomingCall(invitation = invitation, profile)
if (call == null) {
justAcceptIncomingCall(invitation = invitation)
} else {
chatModel.switchingCall.value = true
try {
endCall(call = call)
justAcceptIncomingCall(invitation = invitation, profile)
} finally {
chatModel.switchingCall.value = false
withBGApi {
chatModel.switchingCall.value = true
try {
endCall(call = call)
justAcceptIncomingCall(invitation = invitation)
} finally {
chatModel.switchingCall.value = false
}
}
}
}
private fun justAcceptIncomingCall(invitation: RcvCallInvitation, userProfile: Profile) {
private fun justAcceptIncomingCall(invitation: RcvCallInvitation) {
with (chatModel) {
activeCall.value = Call(
remoteHostId = invitation.remoteHostId,
userProfile = userProfile,
contact = invitation.contact,
callState = CallState.InvitationAccepted,
localMedia = invitation.callType.media,
@@ -70,23 +68,17 @@ class CallManager(val chatModel: ChatModel) {
}
suspend fun endCall(call: Call) {
with(chatModel) {
// If there is active call currently and it's with other contact, don't interrupt it
if (activeCall.value != null && !(activeCall.value?.remoteHostId == call.remoteHostId && activeCall.value?.contact?.id == call.contact.id)) return
// Don't destroy WebView if you plan to accept next call right after this one
if (!switchingCall.value) {
showCallView.value = false
activeCall.value = null
activeCallViewIsCollapsed.value = false
platform.androidCallEnded()
}
with (chatModel) {
if (call.callState == CallState.Ended) {
Log.d(TAG, "CallManager.endCall: call ended")
activeCall.value = null
showCallView.value = false
} else {
Log.d(TAG, "CallManager.endCall: ending call...")
//callCommand.add(WCallCommand.End)
callCommand.add(WCallCommand.End)
showCallView.value = false
controller.apiEndCall(call.remoteHostId, call.contact)
activeCall.value = null
}
}
}
@@ -7,11 +7,11 @@ import kotlinx.datetime.Instant
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import java.net.URI
import java.util.*
import kotlin.collections.ArrayList
data class Call(
val remoteHostId: Long?,
val userProfile: Profile,
val contact: Contact,
val callState: CallState,
val localMedia: CallMediaType,
@@ -23,7 +23,7 @@ data class Call(
val soundSpeaker: Boolean = localMedia == CallMediaType.Video,
var localCamera: VideoCamera = VideoCamera.User,
val connectionInfo: ConnectionInfo? = null,
var connectedAt: Instant? = null,
var connectedAt: Instant? = null
) {
val encrypted: Boolean get() = localEncrypted && sharedKey != null
val localEncrypted: Boolean get() = localCapabilities?.encryption ?: false
@@ -36,9 +36,6 @@ data class Call(
}
val hasMedia: Boolean get() = callState == CallState.OfferSent || callState == CallState.Negotiated || callState == CallState.Connected
fun supportsVideo(): Boolean = peerMedia == CallMediaType.Video || localMedia == CallMediaType.Video
}
enum class CallState {
@@ -78,7 +75,6 @@ sealed class WCallCommand {
@Serializable @SerialName("media") data class Media(val media: CallMediaType, val enable: Boolean): WCallCommand()
@Serializable @SerialName("camera") data class Camera(val camera: VideoCamera): WCallCommand()
@Serializable @SerialName("description") data class Description(val state: String, val description: String): WCallCommand()
@Serializable @SerialName("layout") data class Layout(val layout: LayoutType): WCallCommand()
@Serializable @SerialName("end") object End: WCallCommand()
}
@@ -171,13 +167,6 @@ enum class VideoCamera {
val flipped: VideoCamera get() = if (this == User) Environment else User
}
@Serializable
enum class LayoutType {
@SerialName("default") Default,
@SerialName("localVideo") LocalVideo,
@SerialName("remoteVideo") RemoteVideo
}
@Serializable
data class ConnectionState(
val connectionState: String,
@@ -324,7 +324,7 @@ fun ChatItemInfoView(chatModel: ChatModel, ci: ChatItem, ciInfo: ChatItemInfo, d
.fillMaxHeight(),
verticalArrangement = Arrangement.SpaceBetween
) {
LaunchedEffect(ciInfo) {
LaunchedEffect(Unit) {
if (ciInfo.memberDeliveryStatuses != null) {
selection.value = CIInfoTab.Delivery(ciInfo.memberDeliveryStatuses)
}
@@ -67,13 +67,13 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: suspend (chatId:
launch {
snapshotFlow { chatModel.chatId.value }
.distinctUntilChanged()
.filterNotNull()
.onEach { Log.d(TAG, "TODOCHAT: chatId: activeChatId ${activeChat.value?.id} == new chatId $it ${activeChat.value?.id == it} ") }
.filter { it != null && activeChat.value?.id != it }
.collect { chatId ->
if (activeChat.value?.id != chatId) {
// 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(chatId)
}
// 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(chatId!!)
Log.d(TAG, "TODOCHAT: chatId: activeChatId became ${activeChat.value?.id}")
markUnreadChatAsRead(activeChat, chatModel)
}
}
@@ -92,10 +92,12 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: suspend (chatId:
}
}
.distinctUntilChanged()
.onEach { Log.d(TAG, "TODOCHAT: chats: activeChatId ${activeChat.value?.id} == new chatId ${it?.id} ${activeChat.value?.id == it?.id} ") }
// Only changed chatInfo is important thing. Other properties can be skipped for reducing recompositions
.filter { it != null && it.chatInfo != activeChat.value?.chatInfo }
.filter { it != null && it?.chatInfo != activeChat.value?.chatInfo }
.collect {
activeChat.value = it
Log.d(TAG, "TODOCHAT: chats: activeChatId became ${activeChat.value?.id}")
}
}
}
@@ -146,6 +148,7 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: suspend (chatId:
},
attachmentOption,
attachmentBottomSheetState,
chatModel.chatItems,
searchText,
useLinkPreviews = useLinkPreviews,
linkMode = chatModel.simplexLinkMode.value,
@@ -223,17 +226,19 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: suspend (chatId:
loadPrevMessages = {
if (chatModel.chatId.value != activeChat.value?.id) return@ChatLayout
val c = chatModel.getChat(chatModel.chatId.value ?: return@ChatLayout)
val firstId = chatModel.chatItems.value.firstOrNull()?.id
val firstId = chatModel.chatItems.firstOrNull()?.id
if (c != null && firstId != null) {
withBGApi {
Log.d(TAG, "TODOCHAT: loadPrevMessages: loading for ${c.id}, current chatId ${ChatModel.chatId.value}, size was ${ChatModel.chatItems.size}")
apiLoadPrevMessages(c, chatModel, firstId, searchText.value)
Log.d(TAG, "TODOCHAT: loadPrevMessages: loaded for ${c.id}, current chatId ${ChatModel.chatId.value}, size now ${ChatModel.chatItems.size}")
}
}
},
deleteMessage = { itemId, mode ->
withBGApi {
val cInfo = chat.chatInfo
val toDeleteItem = chatModel.chatItems.value.firstOrNull { it.id == itemId }
val toDeleteItem = chatModel.chatItems.firstOrNull { it.id == itemId }
val toModerate = toDeleteItem?.memberToModerate(chat.chatInfo)
val groupInfo = toModerate?.first
val groupMember = toModerate?.second
@@ -301,9 +306,7 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: suspend (chatId:
withBGApi {
val cInfo = chat.chatInfo
if (cInfo is ChatInfo.Direct) {
val contactInfo = chatModel.controller.apiContactInfo(chat.remoteHostId, cInfo.contact.contactId)
val profile = contactInfo?.second ?: chatModel.currentUser.value?.profile?.toProfile() ?: return@withBGApi
chatModel.activeCall.value = Call(remoteHostId = chatRh, contact = cInfo.contact, callState = CallState.WaitCapabilities, localMedia = media, userProfile = profile)
chatModel.activeCall.value = Call(remoteHostId = chatRh, contact = cInfo.contact, callState = CallState.WaitCapabilities, localMedia = media)
chatModel.showCallView.value = true
chatModel.callCommand.add(WCallCommand.Capabilities(media))
}
@@ -401,15 +404,12 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: suspend (chatId:
setGroupMembers(chatRh, chat.chatInfo.groupInfo, chatModel)
}
ModalManager.end.closeModals()
ModalManager.end.showModalCloseable(endButtons = {
ModalManager.end.showModal(endButtons = {
ShareButton {
clipboard.shareText(itemInfoShareText(chatModel, cItem, ciInfo, chatModel.controller.appPrefs.developerTools.get()))
}
}) { close ->
}) {
ChatItemInfoView(chatModel, cItem, ciInfo, devTools = chatModel.controller.appPrefs.developerTools.get())
KeyChangeEffect(chatModel.chatId.value) {
close()
}
}
}
}
@@ -495,6 +495,7 @@ fun ChatLayout(
composeView: (@Composable () -> Unit),
attachmentOption: MutableState<AttachmentOption?>,
attachmentBottomSheetState: ModalBottomSheetState,
chatItems: List<ChatItem>,
searchValue: State<String>,
useLinkPreviews: Boolean,
linkMode: SimplexLinkMode,
@@ -581,7 +582,7 @@ fun ChatLayout(
.padding(contentPadding)
) {
ChatItemsList(
chat, unreadCount, composeState, searchValue,
chat, unreadCount, composeState, chatItems, searchValue,
useLinkPreviews, linkMode, showMemberInfo, loadPrevMessages, deleteMessage, deleteMessages,
receiveFile, cancelFile, joinGroup, acceptCall, acceptFeature, openDirectChat,
updateContactStats, updateMemberStats, syncContactConnection, syncMemberConnection, findModelChat, findModelMember,
@@ -646,7 +647,7 @@ fun ChatInfoToolbar(
}
}
if (chat.chatInfo is ChatInfo.Direct && chat.chatInfo.contact.mergedPreferences.calls.enabled.forUser) {
if (chat.chatInfo is ChatInfo.Direct && chat.chatInfo.contact.allowsFeature(ChatFeature.Calls)) {
if (activeCall == null) {
barButtons.add {
if (appPlatform.isAndroid) {
@@ -675,7 +676,7 @@ fun ChatInfoToolbar(
}
}
}
} else if (activeCall?.contact?.id == chat.id && appPlatform.isDesktop) {
} else if (activeCall?.contact?.id == chat.id) {
barButtons.add {
val call = remember { chatModel.activeCall }.value
val connectedAt = call?.connectedAt
@@ -839,6 +840,7 @@ fun BoxWithConstraintsScope.ChatItemsList(
chat: Chat,
unreadCount: State<Int>,
composeState: MutableState<ComposeState>,
chatItems: List<ChatItem>,
searchValue: State<String>,
useLinkPreviews: Boolean,
linkMode: SimplexLinkMode,
@@ -867,7 +869,7 @@ fun BoxWithConstraintsScope.ChatItemsList(
) {
val listState = rememberLazyListState()
val scope = rememberCoroutineScope()
ScrollToBottom(chat.id, listState, chatModel.chatItems)
ScrollToBottom(chat.id, listState, chatItems)
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()) {
@@ -884,7 +886,7 @@ fun BoxWithConstraintsScope.ChatItemsList(
PreloadItems(listState, ChatPagination.UNTIL_PRELOAD_COUNT, loadPrevMessages)
Spacer(Modifier.size(8.dp))
val reversedChatItems by remember { derivedStateOf { chatModel.chatItems.asReversed() } }
val reversedChatItems by remember { derivedStateOf { chatItems.reversed().toList() } }
val maxHeightRounded = with(LocalDensity.current) { maxHeight.roundToPx() }
val scrollToItem: (Long) -> Unit = { itemId: Long ->
val index = reversedChatItems.indexOfFirst { it.id == itemId }
@@ -937,7 +939,7 @@ fun BoxWithConstraintsScope.ChatItemsList(
}
}
val provider = {
providerForGallery(i, chatModel.chatItems.value, cItem.id) { indexInReversed ->
providerForGallery(i, chatItems, cItem.id) { indexInReversed ->
scope.launch {
listState.scrollToItem(
kotlin.math.min(reversedChatItems.lastIndex, indexInReversed + 1),
@@ -1060,11 +1062,11 @@ fun BoxWithConstraintsScope.ChatItemsList(
}
}
}
FloatingButtons(chatModel.chatItems, unreadCount, chat.chatStats.minUnreadItemId, searchValue, markRead, setFloatingButton, listState)
FloatingButtons(chatItems, unreadCount, chat.chatStats.minUnreadItemId, searchValue, markRead, setFloatingButton, listState)
}
@Composable
private fun ScrollToBottom(chatId: ChatId, listState: LazyListState, chatItems: State<List<ChatItem>>) {
private fun ScrollToBottom(chatId: ChatId, listState: LazyListState, chatItems: List<ChatItem>) {
val scope = rememberCoroutineScope()
// Helps to scroll to bottom after moving from Group to Direct chat
// and prevents scrolling to bottom on orientation change
@@ -1082,7 +1084,7 @@ private fun ScrollToBottom(chatId: ChatId, listState: LazyListState, chatItems:
* When the first visible item (from bottom) is visible (even partially) we can autoscroll to 0 item. Or just scrollBy small distance otherwise
* */
LaunchedEffect(Unit) {
snapshotFlow { chatItems.value.lastOrNull()?.id }
snapshotFlow { chatItems.lastOrNull()?.id }
.distinctUntilChanged()
.filter { listState.layoutInfo.visibleItemsInfo.firstOrNull()?.key != it }
.collect {
@@ -1105,7 +1107,7 @@ private fun ScrollToBottom(chatId: ChatId, listState: LazyListState, chatItems:
@Composable
fun BoxWithConstraintsScope.FloatingButtons(
chatItems: State<List<ChatItem>>,
chatItems: List<ChatItem>,
unreadCount: State<Int>,
minUnreadItemId: Long,
searchValue: State<String>,
@@ -1139,11 +1141,10 @@ fun BoxWithConstraintsScope.FloatingButtons(
val bottomUnreadCount by remember {
derivedStateOf {
if (unreadCount.value == 0) return@derivedStateOf 0
val items = chatItems.value
val from = items.lastIndex - firstVisibleIndex - lastIndexOfVisibleItems
if (items.size <= from || from < 0) return@derivedStateOf 0
val from = chatItems.lastIndex - firstVisibleIndex - lastIndexOfVisibleItems
if (chatItems.size <= from || from < 0) return@derivedStateOf 0
items.subList(from, items.size).count { it.isRcvNew }
chatItems.subList(from, chatItems.size).count { it.isRcvNew }
}
}
val firstVisibleOffset = (-with(LocalDensity.current) { maxHeight.roundToPx() } * 0.8).toInt()
@@ -1189,7 +1190,7 @@ fun BoxWithConstraintsScope.FloatingButtons(
painterResource(MR.images.ic_check),
onClick = {
markRead(
CC.ItemRange(minUnreadItemId, chatItems.value[chatItems.size - listState.layoutInfo.visibleItemsInfo.lastIndex - 1].id - 1),
CC.ItemRange(minUnreadItemId, chatItems[chatItems.size - listState.layoutInfo.visibleItemsInfo.lastIndex - 1].id - 1),
bottomUnreadCount
)
showDropDown.value = false
@@ -1494,6 +1495,7 @@ fun PreviewChatLayout() {
composeView = {},
attachmentOption = remember { mutableStateOf<AttachmentOption?>(null) },
attachmentBottomSheetState = rememberModalBottomSheetState(initialValue = ModalBottomSheetValue.Hidden),
chatItems = chatItems,
searchValue,
useLinkPreviews = true,
linkMode = SimplexLinkMode.DESCRIPTION,
@@ -1566,6 +1568,7 @@ fun PreviewGroupChatLayout() {
composeView = {},
attachmentOption = remember { mutableStateOf<AttachmentOption?>(null) },
attachmentBottomSheetState = rememberModalBottomSheetState(initialValue = ModalBottomSheetValue.Hidden),
chatItems = chatItems,
searchValue,
useLinkPreviews = true,
linkMode = SimplexLinkMode.DESCRIPTION,
@@ -267,7 +267,7 @@ fun ComposeView(
fun loadLinkPreview(url: String, wait: Long? = null) {
if (pendingLinkUrl.value == url) {
composeState.value = composeState.value.copy(preview = ComposePreview.CLinkPreview(null))
withLongRunningApi(slow = 60_000) {
withLongRunningApi(slow = 30_000, deadlock = 60_000) {
if (wait != null) delay(wait)
val lp = getLinkPreview(url)
if (lp != null && pendingLinkUrl.value == url) {
@@ -551,7 +551,7 @@ fun ComposeView(
}
fun sendMessage(ttl: Int?) {
withLongRunningApi(slow = 120_000) {
withLongRunningApi(slow = 30_000, deadlock = 60_000) {
sendMessageAsync(null, false, ttl)
}
}
@@ -583,10 +583,6 @@ fun ComposeView(
}
fun cancelLinkPreview() {
val pendingLink = pendingLinkUrl.value
if (pendingLink != null) {
cancelledLinks.add(pendingLink)
}
val uri = composeState.value.linkPreview?.uri
if (uri != null) {
cancelledLinks.add(uri)
@@ -665,7 +661,7 @@ fun ComposeView(
fun editPrevMessage() {
if (composeState.value.contextItem != ComposeContextItem.NoContextItem || composeState.value.preview != ComposePreview.NoPreview) return
val lastEditable = chatModel.chatItems.value.findLast { it.meta.editable }
val lastEditable = chatModel.chatItems.findLast { it.meta.editable }
if (lastEditable != null) {
composeState.value = ComposeState(editingItem = lastEditable, useLinkPreviews = useLinkPreviews)
}
@@ -59,6 +59,14 @@ fun SendMsgView(
) {
val showCustomDisappearingMessageDialog = remember { mutableStateOf(false) }
if (showCustomDisappearingMessageDialog.value) {
CustomDisappearingMessageDialog(
sendMessage = sendMessage,
setShowDialog = { showCustomDisappearingMessageDialog.value = it },
customDisappearingMessageTimePref = customDisappearingMessageTimePref
)
}
Box(Modifier.padding(vertical = 8.dp)) {
val cs = composeState.value
var progressByTimeout by rememberSaveable { mutableStateOf(false) }
@@ -195,11 +203,6 @@ fun SendMsgView(
DefaultDropdownMenu(showDropdown) {
menuItems.forEach { composable -> composable() }
}
CustomDisappearingMessageDialog(
showCustomDisappearingMessageDialog,
sendMessage = sendMessage,
customDisappearingMessageTimePref = customDisappearingMessageTimePref
)
} else {
SendMsgButton(icon, sendButtonSize, sendButtonAlpha, sendButtonColor, !sendMsgButtonDisabled, sendMessage)
}
@@ -217,43 +220,93 @@ expect fun VoiceButtonWithoutPermissionByPlatform()
@Composable
private fun CustomDisappearingMessageDialog(
showMenu: MutableState<Boolean>,
sendMessage: (Int?) -> Unit,
setShowDialog: (Boolean) -> Unit,
customDisappearingMessageTimePref: SharedPreference<Int>?
) {
DefaultDropdownMenu(showMenu) {
Text(
generalGetString(MR.strings.send_disappearing_message),
Modifier.padding(vertical = DEFAULT_PADDING_HALF, horizontal = DEFAULT_PADDING * 1.5f),
fontSize = 16.sp,
color = MaterialTheme.colors.secondary
)
val showCustomTimePicker = remember { mutableStateOf(false) }
ItemAction(generalGetString(MR.strings.send_disappearing_message_30_seconds)) {
sendMessage(30)
showMenu.value = false
if (showCustomTimePicker.value) {
val selectedDisappearingMessageTime = remember {
mutableStateOf(customDisappearingMessageTimePref?.get?.invoke() ?: 300)
}
ItemAction(generalGetString(MR.strings.send_disappearing_message_1_minute)) {
sendMessage(60)
showMenu.value = false
CustomTimePickerDialog(
selectedDisappearingMessageTime,
title = generalGetString(MR.strings.delete_after),
confirmButtonText = generalGetString(MR.strings.send_disappearing_message_send),
confirmButtonAction = { ttl ->
sendMessage(ttl)
customDisappearingMessageTimePref?.set?.invoke(ttl)
setShowDialog(false)
},
cancel = { setShowDialog(false) }
)
} else {
@Composable
fun ChoiceButton(
text: String,
onClick: () -> Unit
) {
TextButton(onClick) {
Text(
text,
fontSize = 18.sp,
color = MaterialTheme.colors.primary
)
}
}
ItemAction(generalGetString(MR.strings.send_disappearing_message_5_minutes)) {
sendMessage(300)
showMenu.value = false
}
ItemAction(generalGetString(MR.strings.send_disappearing_message_custom_time)) {
showMenu.value = false
val selectedDisappearingMessageTime = mutableStateOf(customDisappearingMessageTimePref?.get?.invoke() ?: 300)
showCustomTimePickerDialog(
selectedDisappearingMessageTime,
title = generalGetString(MR.strings.delete_after),
confirmButtonText = generalGetString(MR.strings.send_disappearing_message_send),
confirmButtonAction = { ttl ->
sendMessage(ttl)
customDisappearingMessageTimePref?.set?.invoke(ttl)
},
cancel = { showMenu.value = false }
)
DefaultDialog(onDismissRequest = { setShowDialog(false) }) {
Surface(
shape = RoundedCornerShape(corner = CornerSize(25.dp)),
contentColor = LocalContentColor.current
) {
Box(
contentAlignment = Alignment.Center
) {
Column(
modifier = Modifier.padding(DEFAULT_PADDING),
verticalArrangement = Arrangement.spacedBy(6.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Text(" ") // centers title
Text(
generalGetString(MR.strings.send_disappearing_message),
fontSize = 16.sp,
color = MaterialTheme.colors.secondary
)
Icon(
painterResource(MR.images.ic_close),
generalGetString(MR.strings.icon_descr_close_button),
tint = MaterialTheme.colors.secondary,
modifier = Modifier
.size(25.dp)
.clickable { setShowDialog(false) }
)
}
ChoiceButton(generalGetString(MR.strings.send_disappearing_message_30_seconds)) {
sendMessage(30)
setShowDialog(false)
}
ChoiceButton(generalGetString(MR.strings.send_disappearing_message_1_minute)) {
sendMessage(60)
setShowDialog(false)
}
ChoiceButton(generalGetString(MR.strings.send_disappearing_message_5_minutes)) {
sendMessage(300)
setShowDialog(false)
}
ChoiceButton(generalGetString(MR.strings.send_disappearing_message_custom_time)) {
showCustomTimePicker.value = true
}
}
}
}
}
}
}
@@ -54,7 +54,7 @@ fun AddGroupMembersView(rhId: Long?, groupInfo: GroupInfo, creatingGroup: Boolea
},
inviteMembers = {
allowModifyMembers = false
withLongRunningApi(slow = 120_000) {
withLongRunningApi(slow = 30_000, deadlock = 60_000) {
for (contactId in selectedContacts) {
val member = chatModel.controller.apiAddMember(rhId, groupInfo.groupId, contactId, selectedRole.value)
if (member != null) {
@@ -86,7 +86,7 @@ fun getContactsToAdd(chatModel: ChatModel, search: String): List<Contact> {
.map { it.chatInfo }
.filterIsInstance<ChatInfo.Direct>()
.map { it.contact }
.filter { c -> c.ready && c.active && c.contactId !in memberContactIds && c.chatViewName.lowercase().contains(s) }
.filter { it.contactId !in memberContactIds && it.chatViewName.lowercase().contains(s) }
.sortedBy { it.displayName.lowercase() }
.toList()
}
@@ -152,7 +152,7 @@ fun leaveGroupDialog(rhId: Long?, groupInfo: GroupInfo, chatModel: ChatModel, cl
text = generalGetString(MR.strings.you_will_stop_receiving_messages_from_this_group_chat_history_will_be_preserved),
confirmText = generalGetString(MR.strings.leave_group_button),
onConfirm = {
withLongRunningApi(60_000) {
withBGApi {
chatModel.controller.leaveGroup(rhId, groupInfo.groupId)
close?.invoke()
}
@@ -424,47 +424,69 @@ private fun MemberVerifiedShield() {
@Composable
private fun DropDownMenuForMember(rhId: Long?, member: GroupMember, groupInfo: GroupInfo, showMenu: MutableState<Boolean>) {
if (groupInfo.membership.memberRole >= GroupMemberRole.Admin) {
val canBlockForAll = member.canBlockForAll(groupInfo)
val canRemove = member.canBeRemoved(groupInfo)
if (canBlockForAll || canRemove) {
DefaultDropdownMenu(showMenu) {
if (canBlockForAll) {
if (member.blockedByAdmin) {
ItemAction(stringResource(MR.strings.unblock_for_all), painterResource(MR.images.ic_do_not_touch), onClick = {
unblockForAllAlert(rhId, groupInfo, member)
showMenu.value = false
})
} else {
ItemAction(stringResource(MR.strings.block_for_all), painterResource(MR.images.ic_back_hand), color = MaterialTheme.colors.error, onClick = {
blockForAllAlert(rhId, groupInfo, member)
showMenu.value = false
})
}
}
if (canRemove) {
ItemAction(stringResource(MR.strings.remove_member_button), painterResource(MR.images.ic_delete), color = MaterialTheme.colors.error, onClick = {
removeMemberAlert(rhId, groupInfo, member)
showMenu.value = false
})
}
}
// revert from this:
DefaultDropdownMenu(showMenu) {
if (member.canBeRemoved(groupInfo)) {
ItemAction(stringResource(MR.strings.remove_member_button), painterResource(MR.images.ic_delete), color = MaterialTheme.colors.error, onClick = {
removeMemberAlert(rhId, groupInfo, member)
showMenu.value = false
})
}
} else if (!member.blockedByAdmin) {
DefaultDropdownMenu(showMenu) {
if (member.memberSettings.showMessages) {
ItemAction(stringResource(MR.strings.block_member_button), painterResource(MR.images.ic_back_hand), color = MaterialTheme.colors.error, onClick = {
blockMemberAlert(rhId, groupInfo, member)
showMenu.value = false
})
} else {
ItemAction(stringResource(MR.strings.unblock_member_button), painterResource(MR.images.ic_do_not_touch), onClick = {
unblockMemberAlert(rhId, groupInfo, member)
showMenu.value = false
})
}
if (member.memberSettings.showMessages) {
ItemAction(stringResource(MR.strings.block_member_button), painterResource(MR.images.ic_back_hand), color = MaterialTheme.colors.error, onClick = {
blockMemberAlert(rhId, groupInfo, member)
showMenu.value = false
})
} else {
ItemAction(stringResource(MR.strings.unblock_member_button), painterResource(MR.images.ic_do_not_touch), onClick = {
unblockMemberAlert(rhId, groupInfo, member)
showMenu.value = false
})
}
}
// revert to this: vvv
// if (groupInfo.membership.memberRole >= GroupMemberRole.Admin) {
// val canBlockForAll = member.canBlockForAll(groupInfo)
// val canRemove = member.canBeRemoved(groupInfo)
// if (canBlockForAll || canRemove) {
// DefaultDropdownMenu(showMenu) {
// if (canBlockForAll) {
// if (member.blockedByAdmin) {
// ItemAction(stringResource(MR.strings.unblock_for_all), painterResource(MR.images.ic_do_not_touch), onClick = {
// unblockForAllAlert(rhId, groupInfo, member)
// showMenu.value = false
// })
// } else {
// ItemAction(stringResource(MR.strings.block_for_all), painterResource(MR.images.ic_back_hand), color = MaterialTheme.colors.error, onClick = {
// blockForAllAlert(rhId, groupInfo, member)
// showMenu.value = false
// })
// }
// }
// if (canRemove) {
// ItemAction(stringResource(MR.strings.remove_member_button), painterResource(MR.images.ic_delete), color = MaterialTheme.colors.error, onClick = {
// removeMemberAlert(rhId, groupInfo, member)
// showMenu.value = false
// })
// }
// }
// }
// } else if (!member.blockedByAdmin) {
// DefaultDropdownMenu(showMenu) {
// if (member.memberSettings.showMessages) {
// ItemAction(stringResource(MR.strings.block_member_button), painterResource(MR.images.ic_back_hand), color = MaterialTheme.colors.error, onClick = {
// blockMemberAlert(rhId, groupInfo, member)
// showMenu.value = false
// })
// } else {
// ItemAction(stringResource(MR.strings.unblock_member_button), painterResource(MR.images.ic_do_not_touch), onClick = {
// unblockMemberAlert(rhId, groupInfo, member)
// showMenu.value = false
// })
// }
// }
// }
// ^^^
}
@Composable
@@ -3,9 +3,11 @@ package chat.simplex.common.views.chat.group
import InfoRow
import SectionBottomSpacer
import SectionDividerSpaced
import SectionItemView
import SectionSpacer
import SectionTextFooter
import SectionView
import TextIconSpaced
import androidx.compose.desktop.ui.tooling.preview.Preview
import java.net.URI
import androidx.compose.foundation.*
@@ -72,8 +74,9 @@ fun GroupMemberInfoView(
if (chatModel.getContactChat(it) == null) {
chatModel.addChat(c)
}
chatModel.chatItems.clear()
chatModel.chatItemStatuses.clear()
chatModel.chatItems.replaceAll(c.chatItems)
chatModel.chatItems.addAll(c.chatItems)
chatModel.chatId.value = c.id
closeAll()
}
@@ -387,11 +390,25 @@ fun GroupMemberInfoLayout(
}
}
if (groupInfo.membership.memberRole >= GroupMemberRole.Admin) {
AdminDestructiveSection()
} else {
NonAdminBlockSection()
// revert from this:
SectionDividerSpaced(maxBottomPadding = false)
SectionView {
if (member.memberSettings.showMessages) {
BlockMemberButton(blockMember)
} else {
UnblockMemberButton(unblockMember)
}
if (member.canBeRemoved(groupInfo)) {
RemoveMemberButton(removeMember)
}
}
// revert to this: vvv
// if (groupInfo.membership.memberRole >= GroupMemberRole.Admin) {
// AdminDestructiveSection()
// } else {
// NonAdminBlockSection()
// }
// ^^^
if (developerTools) {
SectionDividerSpaced()
@@ -3,7 +3,6 @@ package chat.simplex.common.views.chat.group
import SectionBottomSpacer
import SectionDividerSpaced
import SectionItemView
import SectionTextFooter
import SectionView
import TextIconSpaced
import androidx.compose.foundation.layout.*
@@ -15,7 +14,6 @@ import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalClipboardManager
import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.text.AnnotatedString
@@ -29,13 +27,9 @@ import chat.simplex.common.views.chat.item.MarkdownText
import chat.simplex.common.views.helpers.*
import chat.simplex.common.model.ChatModel
import chat.simplex.common.model.GroupInfo
import chat.simplex.common.platform.chatJsonLength
import chat.simplex.common.ui.theme.DEFAULT_PADDING_HALF
import chat.simplex.res.MR
import kotlinx.coroutines.delay
private const val maxByteCount = 1200
@Composable
fun GroupWelcomeView(m: ChatModel, rhId: Long?, groupInfo: GroupInfo, close: () -> Unit) {
var gInfo by remember { mutableStateOf(groupInfo) }
@@ -60,11 +54,8 @@ fun GroupWelcomeView(m: ChatModel, rhId: Long?, groupInfo: GroupInfo, close: ()
ModalView(
close = {
when {
welcomeTextUnchanged(welcomeText, gInfo) -> close()
!welcomeTextFitsLimit(welcomeText) -> showUnsavedChangesTooLongAlert(close)
else -> showUnsavedChangesAlert({ save(close) }, close)
}
if (welcomeText.value == gInfo.groupProfile.description || (welcomeText.value == "" && gInfo.groupProfile.description == null)) close()
else showUnsavedChangesAlert({ save(close) }, close)
},
) {
GroupWelcomeLayout(
@@ -76,14 +67,6 @@ fun GroupWelcomeView(m: ChatModel, rhId: Long?, groupInfo: GroupInfo, close: ()
}
}
private fun welcomeTextUnchanged(welcomeText: MutableState<String>, groupInfo: GroupInfo): Boolean {
return welcomeText.value == groupInfo.groupProfile.description || (welcomeText.value == "" && groupInfo.groupProfile.description == null)
}
private fun welcomeTextFitsLimit(welcomeText: MutableState<String>): Boolean {
return chatJsonLength(welcomeText.value) <= maxByteCount
}
@Composable
private fun GroupWelcomeLayout(
welcomeText: MutableState<String>,
@@ -112,13 +95,6 @@ private fun GroupWelcomeLayout(
} else {
TextPreview(wt.value, linkMode)
}
SectionTextFooter(
if (!welcomeTextFitsLimit(wt)) { generalGetString(MR.strings.message_too_large) } else "",
color = if (welcomeTextFitsLimit(wt)) MaterialTheme.colors.secondary else Color.Red
)
Spacer(Modifier.size(8.dp))
ChangeModeButton(
editMode.value,
click = {
@@ -128,18 +104,10 @@ private fun GroupWelcomeLayout(
)
val clipboard = LocalClipboardManager.current
CopyTextButton { clipboard.setText(AnnotatedString(wt.value)) }
Divider(
Modifier.padding(
start = DEFAULT_PADDING_HALF,
top = 8.dp,
end = DEFAULT_PADDING_HALF,
bottom = 8.dp)
)
SectionDividerSpaced(maxBottomPadding = false)
SaveButton(
save = save,
disabled = welcomeTextUnchanged(wt, groupInfo) || !welcomeTextFitsLimit(wt)
disabled = wt.value == groupInfo.groupProfile.description || (wt.value == "" && groupInfo.groupProfile.description == null)
)
} else {
val clipboard = LocalClipboardManager.current
@@ -214,11 +182,3 @@ private fun showUnsavedChangesAlert(save: () -> Unit, revert: () -> Unit) {
onDismiss = revert,
)
}
private fun showUnsavedChangesTooLongAlert(revert: () -> Unit) {
AlertManager.shared.showAlertDialogStacked(
title = generalGetString(MR.strings.welcome_message_is_too_long),
confirmText = generalGetString(MR.strings.exit_without_saving),
onConfirm = revert,
)
}
@@ -94,7 +94,7 @@ fun CIFileView(
FileProtocol.LOCAL -> {}
}
file.fileStatus is CIFileStatus.RcvComplete || (file.fileStatus is CIFileStatus.SndStored && file.fileProtocol == FileProtocol.LOCAL) -> {
withLongRunningApi(slow = 600_000) {
withLongRunningApi(slow = 60_000, deadlock = 600_000) {
var filePath = getLoadedFilePath(file)
if (chatModel.connectedToRemote() && filePath == null) {
file.loadRemoteFile(true)
@@ -41,7 +41,7 @@ fun CIVideoView(
val filePath = remember(file, CIFile.cachedRemoteFileRequests.toList()) { mutableStateOf(getLoadedFilePath(file)) }
if (chatModel.connectedToRemote()) {
LaunchedEffect(file) {
withLongRunningApi(slow = 600_000) {
withLongRunningApi(slow = 60_000, deadlock = 600_000) {
if (file != null && file.loaded && getLoadedFilePath(file) == null) {
file.loadRemoteFile(false)
filePath.value = getLoadedFilePath(file)
@@ -103,7 +103,7 @@ fun ChatItemView(
setReaction(cInfo, cItem, !r.userReacted, r.reaction)
}
}
Row(modifier.padding(2.dp), verticalAlignment = Alignment.CenterVertically) {
Row(modifier.padding(2.dp)) {
ReactionIcon(r.reaction.text, fontSize = 12.sp)
if (r.totalReacted > 1) {
Spacer(Modifier.width(4.dp))
@@ -112,6 +112,7 @@ fun ChatItemView(
fontSize = 11.5.sp,
fontWeight = if (r.userReacted) FontWeight.Bold else FontWeight.Normal,
color = if (r.userReacted) MaterialTheme.colors.primary else MaterialTheme.colors.secondary,
modifier = if (appPlatform.isAndroid) Modifier else Modifier.padding(top = 4.dp)
)
}
}
@@ -177,8 +178,7 @@ fun ChatItemView(
fun MsgContentItemDropdownMenu() {
val saveFileLauncher = rememberSaveFileLauncher(ciFile = cItem.file)
when {
// cItem.id check is a special case for live message chat item which has negative ID while not sent yet
cItem.content.msgContent != null && cItem.id >= 0 -> {
cItem.content.msgContent != null -> {
DefaultDropdownMenu(showMenu) {
if (cInfo.featureEnabled(ChatFeature.Reactions) && cItem.allowAddReaction) {
MsgReactionsMenu()
@@ -213,7 +213,7 @@ fun ChatItemView(
showMenu.value = false
}
if (chatModel.connectedToRemote() && fileSource == null) {
withLongRunningApi(slow = 600_000) {
withLongRunningApi(slow = 60_000, deadlock = 600_000) {
cItem.file?.loadRemoteFile(true)
fileSource = getLoadedFileSource(cItem.file)
shareIfExists()
@@ -527,9 +527,8 @@ fun DeleteItemAction(
val range = chatViewItemsRange(currIndex, prevHidden)
if (range != null) {
val itemIds: ArrayList<Long> = arrayListOf()
val reversedChatItems = chatModel.chatItems.asReversed()
for (i in range) {
itemIds.add(reversedChatItems[i].id)
itemIds.add(chatModel.chatItems.asReversed()[i].id)
}
deleteMessagesAlertDialog(itemIds, generalGetString(MR.strings.delete_message_mark_deleted_warning), deleteMessages = deleteMessages)
} else {
@@ -652,23 +651,6 @@ fun ItemAction(text: String, icon: ImageVector, onClick: () -> Unit, color: Colo
}
}
@Composable
fun ItemAction(text: String, color: Color = Color.Unspecified, onClick: () -> Unit) {
val finalColor = if (color == Color.Unspecified) {
MenuTextColor
} else color
DropdownMenuItem(onClick, contentPadding = PaddingValues(horizontal = DEFAULT_PADDING * 1.5f)) {
Text(
text,
modifier = Modifier
.fillMaxWidth()
.weight(1F)
.padding(end = 15.dp),
color = finalColor
)
}
}
fun cancelFileAlertDialog(fileId: Long, cancelFile: (Long) -> Unit, cancelAction: CancelAction) {
AlertManager.shared.showAlertDialog(
title = generalGetString(cancelAction.alert.titleId),
@@ -91,7 +91,7 @@ private fun MergedMarkedDeletedText(chatItem: ChatItem, revealed: MutableState<B
)
}
fun markedDeletedText(meta: CIMeta): String =
private fun markedDeletedText(meta: CIMeta): String =
when (meta.itemDeleted) {
is CIDeleted.Moderated ->
String.format(generalGetString(MR.strings.moderated_item_description), meta.itemDeleted.byGroupMember.displayName)
@@ -212,15 +212,18 @@ suspend fun openGroupChat(rhId: Long?, groupId: Long, chatModel: ChatModel) {
}
suspend fun openChat(rhId: Long?, chatInfo: ChatInfo, chatModel: ChatModel) {
Log.d(TAG, "TODOCHAT: openChat: opening ${chatInfo.id}, current chatId ${ChatModel.chatId.value}, size ${ChatModel.chatItems.size}")
val chat = chatModel.controller.apiGetChat(rhId, chatInfo.chatType, chatInfo.apiId)
if (chat != null) {
openLoadedChat(chat, chatModel)
Log.d(TAG, "TODOCHAT: openChat: opened ${chatInfo.id}, current chatId ${ChatModel.chatId.value}, size ${ChatModel.chatItems.size}")
}
}
fun openLoadedChat(chat: Chat, chatModel: ChatModel) {
chatModel.chatItems.clear()
chatModel.chatItemStatuses.clear()
chatModel.chatItems.replaceAll(chat.chatItems)
chatModel.chatItems.addAll(chat.chatItems)
chatModel.chatId.value = chat.chatInfo.id
}
@@ -236,7 +239,8 @@ suspend fun apiFindMessages(ch: Chat, chatModel: ChatModel, search: String) {
val chatInfo = ch.chatInfo
val chat = chatModel.controller.apiGetChat(ch.remoteHostId, chatInfo.chatType, chatInfo.apiId, search = search) ?: return
if (chatModel.chatId.value != chat.id) return
chatModel.chatItems.replaceAll(chat.chatItems)
chatModel.chatItems.clear()
chatModel.chatItems.addAll(0, chat.chatItems)
}
suspend fun setGroupMembers(rhId: Long?, groupInfo: GroupInfo, chatModel: ChatModel) {
@@ -29,7 +29,6 @@ import chat.simplex.common.views.onboarding.WhatsNewView
import chat.simplex.common.views.onboarding.shouldShowWhatsNew
import chat.simplex.common.views.usersettings.SettingsView
import chat.simplex.common.platform.*
import chat.simplex.common.views.call.Call
import chat.simplex.common.views.newchat.*
import chat.simplex.res.MR
import kotlinx.coroutines.*
@@ -122,12 +121,7 @@ fun ChatListView(chatModel: ChatModel, settingsState: SettingsViewState, setPerf
}
}
if (searchText.value.text.isEmpty()) {
if (appPlatform.isDesktop) {
val call = remember { chatModel.activeCall }.value
if (call != null) {
ActiveCallInteractiveArea(call, newChatSheetState)
}
}
DesktopActiveCallOverlayLayout(newChatSheetState)
// TODO disable this button and sheet for the duration of the switch
tryOrShowError("NewChatSheet", error = {}) {
NewChatSheet(chatModel, newChatSheetState, stopped, hideNewChatSheet)
@@ -320,7 +314,7 @@ private fun ToggleFilterDisabledButton() {
}
@Composable
expect fun ActiveCallInteractiveArea(call: Call, newChatSheetState: MutableStateFlow<AnimatedViewState>)
expect fun DesktopActiveCallOverlayLayout(newChatSheetState: MutableStateFlow<AnimatedViewState>)
fun connectIfOpenedViaUri(rhId: Long?, uri: URI, chatModel: ChatModel) {
Log.d(TAG, "connectIfOpenedViaUri: opened via link")
@@ -26,7 +26,6 @@ import chat.simplex.common.views.helpers.*
import chat.simplex.common.model.*
import chat.simplex.common.model.GroupInfo
import chat.simplex.common.platform.chatModel
import chat.simplex.common.views.chat.item.markedDeletedText
import chat.simplex.res.MR
import dev.icerock.moko.resources.ImageResource
@@ -171,7 +170,7 @@ fun ChatPreviewView(
val (text: CharSequence, inlineTextContent) = when {
chatModelDraftChatId == chat.id && chatModelDraft != null -> remember(chatModelDraft) { messageDraft(chatModelDraft) }
ci.meta.itemDeleted == null -> ci.text to null
else -> markedDeletedText(ci.meta) to null
else -> generalGetString(MR.strings.marked_deleted_description) to null
}
val formattedText = when {
chatModelDraftChatId == chat.id && chatModelDraft != null -> null
@@ -287,7 +286,7 @@ fun ChatPreviewView(
Box(
contentAlignment = Alignment.TopEnd
) {
val ts = chat.chatItems.lastOrNull()?.timestampText ?: getTimestampText(chat.chatInfo.chatTs)
val ts = chat.chatItems.lastOrNull()?.timestampText ?: getTimestampText(chat.chatInfo.updatedAt)
Text(
ts,
color = MaterialTheme.colors.secondary,
@@ -85,7 +85,7 @@ private fun ShareListToolbar(chatModel: ChatModel, userPickerState: MutableState
userPickerState.value = AnimatedViewState.VISIBLE
}
}
else -> NavigationButtonBack(onButtonClicked = { chatModel.sharedContent.value = null })
else -> NavigationButtonBack { chatModel.sharedContent.value = null }
}
}
if (chatModel.chats.size >= 8) {
@@ -143,7 +143,7 @@ private fun ShareList(chatModel: ChatModel, search: String) {
}
val chats by remember(search) {
derivedStateOf {
if (search.isEmpty()) chatModel.chats.toList().filter { it.chatInfo.ready } else chatModel.chats.toList().filter { it.chatInfo.ready }.filter(filter)
if (search.isEmpty()) chatModel.chats.filter { it.chatInfo.ready } else chatModel.chats.filter { it.chatInfo.ready }.filter(filter)
}
}
LazyColumn(
@@ -62,7 +62,7 @@ fun DatabaseEncryptionView(m: ChatModel) {
initialRandomDBPassphrase,
progressIndicator,
onConfirmEncrypt = {
withLongRunningApi {
withLongRunningApi(slow = 30_000, deadlock = 60_000) {
encryptDatabase(currentKey, newKey, confirmNewKey, initialRandomDBPassphrase, useKeychain, storedKey, progressIndicator)
}
}
@@ -233,13 +233,13 @@ fun resetFormAfterEncryption(
storedKey: MutableState<Boolean>,
stored: Boolean = false,
) {
m.chatDbEncrypted.value = true
initialRandomDBPassphrase.value = false
m.controller.appPrefs.initialRandomDBPassphrase.set(false)
currentKey.value = ""
newKey.value = ""
confirmNewKey.value = ""
storedKey.value = stored
m.chatDbEncrypted.value = true
initialRandomDBPassphrase.value = false
m.controller.appPrefs.initialRandomDBPassphrase.set(false)
}
fun setUseKeychain(value: Boolean, useKeychain: MutableState<Boolean>, prefs: AppPreferences) {
@@ -392,11 +392,12 @@ suspend fun encryptDatabase(
false
}
else -> {
val new = newKey.value
resetFormAfterEncryption(m, initialRandomDBPassphrase, currentKey, newKey, confirmNewKey, storedKey, useKeychain.value)
prefs.initialRandomDBPassphrase.set(false)
initialRandomDBPassphrase.value = false
if (useKeychain.value) {
DatabaseUtils.ksDatabasePassword.set(new)
DatabaseUtils.ksDatabasePassword.set(newKey.value)
}
resetFormAfterEncryption(m, initialRandomDBPassphrase, currentKey, newKey, confirmNewKey, storedKey, useKeychain.value)
operationEnded(m, progressIndicator) {
AlertManager.shared.showAlertMsg(generalGetString(MR.strings.database_encrypted))
}
@@ -368,7 +368,7 @@ fun chatArchiveTitle(chatArchiveTime: Instant, chatLastStart: Instant): String {
}
fun startChat(m: ChatModel, chatLastStart: MutableState<Instant?>, chatDbChanged: MutableState<Boolean>, progressIndicator: MutableState<Boolean>? = null) {
withLongRunningApi {
withLongRunningApi(slow = 30_000, deadlock = 60_000) {
try {
progressIndicator?.value = true
if (chatDbChanged.value) {
@@ -581,7 +581,7 @@ private fun importArchive(
progressIndicator.value = true
val archivePath = saveArchiveFromURI(importedArchiveURI)
if (archivePath != null) {
withLongRunningApi {
withLongRunningApi(slow = 60_000, deadlock = 180_000) {
try {
m.controller.apiDeleteStorage()
try {
@@ -12,7 +12,6 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.*
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalClipboardManager
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.style.TextAlign
@@ -23,8 +22,6 @@ import chat.simplex.common.ui.theme.*
import chat.simplex.res.MR
import dev.icerock.moko.resources.StringResource
import dev.icerock.moko.resources.compose.painterResource
import dev.icerock.moko.resources.compose.stringResource
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
class AlertManager {
@@ -131,8 +128,6 @@ class AlertManager {
) {
val focusRequester = remember { FocusRequester() }
LaunchedEffect(Unit) {
// Wait before focusing to prevent auto-confirming if a user used Enter key on hardware keyboard
delay(200)
focusRequester.requestFocus()
}
TextButton(onClick = {
@@ -191,7 +186,6 @@ class AlertManager {
title: String, text: String? = null,
confirmText: String = generalGetString(MR.strings.ok),
hostDevice: Pair<Long?, String>? = null,
shareText: Boolean? = null
) {
showAlert {
AlertDialog(
@@ -201,23 +195,12 @@ class AlertManager {
AlertContent(text, hostDevice, extraPadding = true) {
val focusRequester = remember { FocusRequester() }
LaunchedEffect(Unit) {
// Wait before focusing to prevent auto-confirming if a user used Enter key on hardware keyboard
delay(200)
focusRequester.requestFocus()
}
// Can pass shareText = false to prevent showing Share button if it's needed in a specific case
val showShareButton = text != null && (shareText == true || (shareText == null && text.length > 500))
Row(
Modifier.fillMaxWidth().padding(horizontal = DEFAULT_PADDING),
horizontalArrangement = if (showShareButton) Arrangement.SpaceBetween else Arrangement.Center
horizontalArrangement = Arrangement.Center
) {
val clipboard = LocalClipboardManager.current
if (showShareButton && text != null) {
TextButton(onClick = {
clipboard.shareText(text)
hideAlert()
}) { Text(stringResource(MR.strings.share_verb)) }
}
TextButton(
onClick = {
hideAlert()
@@ -27,8 +27,8 @@ fun ChatInfoImage(chatInfo: ChatInfo, size: Dp, iconColor: Color = MaterialTheme
val icon =
when (chatInfo) {
is ChatInfo.Group -> MR.images.ic_supervised_user_circle_filled
is ChatInfo.Local -> MR.images.ic_folder_filled
else -> MR.images.ic_account_circle_filled
is ChatInfo.Direct -> MR.images.ic_account_circle_filled
else -> MR.images.ic_folder_filled
}
ProfileImage(size, chatInfo.image, icon, if (chatInfo is ChatInfo.Local) NoteFolderIconColor else iconColor)
}
@@ -18,7 +18,7 @@ import chat.simplex.res.MR
import dev.icerock.moko.resources.compose.painterResource
@Composable
fun CloseSheetBar(close: (() -> Unit)?, showClose: Boolean = true, tintColor: Color = if (close != null) MaterialTheme.colors.primary else MaterialTheme.colors.secondary, endButtons: @Composable RowScope.() -> Unit = {}) {
fun CloseSheetBar(close: (() -> Unit)?, showClose: Boolean = true, endButtons: @Composable RowScope.() -> Unit = {}) {
Column(
Modifier
.fillMaxWidth()
@@ -35,7 +35,7 @@ fun CloseSheetBar(close: (() -> Unit)?, showClose: Boolean = true, tintColor: Co
verticalAlignment = Alignment.CenterVertically
) {
if (showClose) {
NavigationButtonBack(tintColor = tintColor, onButtonClicked = close)
NavigationButtonBack(onButtonClicked = close)
} else {
Spacer(Modifier)
}
@@ -1,21 +1,116 @@
package chat.simplex.common.views.helpers
import SectionItemView
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CornerSize
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.style.TextAlign
import dev.icerock.moko.resources.compose.painterResource
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.ui.window.Dialog
import chat.simplex.common.ui.theme.DEFAULT_PADDING
import chat.simplex.common.model.CustomTimeUnit
import chat.simplex.common.model.timeText
import chat.simplex.res.MR
import com.sd.lib.compose.wheel_picker.*
@Composable
expect fun CustomTimePicker(
fun CustomTimePicker(
selection: MutableState<Int>,
timeUnitsLimits: List<TimeUnitLimits> = TimeUnitLimits.defaultUnitsLimits
)
) {
fun getUnitValues(unit: CustomTimeUnit, selectedValue: Int): List<Int> {
val unitLimits = timeUnitsLimits.firstOrNull { it.timeUnit == unit } ?: TimeUnitLimits.defaultUnitLimits(unit)
val regularUnitValues = (unitLimits.minValue..unitLimits.maxValue).toList()
return regularUnitValues + if (regularUnitValues.contains(selectedValue)) emptyList() else listOf(selectedValue)
}
val (unit, duration) = CustomTimeUnit.toTimeUnit(selection.value)
val selectedUnit: MutableState<CustomTimeUnit> = remember { mutableStateOf(unit) }
val selectedDuration = remember { mutableStateOf(duration) }
val selectedUnitValues = remember { mutableStateOf(getUnitValues(selectedUnit.value, selectedDuration.value)) }
val isTriggered = remember { mutableStateOf(false) }
LaunchedEffect(selectedUnit.value) {
// on initial composition, if passed selection doesn't fit into picker bounds, so that selectedDuration is bigger than selectedUnit maxValue
// (e.g., for selection = 121 seconds: selectedUnit would be Second, selectedDuration would be 121 > selectedUnit maxValue of 120),
// selectedDuration would've been replaced by maxValue - isTriggered check prevents this by skipping LaunchedEffect on initial composition
if (isTriggered.value) {
val maxValue = timeUnitsLimits.firstOrNull { it.timeUnit == selectedUnit.value }?.maxValue
if (maxValue != null && selectedDuration.value > maxValue) {
selectedDuration.value = maxValue
selectedUnitValues.value = getUnitValues(selectedUnit.value, selectedDuration.value)
} else {
selectedUnitValues.value = getUnitValues(selectedUnit.value, selectedDuration.value)
selection.value = selectedUnit.value.toSeconds * selectedDuration.value
}
} else {
isTriggered.value = true
}
}
LaunchedEffect(selectedDuration.value) {
selection.value = selectedUnit.value.toSeconds * selectedDuration.value
}
Row(
Modifier
.fillMaxWidth()
.padding(horizontal = DEFAULT_PADDING),
horizontalArrangement = Arrangement.spacedBy(0.dp)
) {
Column(Modifier.weight(1f)) {
val durationPickerState = rememberFWheelPickerState(selectedUnitValues.value.indexOf(selectedDuration.value))
FVerticalWheelPicker(
count = selectedUnitValues.value.count(),
state = durationPickerState,
unfocusedCount = 2,
focus = {
FWheelPickerFocusVertical(dividerColor = MaterialTheme.colors.primary)
}
) { index ->
Text(
selectedUnitValues.value[index].toString(),
fontSize = 18.sp,
color = MaterialTheme.colors.primary
)
}
LaunchedEffect(durationPickerState) {
snapshotFlow { durationPickerState.currentIndex }
.collect {
selectedDuration.value = selectedUnitValues.value[it]
}
}
}
Column(Modifier.weight(1f)) {
val unitPickerState = rememberFWheelPickerState(timeUnitsLimits.indexOfFirst { it.timeUnit == selectedUnit.value })
FVerticalWheelPicker(
count = timeUnitsLimits.count(),
state = unitPickerState,
unfocusedCount = 2,
focus = {
FWheelPickerFocusVertical(dividerColor = MaterialTheme.colors.primary)
}
) { index ->
Text(
timeUnitsLimits[index].timeUnit.text,
fontSize = 18.sp,
color = MaterialTheme.colors.primary
)
}
LaunchedEffect(unitPickerState) {
snapshotFlow { unitPickerState.currentIndex }
.collect {
selectedUnit.value = timeUnitsLimits[it].timeUnit
}
}
}
}
}
data class TimeUnitLimits(
val timeUnit: CustomTimeUnit,
@@ -46,7 +141,8 @@ data class TimeUnitLimits(
}
}
fun showCustomTimePickerDialog(
@Composable
fun CustomTimePickerDialog(
selection: MutableState<Int>,
timeUnitsLimits: List<TimeUnitLimits> = TimeUnitLimits.defaultUnitsLimits,
title: String,
@@ -54,26 +150,53 @@ fun showCustomTimePickerDialog(
confirmButtonAction: (Int) -> Unit,
cancel: () -> Unit
) {
AlertManager.shared.showAlertDialogButtonsColumn(
title = title,
onDismissRequest = cancel
) {
Column(horizontalAlignment = Alignment.CenterHorizontally) {
CustomTimePicker(
selection,
timeUnitsLimits
)
SectionItemView({
AlertManager.shared.hideAlert()
confirmButtonAction(selection.value)
}
DefaultDialog(onDismissRequest = cancel) {
Surface(
shape = RoundedCornerShape(corner = CornerSize(25.dp)),
contentColor = LocalContentColor.current
) {
Box(
contentAlignment = Alignment.Center
) {
Text(
confirmButtonText,
Modifier.fillMaxWidth(),
textAlign = TextAlign.Center,
color = MaterialTheme.colors.primary
)
Column(
modifier = Modifier.padding(DEFAULT_PADDING),
verticalArrangement = Arrangement.spacedBy(6.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Text(" ") // centers title
Text(
title,
fontSize = 16.sp,
color = MaterialTheme.colors.secondary
)
Icon(
painterResource(MR.images.ic_close),
generalGetString(MR.strings.icon_descr_close_button),
tint = MaterialTheme.colors.secondary,
modifier = Modifier
.size(25.dp)
.clickable { cancel() }
)
}
CustomTimePicker(
selection,
timeUnitsLimits
)
TextButton(onClick = { confirmButtonAction(selection.value) }) {
Text(
confirmButtonText,
fontSize = 18.sp,
color = MaterialTheme.colors.primary
)
}
}
}
}
}
@@ -97,6 +220,7 @@ fun DropdownCustomTimePickerSettingRow(
val dropdownSelection: MutableState<DropdownSelection> = remember { mutableStateOf(DropdownSelection.DropdownValue(selection.value)) }
val values: MutableState<List<DropdownSelection>> = remember { mutableStateOf(getValues(selection.value)) }
val showCustomTimePicker = remember { mutableStateOf(false) }
fun updateValue(selectedValue: Int?) {
values.value = getValues(selectedValue)
@@ -123,22 +247,28 @@ fun DropdownCustomTimePickerSettingRow(
onSelected = { sel: DropdownSelection ->
when (sel) {
is DropdownSelection.DropdownValue -> updateValue(sel.value)
DropdownSelection.Custom -> {
val selectedCustomTime = mutableStateOf(selection.value ?: 86400)
showCustomTimePickerDialog(
selectedCustomTime,
timeUnitsLimits = customPickerTimeUnitsLimits,
title = customPickerTitle,
confirmButtonText = customPickerConfirmButtonText,
confirmButtonAction = ::updateValue,
cancel = {
dropdownSelection.value = DropdownSelection.DropdownValue(selection.value)
}
)
}
DropdownSelection.Custom -> showCustomTimePicker.value = true
}
}
)
if (showCustomTimePicker.value) {
val selectedCustomTime = remember { mutableStateOf(selection.value ?: 86400) }
CustomTimePickerDialog(
selectedCustomTime,
timeUnitsLimits = customPickerTimeUnitsLimits,
title = customPickerTitle,
confirmButtonText = customPickerConfirmButtonText,
confirmButtonAction = { time ->
updateValue(time)
showCustomTimePicker.value = false
},
cancel = {
dropdownSelection.value = DropdownSelection.DropdownValue(selection.value)
showCustomTimePicker.value = false
}
)
}
}
private sealed class DropdownSelection {
@@ -5,7 +5,6 @@ import androidx.compose.material.*
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import chat.simplex.common.ui.theme.DEFAULT_PADDING
@@ -21,7 +20,7 @@ fun DefaultProgressView(description: String?) {
strokeWidth = 2.5.dp
)
if (description != null) {
Text(description, textAlign = TextAlign.Center)
Text(description)
}
}
}
@@ -44,10 +44,10 @@ fun DefaultTopAppBar(
}
@Composable
fun NavigationButtonBack(onButtonClicked: (() -> Unit)?, tintColor: Color = if (onButtonClicked != null) MaterialTheme.colors.primary else MaterialTheme.colors.secondary) {
fun NavigationButtonBack(onButtonClicked: (() -> Unit)?) {
IconButton(onButtonClicked ?: {}, enabled = onButtonClicked != null) {
Icon(
painterResource(MR.images.ic_arrow_back_ios_new), stringResource(MR.strings.back), tint = tintColor
painterResource(MR.images.ic_arrow_back_ios_new), stringResource(MR.strings.back), tint = if (onButtonClicked != null) MaterialTheme.colors.primary else MaterialTheme.colors.secondary
)
}
}
@@ -29,7 +29,7 @@ fun ModalView(
}
Surface(Modifier.fillMaxSize(), contentColor = LocalContentColor.current) {
Column(if (background != MaterialTheme.colors.background) Modifier.background(background) else Modifier.themedBackground()) {
CloseSheetBar(close, showClose, endButtons = endButtons)
CloseSheetBar(close, showClose, endButtons)
Box(modifier) { content() }
}
}
@@ -61,10 +61,10 @@ class ModalManager(private val placement: ModalPlacement? = null) {
}
}
fun showModalCloseable(settings: Boolean = false, showClose: Boolean = true, endButtons: @Composable RowScope.() -> Unit = {}, content: @Composable ModalData.(close: () -> Unit) -> Unit) {
fun showModalCloseable(settings: Boolean = false, showClose: Boolean = true, content: @Composable ModalData.(close: () -> Unit) -> Unit) {
val data = ModalData()
showCustomModal { close ->
ModalView(close, showClose = showClose, endButtons = endButtons, content = { data.content(close) })
ModalView(close, showClose = showClose, content = { data.content(close) })
}
}
@@ -16,7 +16,7 @@ class ProcessedErrors <T: AgentErrorType>(val interval: Long) {
fun newError(error: T, offerRestart: Boolean) {
timer.cancel()
timer = withLongRunningApi(slow = 130_000) {
timer = withLongRunningApi(slow = 70_000, deadlock = 130_000) {
val delayBeforeNext = (lastShownTimestamp + interval) - System.currentTimeMillis()
if ((lastShownOfferRestart || !offerRestart) && delayBeforeNext >= 0) {
delay(delayBeforeNext)
@@ -22,7 +22,6 @@ import dev.icerock.moko.resources.compose.stringResource
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.*
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import chat.simplex.res.MR
@@ -96,7 +95,7 @@ fun SearchTextField(
value = searchText.value.text,
innerTextField = innerTextField,
placeholder = {
Text(placeholder, maxLines = 1, overflow = TextOverflow.Ellipsis)
Text(placeholder)
},
trailingIcon = if (searchText.value.text.isNotEmpty()) {{
IconButton({
@@ -198,16 +198,16 @@ fun <T> SectionItemWithValue(
}
@Composable
fun SectionTextFooter(text: String, color: Color = MaterialTheme.colors.secondary) {
SectionTextFooter(AnnotatedString(text), color = color)
fun SectionTextFooter(text: String) {
SectionTextFooter(AnnotatedString(text))
}
@Composable
fun SectionTextFooter(text: AnnotatedString, textAlign: TextAlign = TextAlign.Start, color: Color = MaterialTheme.colors.secondary) {
fun SectionTextFooter(text: AnnotatedString, textAlign: TextAlign = TextAlign.Start) {
Text(
text,
Modifier.padding(start = DEFAULT_PADDING, end = DEFAULT_PADDING, top = DEFAULT_PADDING_HALF).fillMaxWidth(0.9F),
color = color,
color = MaterialTheme.colors.secondary,
lineHeight = 18.sp,
fontSize = 14.sp,
textAlign = textAlign
@@ -37,22 +37,30 @@ fun withBGApi(action: suspend CoroutineScope.() -> Unit): Job =
CoroutineScope(singleThreadDispatcher).launch(block = { wrapWithLogging(action, it) })
}
fun withLongRunningApi(slow: Long = Long.MAX_VALUE, action: suspend CoroutineScope.() -> Unit): Job =
fun withLongRunningApi(slow: Long = Long.MAX_VALUE, deadlock: Long = Long.MAX_VALUE, action: suspend CoroutineScope.() -> Unit): Job =
Exception().let {
CoroutineScope(Dispatchers.Default).launch(block = { wrapWithLogging(action, it, slow = slow) })
CoroutineScope(Dispatchers.Default).launch(block = { wrapWithLogging(action, it, slow = slow, deadlock = deadlock) })
}
private suspend fun wrapWithLogging(action: suspend CoroutineScope.() -> Unit, exception: java.lang.Exception, slow: Long = 20_000) = coroutineScope {
private suspend fun wrapWithLogging(action: suspend CoroutineScope.() -> Unit, exception: java.lang.Exception, slow: Long = 10_000, deadlock: Long = 60_000) = coroutineScope {
val start = System.currentTimeMillis()
val job = launch {
delay(deadlock)
Log.e(TAG, "Possible deadlock of the thread, not finished after ${deadlock / 1000}s:\n${exception.stackTraceToString()}")
AlertManager.shared.showAlertMsg(
title = generalGetString(MR.strings.possible_deadlock_title),
text = generalGetString(MR.strings.possible_deadlock_desc).format(deadlock / 1000, exception.stackTraceToString()),
)
}
action()
val end = System.currentTimeMillis()
if (end - start > slow) {
Log.e(TAG, "Possible problem with execution of the thread, took ${(end - start) / 1000}s:\n${exception.stackTraceToString()}")
if (appPreferences.developerTools.get() && appPreferences.showSlowApiCalls.get()) {
job.cancel()
if (appPreferences.developerTools.get() && appPreferences.showSlowApiCalls.get()) {
val end = System.currentTimeMillis()
if (end - start > slow) {
Log.e(TAG, "Possible problem with execution of the thread, took ${(end - start) / 1000}s:\n${exception.stackTraceToString()}")
AlertManager.shared.showAlertMsg(
title = generalGetString(MR.strings.possible_slow_function_title),
text = generalGetString(MR.strings.possible_slow_function_desc).format((end - start) / 1000, exception.stackTraceToString()),
shareText = true
)
}
}
@@ -411,7 +419,7 @@ expect fun ByteArray.toBase64StringForPassphrase(): String
// Android's default implementation that was used before multiplatform, adds non-needed characters at the end of string
// which can be bypassed by:
// fun String.toByteArrayFromBase64(): ByteArray = Base64.getMimeDecoder().decode(this.trimEnd { it == '\n' || it == ' ' })
// fun String.toByteArrayFromBase64(): ByteArray = Base64.getDecoder().decode(this.trimEnd { it == '\n' || it == ' ' })
expect fun String.toByteArrayFromBase64ForPassphrase(): ByteArray
val LongRange.Companion.saver
@@ -49,7 +49,7 @@ fun LocalAuthView(m: ChatModel, authRequest: LocalAuthRequest) {
}
private fun deleteStorageAndRestart(m: ChatModel, password: String, completed: (LAResult) -> Unit) {
withLongRunningApi {
withLongRunningApi(slow = 30_000, deadlock = 60_000) {
try {
/** Waiting until [initChatController] finishes */
while (m.ctrlInitInProgress.value) {
@@ -50,7 +50,7 @@ fun SetupDatabasePassphrase(m: ChatModel) {
confirmNewKey,
progressIndicator,
onConfirmEncrypt = {
withLongRunningApi {
withLongRunningApi(slow = 30_000, deadlock = 60_000) {
if (m.chatRunning.value == true) {
// Stop chat if it's started before doing anything
stopChatAsync(m)
@@ -36,7 +36,7 @@ fun WhatsNewView(viaSettings: Boolean = false, close: () -> Unit) {
Icon(
painterResource(MR.images.ic_open_in_new), stringResource(titleId), tint = MaterialTheme.colors.primary,
modifier = Modifier
.clickable { if (link.startsWith("simplex:")) uriHandler.openVerifiedSimplexUri(link) else uriHandler.openUriCatching(link) }
.clickable { uriHandler.openUriCatching(link) }
)
}
@@ -47,6 +47,10 @@ fun NetworkAndServersView(
val onionHosts = remember { mutableStateOf(netCfg.onionHosts) }
val sessionMode = remember { mutableStateOf(netCfg.sessionMode) }
LaunchedEffect(Unit) {
chatModel.userSMPServersUnsaved.value = null
}
val proxyPort = remember { derivedStateOf { chatModel.controller.appPrefs.networkProxyHostPort.state.value?.split(":")?.lastOrNull()?.toIntOrNull() ?: 9050 } }
NetworkAndServersLayout(
currentRemoteHost = currentRemoteHost,
@@ -96,7 +96,7 @@ fun PrivacySettingsView(
val currentUser = chatModel.currentUser.value
if (currentUser != null) {
fun setSendReceiptsContacts(enable: Boolean, clearOverrides: Boolean) {
withLongRunningApi(slow = 60_000) {
withLongRunningApi(slow = 30_000, deadlock = 60_000) {
val mrs = UserMsgReceiptSettings(enable, clearOverrides)
chatModel.controller.apiSetUserContactReceipts(currentUser, mrs)
chatModel.controller.appPrefs.privacyDeliveryReceiptsSet.set(true)
@@ -119,7 +119,7 @@ fun PrivacySettingsView(
}
fun setSendReceiptsGroups(enable: Boolean, clearOverrides: Boolean) {
withLongRunningApi(slow = 60_000) {
withLongRunningApi(slow = 30_000, deadlock = 60_000) {
val mrs = UserMsgReceiptSettings(enable, clearOverrides)
chatModel.controller.apiSetUserGroupReceipts(currentUser, mrs)
chatModel.controller.appPrefs.privacyDeliveryReceiptsSet.set(true)
@@ -28,18 +28,19 @@ import chat.simplex.res.MR
@Composable
fun ModalData.ProtocolServersView(m: ChatModel, rhId: Long?, serverProtocol: ServerProtocol, close: () -> Unit) {
var presetServers by remember(rhId) { mutableStateOf(emptyList<String>()) }
var servers by remember { stateGetOrPut("servers") { emptyList<ServerCfg>() } }
var serversAlreadyLoaded by remember { stateGetOrPut("serversAlreadyLoaded") { false } }
var servers by remember(rhId) {
mutableStateOf(m.userSMPServersUnsaved.value ?: emptyList())
}
val currServers = remember(rhId) { mutableStateOf(servers) }
val testing = rememberSaveable(rhId) { mutableStateOf(false) }
val serversUnchanged = remember(servers) { derivedStateOf { servers == currServers.value || testing.value } }
val allServersDisabled = remember { derivedStateOf { servers.none { it.enabled } } }
val saveDisabled = remember(servers) {
val serversUnchanged = remember { derivedStateOf { servers == currServers.value || testing.value } }
val allServersDisabled = remember { derivedStateOf { servers.all { !it.enabled } } }
val saveDisabled = remember {
derivedStateOf {
servers.isEmpty() ||
servers == currServers.value ||
testing.value ||
servers.none { srv ->
!servers.all { srv ->
val address = parseServerAddress(srv.server)
address != null && uniqueAddress(srv, address, servers)
} ||
@@ -48,8 +49,8 @@ fun ModalData.ProtocolServersView(m: ChatModel, rhId: Long?, serverProtocol: Ser
}
KeyChangeEffect(rhId) {
m.userSMPServersUnsaved.value = null
servers = emptyList()
serversAlreadyLoaded = false
}
LaunchedEffect(rhId) {
@@ -58,9 +59,8 @@ fun ModalData.ProtocolServersView(m: ChatModel, rhId: Long?, serverProtocol: Ser
if (res != null) {
currServers.value = res.protoServers
presetServers = res.presetServers
if (servers.isEmpty() && !serversAlreadyLoaded) {
if (servers.isEmpty()) {
servers = currServers.value
serversAlreadyLoaded = true
}
}
}
@@ -80,11 +80,13 @@ fun ModalData.ProtocolServersView(m: ChatModel, rhId: Long?, serverProtocol: Ser
newServers.add(index, updated)
old = updated
servers = newServers
m.userSMPServersUnsaved.value = servers
},
onDelete = {
val newServers = ArrayList(servers)
newServers.removeAt(index)
servers = newServers
m.userSMPServersUnsaved.value = servers
close()
})
}
@@ -123,6 +125,7 @@ fun ModalData.ProtocolServersView(m: ChatModel, rhId: Long?, serverProtocol: Ser
ScanProtocolServer(rhId) {
close()
servers = servers + it
m.userSMPServersUnsaved.value = servers
}
}
}
@@ -147,11 +150,13 @@ fun ModalData.ProtocolServersView(m: ChatModel, rhId: Long?, serverProtocol: Ser
testServersJob.value = withLongRunningApi {
testServers(testing, servers, m) {
servers = it
m.userSMPServersUnsaved.value = servers
}
}
},
resetServers = {
servers = currServers.value
servers = currServers.value ?: emptyList()
m.userSMPServersUnsaved.value = null
},
saveSMPServers = {
saveServers(rhId, serverProtocol, currServers, servers, m)
@@ -350,6 +355,7 @@ private fun saveServers(rhId: Long?, protocol: ServerProtocol, currServers: Muta
withBGApi {
if (m.controller.setUserProtoServers(rhId, protocol, servers)) {
currServers.value = servers
m.userSMPServersUnsaved.value = null
}
afterSave()
}

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