Compare commits
649 Commits
v4.5.2
...
ep/survey-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f3445d093f | ||
|
|
c7783a7039 | ||
|
|
80bd734cc1 | ||
|
|
0c34a545fa | ||
|
|
65c6c63024 | ||
|
|
f43fd57ec1 | ||
|
|
065b932e1f | ||
|
|
7ebb763889 | ||
|
|
eacfc4aa8c | ||
|
|
9c49b038cd | ||
|
|
1d4afe591e | ||
|
|
10ec3dd8b6 | ||
|
|
a715e847ad | ||
|
|
9ac0f30c5a | ||
|
|
b033fdbeee | ||
|
|
7996194f92 | ||
|
|
c2054b5ccf | ||
|
|
53dbe4b5d8 | ||
|
|
417eca74ad | ||
|
|
d25ef4e1a1 | ||
|
|
5d775a63c6 | ||
|
|
576c886ba0 | ||
|
|
511e3586d9 | ||
|
|
1cb500bc16 | ||
|
|
f5825d20e4 | ||
|
|
7a166e46a9 | ||
|
|
77d249cc37 | ||
|
|
3f905f59df | ||
|
|
87c35b037e | ||
|
|
d63c7d2abc | ||
|
|
ca5b3ddc0d | ||
|
|
4e2acbf456 | ||
|
|
202ecc369a | ||
|
|
e5cec7a68b | ||
|
|
05b292ac00 | ||
|
|
562bd197bb | ||
|
|
94321cfc36 | ||
|
|
7b863ef459 | ||
|
|
1aedfd6e5a | ||
|
|
572e3b7d32 | ||
|
|
ab708f8855 | ||
|
|
f5612504f5 | ||
|
|
94e25d9bb4 | ||
|
|
369d411fc1 | ||
|
|
94312ec6fa | ||
|
|
4b652b62da | ||
|
|
bf4df9ca58 | ||
|
|
27f4661ac4 | ||
|
|
2389e870b3 | ||
|
|
d61ff0f2a7 | ||
|
|
61334d7b77 | ||
|
|
9a714a0926 | ||
|
|
7ddd300fe5 | ||
|
|
6b663baf10 | ||
|
|
048ada79bb | ||
|
|
b69f422708 | ||
|
|
396abdbfab | ||
|
|
938bd56c3a | ||
|
|
d3b5bbe566 | ||
|
|
1bd8f66730 | ||
|
|
c2177f3684 | ||
|
|
72c0c61a86 | ||
|
|
f594752bb1 | ||
|
|
4a3c9366fd | ||
|
|
f5d61e7838 | ||
|
|
e762923410 | ||
|
|
d87b86199c | ||
|
|
a7a66c2b55 | ||
|
|
6ca76ec8a9 | ||
|
|
b089836efc | ||
|
|
90b616cd28 | ||
|
|
f970ef264a | ||
|
|
3793cd138e | ||
|
|
8dd90733b8 | ||
|
|
0f4473d272 | ||
|
|
0bdd96ae8a | ||
|
|
43ceb184c4 | ||
|
|
dd62b1cccb | ||
|
|
2e5a0fca1a | ||
|
|
38f40fec3d | ||
|
|
34c2303ef1 | ||
|
|
ced69e431c | ||
|
|
dcedbac379 | ||
|
|
a6a87cb7de | ||
|
|
416ae400eb | ||
|
|
b69916a3a3 | ||
|
|
62726e345c | ||
|
|
7a8db16791 | ||
|
|
ff7c22e114 | ||
|
|
e24564d7d6 | ||
|
|
ae17566a94 | ||
|
|
c329bf4ea1 | ||
|
|
7fea9c85bd | ||
|
|
313d3a732d | ||
|
|
5d9b6266ea | ||
|
|
c35ce29cc1 | ||
|
|
842bbf26c6 | ||
|
|
ebc5242932 | ||
|
|
be5e0d7f75 | ||
|
|
324a6ba38e | ||
|
|
7b67bc2d47 | ||
|
|
2f7ea909e2 | ||
|
|
9238ac3445 | ||
|
|
5713f81847 | ||
|
|
3bd5fc7463 | ||
|
|
4c33ed92bb | ||
|
|
a0ae4125c5 | ||
|
|
34a60066fb | ||
|
|
1f50e94bc9 | ||
|
|
0e4376bada | ||
|
|
b0ad94fe7f | ||
|
|
fccd4f7ec4 | ||
|
|
3f93397031 | ||
|
|
a5f8641d50 | ||
|
|
f23c0b55f8 | ||
|
|
534151f1bb | ||
|
|
2ad9d0ddbc | ||
|
|
388bdc7083 | ||
|
|
3e370a7c16 | ||
|
|
30d4fc757c | ||
|
|
77b3870654 | ||
|
|
b088b1c44c | ||
|
|
8abad4f711 | ||
|
|
4c668f7a34 | ||
|
|
0bf5fbd641 | ||
|
|
cfec60bf86 | ||
|
|
9caaab0e8e | ||
|
|
6da18d9b2a | ||
|
|
da2622f00e | ||
|
|
7ed581dfbf | ||
|
|
78c0fe73a7 | ||
|
|
15b00f6110 | ||
|
|
f592a26b00 | ||
|
|
30687f5fa6 | ||
|
|
bc7217d686 | ||
|
|
8e0b3fa32e | ||
|
|
d929c34e71 | ||
|
|
ec7bff9205 | ||
|
|
22f20a9c5f | ||
|
|
ddf81d28f1 | ||
|
|
5c105cb746 | ||
|
|
e1370e8f3c | ||
|
|
9fbcc2b5bb | ||
|
|
53d77b25ed | ||
|
|
e7089d4c2f | ||
|
|
6d3cb0ea2e | ||
|
|
46c6f5e615 | ||
|
|
c29c3179a0 | ||
|
|
3e84429a3a | ||
|
|
904b6db628 | ||
|
|
af4e94058a | ||
|
|
91b77b6d63 | ||
|
|
5a0c7c34bf | ||
|
|
3267b4d6ca | ||
|
|
9b302b856a | ||
|
|
4e696aed82 | ||
|
|
425c7b947f | ||
|
|
d4f9429fc1 | ||
|
|
161b43e85d | ||
|
|
d585e8f5a7 | ||
|
|
060e7cdf52 | ||
|
|
6fa002948e | ||
|
|
bbd4e6c8ba | ||
|
|
92cf945e10 | ||
|
|
cc0f55c245 | ||
|
|
22f27c4255 | ||
|
|
14a888bf43 | ||
|
|
f6fddc9436 | ||
|
|
f581e91f19 | ||
|
|
fb72dfcdee | ||
|
|
925813b14c | ||
|
|
abd410fe62 | ||
|
|
875282e9ec | ||
|
|
6afda28367 | ||
|
|
0721b24250 | ||
|
|
10b6bce8a2 | ||
|
|
0101444c5d | ||
|
|
128883b8a3 | ||
|
|
cc75b75d4e | ||
|
|
dea6cd81c7 | ||
|
|
2f53ab08b5 | ||
|
|
d7f3d1f19d | ||
|
|
a4517fcb9b | ||
|
|
4a12cf0922 | ||
|
|
0ee91b0280 | ||
|
|
e131890f54 | ||
|
|
bd069aea49 | ||
|
|
42d4f94fec | ||
|
|
3af2848275 | ||
|
|
8b1e5d3db7 | ||
|
|
57ed903a48 | ||
|
|
6093219ce9 | ||
|
|
f9f34911b1 | ||
|
|
494328541a | ||
|
|
fd2c7c888c | ||
|
|
24c09f2041 | ||
|
|
a1e6d90e31 | ||
|
|
de33fedea4 | ||
|
|
9f89104f94 | ||
|
|
f0e88220c6 | ||
|
|
6d7e16d6e1 | ||
|
|
d7d38fddb8 | ||
|
|
527a5bc6b5 | ||
|
|
9644dcb9b4 | ||
|
|
f4861482f1 | ||
|
|
dc73bb3caf | ||
|
|
bcbfc1758e | ||
|
|
e65dcf51b0 | ||
|
|
1326701440 | ||
|
|
c32e45f686 | ||
|
|
0160684004 | ||
|
|
734b920fde | ||
|
|
174e703b4c | ||
|
|
36336a3a57 | ||
|
|
c10a4346a9 | ||
|
|
db55496fc7 | ||
|
|
9a2efd0ef0 | ||
|
|
579af09816 | ||
|
|
d39614713d | ||
|
|
34af1e258c | ||
|
|
3ff68dbc7b | ||
|
|
8952ac9af0 | ||
|
|
7799a1e260 | ||
|
|
353927e6d2 | ||
|
|
d40db1ddea | ||
|
|
f85a9e174c | ||
|
|
838e14af60 | ||
|
|
1d84c5cad8 | ||
|
|
3be2259068 | ||
|
|
acc4cad082 | ||
|
|
10a1788754 | ||
|
|
f1c1059ff8 | ||
|
|
690c8ea2c9 | ||
|
|
1c8d1bc9ff | ||
|
|
a9416d89e3 | ||
|
|
9e33ba46af | ||
|
|
a0c4726af3 | ||
|
|
a0b3c0a5a4 | ||
|
|
9978957e6c | ||
|
|
f155611d29 | ||
|
|
01b3e98358 | ||
|
|
3a50da1b53 | ||
|
|
a32fd5e665 | ||
|
|
b6a4f5f518 | ||
|
|
e799e80843 | ||
|
|
63cb7a75b3 | ||
|
|
922e95756a | ||
|
|
b49f0d211b | ||
|
|
761fbf7757 | ||
|
|
4ee052e71e | ||
|
|
0274f3c2ac | ||
|
|
ae13f1aa23 | ||
|
|
904405ebee | ||
|
|
a059739210 | ||
|
|
25156bb56c | ||
|
|
d62761b3a8 | ||
|
|
817c0a5672 | ||
|
|
c06a970987 | ||
|
|
baf3a12009 | ||
|
|
0ec2468dce | ||
|
|
0cfc9fd1fa | ||
|
|
a2de9a3846 | ||
|
|
88059a2cc5 | ||
|
|
635d797b2e | ||
|
|
e635c45ec6 | ||
|
|
594ae61192 | ||
|
|
2945f688fa | ||
|
|
d86cca2e26 | ||
|
|
fca315ee1f | ||
|
|
a12f140333 | ||
|
|
ad7e4488ef | ||
|
|
df4e954f8a | ||
|
|
63f344bde6 | ||
|
|
0b8d9d11e2 | ||
|
|
57801fde1f | ||
|
|
c87f4e68f7 | ||
|
|
27762492d7 | ||
|
|
da7c408686 | ||
|
|
0c0a98605d | ||
|
|
108226bcdc | ||
|
|
8b400d4f2c | ||
|
|
d838e7b44d | ||
|
|
7b157fa8e5 | ||
|
|
d19a59a364 | ||
|
|
b95a351222 | ||
|
|
1038acd2ea | ||
|
|
54fc052e47 | ||
|
|
62bac800af | ||
|
|
aa2b36d5cc | ||
|
|
b5f482bb50 | ||
|
|
649c104d29 | ||
|
|
41368c85bf | ||
|
|
af59178318 | ||
|
|
5149623b57 | ||
|
|
205c74b5d8 | ||
|
|
5116bfa79c | ||
|
|
69767126aa | ||
|
|
5af389ae3f | ||
|
|
f711f4d8a8 | ||
|
|
8b80efd537 | ||
|
|
7b83450a9c | ||
|
|
e3011a1cb0 | ||
|
|
7ff8dcfb78 | ||
|
|
551ed202be | ||
|
|
6f11913359 | ||
|
|
d91a78da7d | ||
|
|
02fdd058ec | ||
|
|
08148afac7 | ||
|
|
f037ffe107 | ||
|
|
fa6ba3110b | ||
|
|
6f82ddc032 | ||
|
|
ee3267388f | ||
|
|
f97a1fcedf | ||
|
|
315d830357 | ||
|
|
00caeae914 | ||
|
|
199835b671 | ||
|
|
ce2225d355 | ||
|
|
b4f1f94bcc | ||
|
|
90cee6b802 | ||
|
|
2a883bb958 | ||
|
|
607f77d432 | ||
|
|
c254b33753 | ||
|
|
59f3848056 | ||
|
|
69aa002c83 | ||
|
|
0b57cc08a7 | ||
|
|
8630d1ab12 | ||
|
|
591aa9eaa5 | ||
|
|
f82fa42cba | ||
|
|
aa441c88db | ||
|
|
17ee22da72 | ||
|
|
a9957fb46d | ||
|
|
f5c87fdd4c | ||
|
|
23467a2248 | ||
|
|
9fa93e40cb | ||
|
|
f4b852d2dd | ||
|
|
f698b7fa9f | ||
|
|
b21afa648d | ||
|
|
b9575cc869 | ||
|
|
3ea91bc4ad | ||
|
|
b3dce5fdb0 | ||
|
|
28ad8b8cd5 | ||
|
|
37d4ef770c | ||
|
|
ba24e40512 | ||
|
|
8097593f5e | ||
|
|
23ccd69b5e | ||
|
|
5e0d6d77b9 | ||
|
|
a06393f520 | ||
|
|
549ffcefc0 | ||
|
|
c8721e8000 | ||
|
|
03882367da | ||
|
|
4d700d113d | ||
|
|
17bdd2a1d2 | ||
|
|
80a68012a2 | ||
|
|
3742906f75 | ||
|
|
ae90edcdb5 | ||
|
|
9e76aadb0f | ||
|
|
043544d7ec | ||
|
|
2bf7d1dddc | ||
|
|
e1741118ce | ||
|
|
58fb3f7f2d | ||
|
|
48e92a7e9b | ||
|
|
5bf16da09d | ||
|
|
23ca3dd665 | ||
|
|
2caff25fa2 | ||
|
|
37f835be8c | ||
|
|
a6c1f2f776 | ||
|
|
acf0dfc38c | ||
|
|
67f13831ce | ||
|
|
d50e65af20 | ||
|
|
b8d74b74a6 | ||
|
|
802a28d759 | ||
|
|
c85d43b349 | ||
|
|
64ad491197 | ||
|
|
ddd8e719ef | ||
|
|
e51f7a51cc | ||
|
|
e66f5d488b | ||
|
|
b386cc83a7 | ||
|
|
09481e09b6 | ||
|
|
6913bf1a46 | ||
|
|
e6c87ff00b | ||
|
|
5f41cf3c52 | ||
|
|
9edbe2e589 | ||
|
|
b6876712f0 | ||
|
|
5b4c183466 | ||
|
|
393238f47c | ||
|
|
aea526f69d | ||
|
|
13cf1cc004 | ||
|
|
1766a8bb2c | ||
|
|
9a5732ab70 | ||
|
|
2923ca1356 | ||
|
|
96f0083384 | ||
|
|
febfc396e3 | ||
|
|
29735a807b | ||
|
|
eb36f64676 | ||
|
|
4e01970d69 | ||
|
|
e5713087e3 | ||
|
|
b40fc7ff18 | ||
|
|
06ad2b7972 | ||
|
|
14eeb4451c | ||
|
|
ead67adeb8 | ||
|
|
c2d70a5107 | ||
|
|
90dffc975a | ||
|
|
e5ba7caddc | ||
|
|
ec6cee1389 | ||
|
|
1d16a19373 | ||
|
|
04c90b4f07 | ||
|
|
747cbb8e09 | ||
|
|
834487e548 | ||
|
|
e1b6b54209 | ||
|
|
6366d60e4c | ||
|
|
fabea36682 | ||
|
|
991332a809 | ||
|
|
85537a99e8 | ||
|
|
717c90c58b | ||
|
|
ccb52e0acd | ||
|
|
d84b30c071 | ||
|
|
5ae0afe1fe | ||
|
|
d250e503b0 | ||
|
|
afb0ae3d03 | ||
|
|
1a3f0bed47 | ||
|
|
1e280fb7e1 | ||
|
|
6feac55380 | ||
|
|
93d8eac037 | ||
|
|
a11f99be3d | ||
|
|
da17639309 | ||
|
|
10301aa742 | ||
|
|
2148d50393 | ||
|
|
12fb2a4ec5 | ||
|
|
8085e5b85c | ||
|
|
4ba310ec16 | ||
|
|
865c56f400 | ||
|
|
c510e73256 | ||
|
|
73638129bc | ||
|
|
1a7a79d504 | ||
|
|
d3268e4a72 | ||
|
|
15a93014a5 | ||
|
|
e7735329bc | ||
|
|
3e222c68eb | ||
|
|
a596bd9011 | ||
|
|
21a49710a8 | ||
|
|
ce6fdb2558 | ||
|
|
0baee848a6 | ||
|
|
6f304bc9e6 | ||
|
|
1ca0dfffa0 | ||
|
|
1420084f5e | ||
|
|
3e03474437 | ||
|
|
95366e4d1b | ||
|
|
df1775a1e6 | ||
|
|
30ccea18ab | ||
|
|
4cd90d74ad | ||
|
|
7f1214688a | ||
|
|
aa89d0d156 | ||
|
|
787cd94362 | ||
|
|
ec61a7fc51 | ||
|
|
9b627534f5 | ||
|
|
400a3707b2 | ||
|
|
38a5676b37 | ||
|
|
f00cfa9108 | ||
|
|
afa24722b2 | ||
|
|
ea5cec53bc | ||
|
|
61dc649c70 | ||
|
|
b20824e16c | ||
|
|
39330fdce3 | ||
|
|
6b725a8ef7 | ||
|
|
cbcdeb2b43 | ||
|
|
4351610eca | ||
|
|
935d826a21 | ||
|
|
a8c8137ade | ||
|
|
7b33e1fba8 | ||
|
|
ade7bba97b | ||
|
|
08dd321311 | ||
|
|
67961180c9 | ||
|
|
1093892ede | ||
|
|
ef05fa4905 | ||
|
|
6a99a4f1ae | ||
|
|
4895f396a2 | ||
|
|
c3dffc5909 | ||
|
|
af73e5993d | ||
|
|
dfec1cbb02 | ||
|
|
8d8f7b2524 | ||
|
|
db7b81587f | ||
|
|
31bb744ba7 | ||
|
|
f4b349162f | ||
|
|
7f8adf8f03 | ||
|
|
1f4bb8a224 | ||
|
|
0c3dc8a6e9 | ||
|
|
1f15cf54af | ||
|
|
c96ba30018 | ||
|
|
ffea61917d | ||
|
|
f5c11b8faf | ||
|
|
48b4b23204 | ||
|
|
9df78c8ac8 | ||
|
|
450bfe2e17 | ||
|
|
c79eb36a7a | ||
|
|
a58b3a42db | ||
|
|
e344958224 | ||
|
|
05c4a6c682 | ||
|
|
b2aec6d6a7 | ||
|
|
09c4609b6c | ||
|
|
f6d2aa7aae | ||
|
|
92facf58f7 | ||
|
|
15c36c5a84 | ||
|
|
cea0543e98 | ||
|
|
c0bbe77788 | ||
|
|
9f8cbe140d | ||
|
|
d0cf550b51 | ||
|
|
a86725480f | ||
|
|
9ad22e1f6d | ||
|
|
a266bcbae7 | ||
|
|
1ba210fe77 | ||
|
|
7898395359 | ||
|
|
aeb732c2f6 | ||
|
|
b665dce383 | ||
|
|
f349f124d8 | ||
|
|
8212d7a00e | ||
|
|
36bcb1b26e | ||
|
|
8d6fe2be99 | ||
|
|
d9571c70f2 | ||
|
|
babbca48f8 | ||
|
|
8c4e2e57f9 | ||
|
|
8308651f44 | ||
|
|
ad881bd46a | ||
|
|
c037eb2d24 | ||
|
|
8b4353deba | ||
|
|
a2be0d35fb | ||
|
|
1909bdc702 | ||
|
|
63909defaf | ||
|
|
8544984b17 | ||
|
|
563984c0df | ||
|
|
6500ee5fc9 | ||
|
|
2a9c138a23 | ||
|
|
61d8fa02d4 | ||
|
|
0fac2187f0 | ||
|
|
1db61be860 | ||
|
|
06a0dbd0f2 | ||
|
|
47c6daf0cc | ||
|
|
bcdf502ce6 | ||
|
|
f9e2f4931a | ||
|
|
8929d15df0 | ||
|
|
60d6a47bdb | ||
|
|
2f529535b1 | ||
|
|
90c9eae283 | ||
|
|
added6105b | ||
|
|
2df39b5e24 | ||
|
|
3477dd9400 | ||
|
|
5282551f3d | ||
|
|
dcadaaf29b | ||
|
|
e9d6baa6ba | ||
|
|
6ae052a7a1 | ||
|
|
9f750c2516 | ||
|
|
1fe46834f2 | ||
|
|
3db85c7d37 | ||
|
|
1e4f1b8891 | ||
|
|
0fcd6d40ee | ||
|
|
aaa4ffe789 | ||
|
|
8c4720d0cb | ||
|
|
94b97f6097 | ||
|
|
85800d96c8 | ||
|
|
3b4c06111a | ||
|
|
548d695a82 | ||
|
|
c986a4b88b | ||
|
|
09940ccf8d | ||
|
|
cfc323862f | ||
|
|
d8cc867099 | ||
|
|
dce8a1dff9 | ||
|
|
5bc9e014c2 | ||
|
|
b0c9ba05f3 | ||
|
|
8a2876fca9 | ||
|
|
00d5f3b769 | ||
|
|
17f39ec6a0 | ||
|
|
498ffe8a71 | ||
|
|
858f0f2650 | ||
|
|
66ea2d5d71 | ||
|
|
3dd5b5d835 | ||
|
|
9127b1bbc6 | ||
|
|
1657bcf97d | ||
|
|
428db2f8f4 | ||
|
|
a8fa9b5e58 | ||
|
|
4cc59d9fbd | ||
|
|
c50306709b | ||
|
|
37d0bc2f14 | ||
|
|
2fda0454e3 | ||
|
|
be19af62d9 | ||
|
|
f915eb2a20 | ||
|
|
2bc1236a2c | ||
|
|
9db1924268 | ||
|
|
7a9f220290 | ||
|
|
8145387f77 | ||
|
|
063440e735 | ||
|
|
6724de09c9 | ||
|
|
f379fd0f8c | ||
|
|
34a3387830 | ||
|
|
809cc1f234 | ||
|
|
12200a74ff | ||
|
|
2643ea9066 | ||
|
|
840df89ca6 | ||
|
|
0404b020e6 | ||
|
|
fda41817e9 | ||
|
|
f48cabcc0a | ||
|
|
f123a905d5 | ||
|
|
9b7fbfd513 | ||
|
|
e21b4d4236 | ||
|
|
9ec6911005 | ||
|
|
bfc178faf3 | ||
|
|
c4c93f881d | ||
|
|
d7f9e17bcb | ||
|
|
13706c4f64 | ||
|
|
f2f4b26c35 | ||
|
|
1b7b9da07c | ||
|
|
2817306659 | ||
|
|
5f587c2104 | ||
|
|
f5670c39da | ||
|
|
c0105d135c | ||
|
|
f1a9814faa | ||
|
|
8f0e7512be | ||
|
|
7d49209f79 | ||
|
|
b2e285c2c7 | ||
|
|
54020250dc | ||
|
|
01acbb970a | ||
|
|
36cad35d46 | ||
|
|
41c9c84139 | ||
|
|
9e9ca521b0 | ||
|
|
1927862871 | ||
|
|
c80eaf8550 | ||
|
|
9e6a35bac3 | ||
|
|
62ffcf94a6 | ||
|
|
2b77920dcd | ||
|
|
38b7e4d4a4 | ||
|
|
3e4d4f04ef | ||
|
|
f6f3d17383 | ||
|
|
d5f6b76ec5 | ||
|
|
ae75be56ea | ||
|
|
50b90c4814 | ||
|
|
6eddb5f30f | ||
|
|
cee8f3a4b6 | ||
|
|
a2e5733be6 | ||
|
|
5075657c02 | ||
|
|
0450b1ace2 | ||
|
|
0ebf1da05d | ||
|
|
07ad3edbc2 | ||
|
|
b40ed2a7f3 | ||
|
|
29b074607c | ||
|
|
258a157e44 | ||
|
|
92d9a1f9f2 | ||
|
|
e5009a58df | ||
|
|
35a1ce4903 | ||
|
|
7c4c627ee9 | ||
|
|
b7575ec01d | ||
|
|
a0351d6f99 |
68
.github/ISSUE_TEMPLATE/bug.yml
vendored
Normal file
@@ -0,0 +1,68 @@
|
||||
name: Bug
|
||||
description: File a bug report/issue
|
||||
title: "[Bug]: "
|
||||
labels: ["bug", "triage"]
|
||||
body:
|
||||
- type: checkboxes
|
||||
attributes:
|
||||
label: Is there an existing issue for this?
|
||||
description: Please search to see if an issue already exists for the bug you encountered.
|
||||
options:
|
||||
- label: I have searched the existing issues
|
||||
required: true
|
||||
- type: dropdown
|
||||
attributes:
|
||||
label: Platform
|
||||
description: Multiple selections are possible.
|
||||
multiple: true
|
||||
options:
|
||||
- Linux
|
||||
- Mac
|
||||
- Windows
|
||||
- Android
|
||||
- iOS
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
attributes:
|
||||
label: OS version
|
||||
description: Specify the OS version
|
||||
placeholder: ex. Android 12, Ubuntu 20.04
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
attributes:
|
||||
label: App version
|
||||
description: Specify the SimpleX version
|
||||
placeholder: ex. 4.3.2
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Current Behavior
|
||||
description: A concise description of what you're experiencing.
|
||||
placeholder: Bug happened!
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Expected Behavior
|
||||
description: A concise description of what you expected to happen.
|
||||
placeholder: No bug should happen!
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Steps To Reproduce
|
||||
description: Steps to reproduce the behavior.
|
||||
placeholder: |
|
||||
1. Go to ...
|
||||
3. Click on ...
|
||||
4. See error...
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Relevant log output
|
||||
description: Please copy and paste any relevant log output. This will be automatically formatted into code, so no need for backticks.
|
||||
render: shell
|
||||
1
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
@@ -0,0 +1 @@
|
||||
blank_issues_enabled: true
|
||||
40
.github/ISSUE_TEMPLATE/feature.yml
vendored
Normal file
@@ -0,0 +1,40 @@
|
||||
name: Feature
|
||||
description: Suggest your feature
|
||||
title: "[Feature]: "
|
||||
labels: ["enhancement", "triage"]
|
||||
body:
|
||||
- type: checkboxes
|
||||
attributes:
|
||||
label: Is there an existing issue for this?
|
||||
description: Please search to see if an issue already exists for the bug you encountered.
|
||||
options:
|
||||
- label: I have searched the existing issues
|
||||
required: true
|
||||
- type: dropdown
|
||||
attributes:
|
||||
label: Platform
|
||||
description: Multiple selections are possible. If selected input is "all", this considered to be a general feature.
|
||||
multiple: true
|
||||
options:
|
||||
- Linux
|
||||
- Mac
|
||||
- Windows
|
||||
- Android
|
||||
- iOS
|
||||
- all
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
attributes:
|
||||
label: App version
|
||||
description: Specify the SimpleX version
|
||||
placeholder: ex. 4.3.2
|
||||
validations:
|
||||
required: false
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Feature
|
||||
description: Describe the feature you would like to see added
|
||||
placeholder: SimpleX Chat should make me coffee!
|
||||
validations:
|
||||
required: true
|
||||
16
.github/ISSUE_TEMPLATE/question.yml
vendored
Normal file
@@ -0,0 +1,16 @@
|
||||
name: Question
|
||||
description: Ask your question
|
||||
title: "[Q]: "
|
||||
labels: ["question", "triage"]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Generally, we encourage you to ask questions in our [official group](https://simplex.chat/invitation/#/?v=1&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2FK1rslx-m5bpXVIdMZg9NLUZ_8JBm8xTt%23MCowBQYDK2VuAyEALDeVe-sG8mRY22LsXlPgiwTNs9dbiLrNuA7f3ZMAJ2w%3Dsimplex:/contact#/?v=1&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2FK1rslx-m5bpXVIdMZg9NLUZ_8JBm8xTt%23MCowBQYDK2VuAyEALDeVe-sG8mRY22LsXlPgiwTNs9dbiLrNuA7f3ZMAJ2w%3D), but you can do it anyway :)
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Question
|
||||
description: Please ask your question in plain english.
|
||||
placeholder: Is SimpleX - chat?
|
||||
validations:
|
||||
required: true
|
||||
40
.github/workflows/build.yml
vendored
@@ -16,11 +16,11 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Clone project
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Build changelog
|
||||
id: build_changelog
|
||||
uses: mikepenz/release-changelog-builder-action@v1
|
||||
uses: mikepenz/release-changelog-builder-action@v4
|
||||
with:
|
||||
configuration: .github/changelog_conf.json
|
||||
failOnError: true
|
||||
@@ -52,9 +52,9 @@ jobs:
|
||||
- os: ubuntu-20.04
|
||||
cache_path: ~/.cabal/store
|
||||
asset_name: simplex-chat-ubuntu-20_04-x86-64
|
||||
- os: ubuntu-18.04
|
||||
- os: ubuntu-22.04
|
||||
cache_path: ~/.cabal/store
|
||||
asset_name: simplex-chat-ubuntu-18_04-x86-64
|
||||
asset_name: simplex-chat-ubuntu-22_04-x86-64
|
||||
- os: macos-latest
|
||||
cache_path: ~/.cabal/store
|
||||
asset_name: simplex-chat-macos-x86-64
|
||||
@@ -62,17 +62,25 @@ jobs:
|
||||
cache_path: C:/cabal
|
||||
asset_name: simplex-chat-windows-x86-64
|
||||
steps:
|
||||
- name: Configure pagefile (Windows)
|
||||
if: matrix.os == 'windows-latest'
|
||||
uses: al-cheb/configure-pagefile-action@v1.3
|
||||
with:
|
||||
minimum-size: 16GB
|
||||
maximum-size: 16GB
|
||||
disk-root: "C:"
|
||||
|
||||
- name: Clone project
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Setup Haskell
|
||||
uses: haskell/actions/setup@v1
|
||||
uses: haskell/actions/setup@v2
|
||||
with:
|
||||
ghc-version: "8.10.7"
|
||||
cabal-version: "latest"
|
||||
|
||||
- name: Cache dependencies
|
||||
uses: actions/cache@v2
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: |
|
||||
${{ matrix.cache_path }}
|
||||
@@ -91,8 +99,12 @@ jobs:
|
||||
echo " extra-lib-dirs: /usr/local/opt/openssl@1.1/lib" >> cabal.project.local
|
||||
echo " flags: +openssl" >> cabal.project.local
|
||||
|
||||
- name: Install pkg-config for Mac
|
||||
if: matrix.os == 'macos-latest'
|
||||
run: brew install pkg-config
|
||||
|
||||
- name: Unix prepare cabal.project.local for Ubuntu
|
||||
if: matrix.os == 'ubuntu-20.04' || matrix.os == 'ubuntu-18.04'
|
||||
if: matrix.os == 'ubuntu-20.04' || matrix.os == 'ubuntu-22.04'
|
||||
shell: bash
|
||||
run: |
|
||||
echo "ignore-project: False" >> cabal.project.local
|
||||
@@ -107,12 +119,6 @@ jobs:
|
||||
cabal build --enable-tests
|
||||
echo "::set-output name=bin_path::$(cabal list-bin simplex-chat)"
|
||||
|
||||
- name: Unix test
|
||||
if: matrix.os != 'windows-latest' && matrix.os != 'ubuntu-20.04'
|
||||
timeout-minutes: 20
|
||||
shell: bash
|
||||
run: cabal test --test-show-details=direct
|
||||
|
||||
- name: Unix upload binary to release
|
||||
if: startsWith(github.ref, 'refs/tags/v') && matrix.os != 'windows-latest'
|
||||
uses: svenstaro/upload-release-action@v2
|
||||
@@ -122,6 +128,12 @@ jobs:
|
||||
asset_name: ${{ matrix.asset_name }}
|
||||
tag: ${{ github.ref }}
|
||||
|
||||
- name: Unix test
|
||||
if: matrix.os != 'windows-latest'
|
||||
timeout-minutes: 30
|
||||
shell: bash
|
||||
run: cabal test --test-show-details=direct
|
||||
|
||||
# Unix /
|
||||
|
||||
# / Windows
|
||||
|
||||
2
.github/workflows/web.yml
vendored
@@ -17,7 +17,7 @@ jobs:
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [12.x]
|
||||
node-version: [16.x]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
5
.gitignore
vendored
@@ -45,10 +45,12 @@ tests/tmp
|
||||
tests/tmp*
|
||||
logs/
|
||||
|
||||
|
||||
*.devcontainer
|
||||
# for website
|
||||
website/node_modules/
|
||||
website/src/blog/
|
||||
website/src/docs/
|
||||
website/translations.json
|
||||
website/src/img/images/
|
||||
website/src/images/
|
||||
# Generated files
|
||||
@@ -73,3 +75,4 @@ website/package-lock.json
|
||||
# Ignore test files
|
||||
website/.cache
|
||||
website/test/stubs-layout-cache/_includes/*.js
|
||||
apps/android/app/release
|
||||
|
||||
277
README.md
@@ -1,15 +1,29 @@
|
||||
| Updated 07.02.2023 | Languages: EN, [FR](/docs/lang/fr/README.md) |
|
||||
|
||||
<img src="images/simplex-chat-logo.svg" alt="SimpleX logo" width="100%">
|
||||
|
||||
# SimpleX - the first messaging platform that has no user identifiers of any kind - 100% private by design!
|
||||
|
||||
[](https://github.com/simplex-chat/simplex-chat/actions/workflows/build.yml)
|
||||
[](https://github.com/simplex-chat/simplex-chat/releases)
|
||||
[](https://github.com/simplex-chat/simplex-chat/releases)
|
||||
[](https://www.reddit.com/r/SimpleXChat)
|
||||
[](https://mastodon.social/@simplex)
|
||||
|
||||
| 30/03/2023 | EN, [FR](/docs/lang/fr/README.md), [CZ](/docs/lang/cs/README.md) |
|
||||
|
||||
<img src="images/simplex-chat-logo.svg" alt="SimpleX logo" width="100%">
|
||||
|
||||
# SimpleX - the first messaging platform that has no user identifiers of any kind - 100% private by design!
|
||||
|
||||
[<img src="./images/trail-of-bits.jpg" height="100">](http://simplex.chat/blog/20221108-simplex-chat-v4.2-security-audit-new-website.html) [<img src="./images/privacy-guides.jpg" height="80">](https://www.privacyguides.org/en/real-time-communication/#simplex-chat) [<img src="./images/kuketz-blog.jpg" height="80">](https://www.kuketz-blog.de/simplex-eindruecke-vom-messenger-ohne-identifier/)
|
||||
|
||||
## Welcome to SimpleX Chat!
|
||||
|
||||
1. 📲 [Install the app](#install-the-app).
|
||||
2. ↔️ [Connect to the team](#connect-to-the-team-via-the-app) and [join user groups](#join-user-groups).
|
||||
3. 🤝 [Make a private connection](#make-a-private-connection) with a friend.
|
||||
4. 🔤 [Help translating SimpleX Chat](#help-translating-simplex-chat).
|
||||
5. ⚡️ [Contribute](#contribute) and [help us with donations](#help-us-with-donations).
|
||||
|
||||
[Learn more about SimpleX Chat](#contents).
|
||||
|
||||
## Install the app
|
||||
|
||||
[<img src="https://github.com/simplex-chat/.github/blob/master/profile/images/apple_store.svg" alt="iOS app" height="42">](https://apps.apple.com/us/app/simplex-chat/id1605771084)
|
||||
|
||||
[](https://play.google.com/store/apps/details?id=chat.simplex.app)
|
||||
@@ -24,9 +38,119 @@
|
||||
- 🔐 Double ratchet end-to-end encryption, with additional encryption layer.
|
||||
- 📱 Mobile apps for Android ([Google Play](https://play.google.com/store/apps/details?id=chat.simplex.app), [APK](https://github.com/simplex-chat/simplex-chat/releases/latest/download/simplex.apk)) and [iOS](https://apps.apple.com/us/app/simplex-chat/id1605771084).
|
||||
- 🚀 [TestFlight preview for iOS](https://testflight.apple.com/join/DWuT2LQu) with the new features 1-2 weeks earlier - **limited to 10,000 users**!
|
||||
- 🖥 Available as a terminal (console) app / CLI on Linux, MacOS, Windows.
|
||||
- 🖥 Available as a terminal (console) [app / CLI](#zap-quick-installation-of-a-terminal-app) on Linux, MacOS, Windows.
|
||||
|
||||
**NEW**: Security audit by [Trail of Bits](https://www.trailofbits.com/about), the [new website](https://simplex.chat) and v4.2 released! [See the announcement](./blog/20221108-simplex-chat-v4.2-security-audit-new-website.md)
|
||||
## Connect to the team via the app
|
||||
|
||||
- to ask any questions
|
||||
- to suggest any improvements
|
||||
- to share anything relevant
|
||||
|
||||
## Join user groups
|
||||
|
||||
**Please note**: The groups below are created for the users to be able to ask questions, make suggestions and ask questions about SimpleX Chat only.
|
||||
|
||||
You also can:
|
||||
- criticize the app, and make comparisons with other messengers.
|
||||
- share new messengers you think could be interesting for privacy, as long as you don't spam.
|
||||
- share some privacy related publications, infrequently.
|
||||
- having preliminary approved with the admin in direct message, share the link to a group you created.
|
||||
|
||||
You must:
|
||||
- be polite to other users
|
||||
- avoid spam (too frequent messages, even if they are relevant)
|
||||
- avoid any personal attacks or hostility.
|
||||
- avoid sharing any content that is not relevant to the above (that includes, but is not limited to, discussing politics or any aspects of society other than privacy, security, technology and communications, sharing any content that may be found offensive by other users, etc.).
|
||||
|
||||
Messages not following these rules will be deleted, the right to send messages may be revoked, and the access to the new members to the group may be temporarily restricted, to prevent re-joining under a different name - our imperfect group moderation does not have a better solution at the moment.
|
||||
|
||||
You can join an English-speaking users group if you want to ask any questions: [#SimpleX-Group-4](https://simplex.chat/contact#/?v=1-2&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2Fw2GlucRXtRVgYnbt_9ZP-kmt76DekxxS%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEA0tJhTyMGUxznwmjb7aT24P1I1Wry_iURTuhOFlMb1Eo%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion&data=%7B%22type%22%3A%22group%22%2C%22groupLinkId%22%3A%22WoPxjFqGEDlVazECOSi2dg%3D%3D%22%7D)
|
||||
|
||||
There is also a group [#simplex-devs](https://simplex.chat/contact#/?v=1-2&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2F6eHqy7uAbZPOcA6qBtrQgQquVlt4Ll91%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAqV_pg3FF00L98aCXp4D3bOs4Sxv_UmSd-gb0juVoQVs%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion&data=%7B%22type%22%3A%22group%22%2C%22groupLinkId%22%3A%22XonlixcHBIb2ijCehbZoiw%3D%3D%22%7D) for developers who build on SimpleX platform:
|
||||
|
||||
- chat bots and automations
|
||||
- integrations with other apps
|
||||
- social apps and services
|
||||
- etc.
|
||||
|
||||
There are groups in other languages, that we have the apps interface translated into. These groups are for testing, and asking questions to other SimpleX Chat users:
|
||||
|
||||
[\#SimpleX-DE](https://simplex.chat/contact#/?v=1-2&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2FkIEl7OQzcp-J6aDmjdlQbRJwqkcZE7XR%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAR16PCu02MobRmKAsjzhDWMZcWP9hS8l5AUZi-Gs8z18%253D%26srv%3Dbylepyau3ty4czmn77q4fglvperknl4bi2eb2fdy2bh4jxtf32kf73yd.onion&data=%7B%22type%22%3A%22group%22%2C%22groupLinkId%22%3A%22puYPMCQt11yPUvgmI5jCiw%3D%3D%22%7D) (German-speaking), [\#SimpleX-ES](https://simplex.chat/contact#/?v=1-2&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2FaJ8O1O8A8GbeoaHTo_V8dcefaCl7ouPb%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEA034qWTA3sWcTsi6aWhNf9BA34vKVCFaEBdP2R66z6Ao%253D%26srv%3Dbylepyau3ty4czmn77q4fglvperknl4bi2eb2fdy2bh4jxtf32kf73yd.onion&data=%7B%22type%22%3A%22group%22%2C%22groupLinkId%22%3A%22wiZ1v_wNjLPlT-nCSB-bRA%3D%3D%22%7D) (Spanish-speaking), [\#SimpleX-FR](https://simplex.chat/contact#/?v=1-2&smp=smp%3A%2F%2Fhpq7_4gGJiilmz5Rf-CswuU5kZGkm_zOIooSw6yALRg%3D%40smp5.simplex.im%2FvIHQDxTor53nwnWWTy5cHNwQQAdWN5Hw%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAPdgK1eBnETmgiqEQufbUkydKBJafoRx4iRrtrC2NAGc%253D%26srv%3Djjbyvoemxysm7qxap7m5d5m35jzv5qq6gnlv7s4rsn7tdwwmuqciwpid.onion&data=%7B%22type%22%3A%22group%22%2C%22groupLinkId%22%3A%221FyUryBPza-1ZFFE80Ekbg%3D%3D%22%7D) (French-speaking), [\#SimpleX-RU](https://simplex.chat/contact#/?v=1-2&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2FXZyt3hJmWsycpN7Dqve_wbrAqb6myk1R%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAMFVIoytozTEa_QXOgoZFq_oe0IwZBYKvW50trSFXzXo%253D%26srv%3Dbylepyau3ty4czmn77q4fglvperknl4bi2eb2fdy2bh4jxtf32kf73yd.onion&data=%7B%22type%22%3A%22group%22%2C%22groupLinkId%22%3A%22xz05ngjA3pNIxLZ32a8Vxg%3D%3D%22%7D) (Russian-speaking), [\#SimpleX-IT](https://simplex.chat/contact#/?v=1-2&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2F0weR-ZgDUl7ruOtI_8TZwEsnJP6UiImA%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAq4PSThO9Fvb5ydF48wB0yNbpzCbuQJCW3vZ9BGUfcxk%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion&data=%7B%22type%22%3A%22group%22%2C%22groupLinkId%22%3A%22e-iceLA0SctC62eARgYDWg%3D%3D%22%7D) (Italian-speaking).
|
||||
|
||||
You can join either by opening these links in the app or by opening them in a desktop browser and scanning the QR code.
|
||||
|
||||
## Make a private connection
|
||||
|
||||
You need to share a link with your friend or scan a QR code from their phone, in person or during a video call, to make a connection and start messaging.
|
||||
|
||||
The channel through which you share the link does not have to be secure - it is enough that you can confirm who sent you the message and that your SimpleX connection is established.
|
||||
|
||||
<img src="https://github.com/simplex-chat/.github/blob/master/profile/images/app1.png" alt="Make a private connection" height="360"> <img src="https://github.com/simplex-chat/.github/blob/master/profile/images/arrow.png" height="360"> <img src="https://github.com/simplex-chat/.github/blob/master/profile/images/app2.png" alt="Conversation" height="360"> <img src="https://github.com/simplex-chat/.github/blob/master/profile/images/arrow.png" height="360"> <img src="https://github.com/simplex-chat/.github/blob/master/profile/images/app3.png" alt="Video call" height="360">
|
||||
|
||||
After you connect, you can [verify connection security code](./blog/20230103-simplex-chat-v4.4-disappearing-messages.md#connection-security-verification).
|
||||
|
||||
## User guide (NEW)
|
||||
|
||||
Read about the app features and settings in the new [User guide](./docs/guide/README.md).
|
||||
|
||||
## Help translating SimpleX Chat
|
||||
|
||||
Thanks to our users and [Weblate](https://hosted.weblate.org/engage/simplex-chat/), SimpleX Chat apps, website and documents are translated to many other languages.
|
||||
|
||||
Join our translators to help SimpleX grow!
|
||||
|
||||
|locale|language |contributor|[Android](https://play.google.com/store/apps/details?id=chat.simplex.app) and [iOS](https://apps.apple.com/us/app/simplex-chat/id1605771084)|[website](https://simplex.chat)|Github docs|
|
||||
|:----:|:-------:|:---------:|:---------:|:---------:|:---------:|
|
||||
|🇬🇧 en|English | |✓|✓|✓|✓|
|
||||
|ar|العربية |[jermanuts](https://github.com/jermanuts)||[](https://hosted.weblate.org/projects/simplex-chat/website/ar/)||
|
||||
|🇨🇿 cs|Čeština |[zen0bit](https://github.com/zen0bit)|[](https://hosted.weblate.org/projects/simplex-chat/android/cs/)<br>[](https://hosted.weblate.org/projects/simplex-chat/ios/cs/)|[](https://hosted.weblate.org/projects/simplex-chat/website/cs/)|[✓](https://github.com/simplex-chat/simplex-chat/tree/master/docs/lang/cs)|
|
||||
|🇩🇪 de|Deutsch |[mlanp](https://github.com/mlanp)|[](https://hosted.weblate.org/projects/simplex-chat/android/de/)<br>[](https://hosted.weblate.org/projects/simplex-chat/ios/de/)|[](https://hosted.weblate.org/projects/simplex-chat/website/de/)||
|
||||
|🇪🇸 es|Español |[Mateyhv](https://github.com/Mateyhv)|[](https://hosted.weblate.org/projects/simplex-chat/android/es/)<br>[](https://hosted.weblate.org/projects/simplex-chat/ios/es/)|[](https://hosted.weblate.org/projects/simplex-chat/website/es/)||
|
||||
|🇫🇷 fr|Français |[ishi_sama](https://github.com/ishi-sama)|[](https://hosted.weblate.org/projects/simplex-chat/android/fr/)<br>[](https://hosted.weblate.org/projects/simplex-chat/ios/fr/)|[](https://hosted.weblate.org/projects/simplex-chat/website/fr/)|[✓](https://github.com/simplex-chat/simplex-chat/tree/master/docs/lang/fr)|
|
||||
|🇮🇹 it|Italiano |[unbranched](https://github.com/unbranched)|[](https://hosted.weblate.org/projects/simplex-chat/android/it/)<br>[](https://hosted.weblate.org/projects/simplex-chat/ios/it/)|[](https://hosted.weblate.org/projects/simplex-chat/website/it/)||
|
||||
|🇯🇵 ja|Japanese ||[](https://hosted.weblate.org/projects/simplex-chat/android/ja/)<br>[](https://hosted.weblate.org/projects/simplex-chat/ios/ja/)|||
|
||||
|🇳🇱 nl|Nederlands|[mika-nl](https://github.com/mika-nl)|[](https://hosted.weblate.org/projects/simplex-chat/android/nl/)<br>[](https://hosted.weblate.org/projects/simplex-chat/ios/nl/)|[](https://hosted.weblate.org/projects/simplex-chat/website/nl/)||
|
||||
|🇵🇱 pl|Polski |[BxOxSxS](https://github.com/BxOxSxS)|[](https://hosted.weblate.org/projects/simplex-chat/android/ru/)<br>[](https://hosted.weblate.org/projects/simplex-chat/ios/pl/)|||
|
||||
|🇧🇷 pt-BR|Português||[](https://hosted.weblate.org/projects/simplex-chat/android/pt_BR/)<br>-|[](https://hosted.weblate.org/projects/simplex-chat/website/pt_BR/)||
|
||||
|🇷🇺 ru|Русский ||[](https://hosted.weblate.org/projects/simplex-chat/android/ru/)<br>[](https://hosted.weblate.org/projects/simplex-chat/ios/ru/)|||
|
||||
|🇨🇳 zh-CHS|简体中文|[sith-on-mars](https://github.com/sith-on-mars)<br><br>[Float-hu](https://github.com/Float-hu)|[](https://hosted.weblate.org/projects/simplex-chat/android/zh_Hans/)<br>[](https://hosted.weblate.org/projects/simplex-chat/ios/zh_Hans/)<br> |<br><br>[](https://hosted.weblate.org/projects/simplex-chat/website/zh_Hans/)||
|
||||
|
||||
Languages in progress: Arabic, Japanese, Korean, Portuguese and [others](https://hosted.weblate.org/projects/simplex-chat/#languages). We will be adding more languages as some of the already added are completed – please suggest new languages, review the [translation guide](./docs/TRANSLATIONS.md) and get in touch with us!
|
||||
|
||||
## Contribute
|
||||
|
||||
We would love to have you join the development! You can help us with:
|
||||
|
||||
- [share the color theme](./docs/THEMES.md) you use in Android app!
|
||||
- writing a tutorial or recipes about hosting servers, chat bot automations, etc.
|
||||
- contributing to SimpleX Chat knowledge-base.
|
||||
- developing features - please connect to us via chat so we can help you get started.
|
||||
|
||||
## Help us with donations
|
||||
|
||||
Huge thank you to everybody who donated to SimpleX Chat!
|
||||
|
||||
We are prioritizing users privacy and security - it would be impossible without your support.
|
||||
|
||||
Our pledge to our users is that SimpleX protocols are and will remain open, and in public domain, - so anybody can build the future implementations of the clients and the servers. We are building SimpleX platform based on the same principles as email and web, but much more private and secure.
|
||||
|
||||
Your donations help us raise more funds – any amount, even the price of the cup of coffee, would make a big difference for us.
|
||||
|
||||
It is possible to donate via:
|
||||
|
||||
- [GitHub](https://github.com/sponsors/simplex-chat) - it is commission-free for us.
|
||||
- [OpenCollective](https://opencollective.com/simplex-chat) - it charges a commission, and also accepts donations in crypto-currencies.
|
||||
- Monero address: 8568eeVjaJ1RQ65ZUn9PRQ8ENtqeX9VVhcCYYhnVLxhV4JtBqw42so2VEUDQZNkFfsH5sXCuV7FN8VhRQ21DkNibTZP57Qt
|
||||
- Bitcoin address: 1bpefFkzuRoMY3ZuBbZNZxycbg7NYPYTG
|
||||
- BCH address: 1bpefFkzuRoMY3ZuBbZNZxycbg7NYPYTG
|
||||
- Ethereum address: 0x83fd788f7241a2be61780ea9dc72d2151e6843e2
|
||||
- Solana address: 43tWFWDczgAcn4Rzwkpqg2mqwnQETSiTwznmCgA2tf1L
|
||||
|
||||
Thank you,
|
||||
|
||||
Evgeny
|
||||
|
||||
SimpleX Chat founder
|
||||
|
||||
## Contents
|
||||
|
||||
@@ -38,16 +162,11 @@
|
||||
- [Users own SimpleX network](#users-own-simplex-network)
|
||||
- [Frequently asked questions](#frequently-asked-questions)
|
||||
- [News and updates](#news-and-updates)
|
||||
- [Make a private connection](#make-a-private-connection)
|
||||
- [Quick installation of a terminal app](#zap-quick-installation-of-a-terminal-app)
|
||||
- [SimpleX Platform design](#simplex-platform-design)
|
||||
- [Privacy: technical details and limitations](#privacy-technical-details-and-limitations)
|
||||
- [For developers](#for-developers)
|
||||
- [Roadmap](#roadmap)
|
||||
- [Join a user group](#join-a-user-group)
|
||||
- [Translate the apps](#translate-the-apps)
|
||||
- [Contribute](#contribute)
|
||||
- [Help us with donations](#help-us-with-donations)
|
||||
- [Disclaimers, Security contact, License](#disclaimers)
|
||||
|
||||
## Why privacy matters
|
||||
@@ -88,26 +207,28 @@ You can use SimpleX with your own servers and still communicate with people usin
|
||||
|
||||
Recent updates:
|
||||
|
||||
[Feb 04, 2023. v4.5 released - with multiple user profiles, message draft, transport isolation and Italian interface](./blog/20230204-simplex-chat-v4-5-user-chat-profiles.md).
|
||||
[July 22, 2023. SimpleX Chat: v5.2 released with message delivery receipts](./blog/20230722-simplex-chat-v5-2-message-delivery-receipts.md).
|
||||
|
||||
[Jan 03, 2023. v4.4 released - with disappearing messages, "live" messages, connection security verifications, GIFs and stickers and with French interface language](./blog/20230103-simplex-chat-v4.4-disappearing-messages.md).
|
||||
[May 23, 2023. SimpleX Chat: v5.1 released with message reactions and self-destruct passcode](./blog/20230523-simplex-chat-v5-1-message-reactions-self-destruct-passcode.md).
|
||||
|
||||
[Dec 06, 2022. November reviews and v4.3 released - with instant voice messages, irreversible deletion of sent messages and improved server configuration](./blog/20221206-simplex-chat-v4.3-voice-messages.md).
|
||||
[Apr 22, 2023. SimpleX Chat: vision and funding, v5.0 released with videos and files up to 1gb](./blog/20230422-simplex-chat-vision-funding-v5-videos-files-passcode.md).
|
||||
|
||||
[Nov 08, 2022. Security audit by Trail of Bits, the new website and v4.2 released](./blog/20221108-simplex-chat-v4.2-security-audit-new-website.md).
|
||||
[Mar 28, 2023. v4.6 released - with Android 8+ and ARMv7a support, hidden profiles, community moderation, improved audio/video calls and reduced battery usage](./blog/20230328-simplex-chat-v4-6-hidden-profiles.md).
|
||||
|
||||
[Mar 1, 2023. SimpleX File Transfer Protocol – send large files efficiently, privately and securely, soon to be integrated into SimpleX Chat apps.](./blog/20230301-simplex-file-transfer-protocol.md).
|
||||
|
||||
[Feb 4, 2023. v4.5 released - with multiple user profiles, message draft, transport isolation and Italian interface](./blog/20230204-simplex-chat-v4-5-user-chat-profiles.md).
|
||||
|
||||
[Jan 3, 2023. v4.4 released - with disappearing messages, "live" messages, connection security verifications, GIFs and stickers and with French interface language](./blog/20230103-simplex-chat-v4.4-disappearing-messages.md).
|
||||
|
||||
[Dec 6, 2022. November reviews and v4.3 released - with instant voice messages, irreversible deletion of sent messages and improved server configuration](./blog/20221206-simplex-chat-v4.3-voice-messages.md).
|
||||
|
||||
[Nov 8, 2022. Security audit by Trail of Bits, the new website and v4.2 released](./blog/20221108-simplex-chat-v4.2-security-audit-new-website.md).
|
||||
|
||||
[Sep 28, 2022. v4.0: encrypted local chat database and many other changes](./blog/20220928-simplex-chat-v4-encrypted-database.md).
|
||||
|
||||
[All updates](./blog)
|
||||
|
||||
## Make a private connection
|
||||
|
||||
You need to share a link or scan a QR code (in person or during a video call) to make a connection and start messaging.
|
||||
|
||||
The channel through which you share the link does not have to be secure - it is enough that you can confirm who sent you the message and that your SimpleX connection is established.
|
||||
|
||||
<img src="https://github.com/simplex-chat/.github/blob/master/profile/images/app1.png" alt="Make a private connection" height="360"> <img src="https://github.com/simplex-chat/.github/blob/master/profile/images/arrow.png" height="360"> <img src="https://github.com/simplex-chat/.github/blob/master/profile/images/app2.png" alt="Conversation" height="360"> <img src="https://github.com/simplex-chat/.github/blob/master/profile/images/arrow.png" height="360"> <img src="https://github.com/simplex-chat/.github/blob/master/profile/images/app3.png" alt="Video call" height="360">
|
||||
|
||||
## :zap: Quick installation of a terminal app
|
||||
|
||||
```sh
|
||||
@@ -138,13 +259,15 @@ See [SimpleX Chat Protocol](./docs/protocol/simplex-chat.md) for the format of m
|
||||
|
||||
SimpleX Chat is a work in progress – we are releasing improvements as they are ready. You have to decide if the current state is good enough for your usage scenario.
|
||||
|
||||
We compiled a [glossary of terms](./docs/GLOSSARY.md) used to describe communication systems to help understand some terms below and to help compare advantages and disadvantages of various communication systems.
|
||||
|
||||
What is already implemented:
|
||||
|
||||
1. Instead of user profile identifiers used by all other platforms, even the most private ones, SimpleX uses pairwise per-queue identifiers (2 addresses for each unidirectional message queue, with an optional 3rd address for push notifications on iOS, 2 queues in each connection between the users). It makes observing the network graph on the application level more difficult, as for `n` users there can be up to `n * (n-1)` message queues.
|
||||
2. End-to-end encryption in each message queue using [NaCl cryptobox](https://nacl.cr.yp.to/box.html). This is added to allow redundancy in the future (passing each message via several servers), to avoid having the same ciphertext in different queues (that would only be visible to the attacker if TLS is compromised). The encryption keys used for this encryption are not rotated, instead we are planning to rotate the queues. Curve25519 keys are used for key negotiation.
|
||||
3. [Double ratchet](https://signal.org/docs/specifications/doubleratchet/) end-to-end encryption in each conversation between two users (or group members). This is the same algorithm that is used in Signal and many other messaging apps; it provides OTR messaging with forward secrecy (each message is encrypted by its own ephemeral key), break-in recovery (the keys are frequently re-negotiated as part of the message exchange). Two pairs of Curve448 keys are used for the initial key agreement, initiating party passes these keys via the connection link, accepting side - in the header of the confirmation message.
|
||||
1. Instead of user profile identifiers used by all other platforms, even the most private ones, SimpleX uses [pairwise per-queue identifiers](./docs/GLOSSARY.md#pairwise-pseudonymous-identifier) (2 addresses for each unidirectional message queue, with an optional 3rd address for push notifications on iOS, 2 queues in each connection between the users). It makes observing the network graph on the application level more difficult, as for `n` users there can be up to `n * (n-1)` message queues.
|
||||
2. [End-to-end encryption](./docs/GLOSSARY.md#end-to-end-encryption) in each message queue using [NaCl cryptobox](https://nacl.cr.yp.to/box.html). This is added to allow redundancy in the future (passing each message via several servers), to avoid having the same ciphertext in different queues (that would only be visible to the attacker if TLS is compromised). The encryption keys used for this encryption are not rotated, instead we are planning to rotate the queues. Curve25519 keys are used for key negotiation.
|
||||
3. [Double ratchet](./docs/GLOSSARY.md#double-ratchet-algorithm) end-to-end encryption in each conversation between two users (or group members). This is the same algorithm that is used in Signal and many other messaging apps; it provides OTR messaging with [forward secrecy](./docs/GLOSSARY.md#forward-secrecy) (each message is encrypted by its own ephemeral key) and [break-in recovery](./docs/GLOSSARY.md#post-compromise-security) (the keys are frequently re-negotiated as part of the message exchange). Two pairs of Curve448 keys are used for the initial [key agreement](./docs/GLOSSARY.md#key-agreement-protocol), initiating party passes these keys via the connection link, accepting side - in the header of the confirmation message.
|
||||
4. Additional layer of encryption using NaCL cryptobox for the messages delivered from the server to the recipient. This layer avoids having any ciphertext in common between sent and received traffic of the server inside TLS (and there are no identifiers in common as well).
|
||||
5. Several levels of content padding to frustrate message size attacks.
|
||||
5. Several levels of [content padding](./docs/GLOSSARY.md#message-padding) to frustrate message size attacks.
|
||||
6. Starting from v2 of SMP protocol (the current version is v4) all message metadata, including the time when the message was received by the server (rounded to a second) is sent to the recipients inside an encrypted envelope, so even if TLS is compromised it cannot be observed.
|
||||
7. Only TLS 1.2/1.3 are allowed for client-server connections, limited to cryptographic algorithms: CHACHA20POLY1305_SHA256, Ed25519/Ed448, Curve25519/Curve448.
|
||||
8. To protect against replay attacks SimpleX servers require [tlsunique channel binding](https://www.rfc-editor.org/rfc/rfc5929.html) as session ID in each client command signed with per-queue ephemeral key.
|
||||
@@ -169,6 +292,8 @@ You can:
|
||||
|
||||
If you are considering developing with SimpleX platform please get in touch for any advice and support.
|
||||
|
||||
Please also join [#simplex-devs](https://simplex.chat/contact#/?v=1-2&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2F6eHqy7uAbZPOcA6qBtrQgQquVlt4Ll91%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAqV_pg3FF00L98aCXp4D3bOs4Sxv_UmSd-gb0juVoQVs%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion&data=%7B%22type%22%3A%22group%22%2C%22groupLinkId%22%3A%22XonlixcHBIb2ijCehbZoiw%3D%3D%22%7D) group to ask any questions and share your success stories.
|
||||
|
||||
## Roadmap
|
||||
|
||||
- ✅ Easy to deploy SimpleX server with in-memory message storage, without any dependencies.
|
||||
@@ -202,93 +327,37 @@ If you are considering developing with SimpleX platform please get in touch for
|
||||
- ✅ Multiple user profiles in the same chat database.
|
||||
- ✅ Optionally avoid re-using the same TCP session for multiple connections.
|
||||
- ✅ Preserve message drafts.
|
||||
- 🏗 File server to optimize for efficient and private sending of large files.
|
||||
- 🏗 Improved audio & video calls.
|
||||
- 🏗 SMP queue redundancy and rotation (manual is supported).
|
||||
- 🏗 Reduced battery and traffic usage in large groups.
|
||||
- 🏗 Support older Android OS and 32-bit CPUs.
|
||||
- Ephemeral/disappearing/OTR conversations with the existing contacts.
|
||||
- Access password/pin (with optional alternative access password).
|
||||
- ✅ File server to optimize for efficient and private sending of large files.
|
||||
- ✅ Improved audio & video calls.
|
||||
- ✅ Support older Android OS and 32-bit CPUs.
|
||||
- ✅ Hidden chat profiles.
|
||||
- ✅ Sending and receiving large files via [XFTP protocol](./blog/20230301-simplex-file-transfer-protocol.md).
|
||||
- ✅ Video messages.
|
||||
- ✅ App access passcode.
|
||||
- ✅ Improved Android app UI design.
|
||||
- ✅ Optional alternative access password.
|
||||
- ✅ Message reactions
|
||||
- ✅ Message editing history
|
||||
- ✅ Reduced battery and traffic usage in large groups.
|
||||
- ✅ Message delivery confirmation (with sender opt-out per contact).
|
||||
- 🏗 Desktop client.
|
||||
- SMP queue redundancy and rotation (manual is supported).
|
||||
- Include optional message into connection request sent via contact address.
|
||||
- Local app files encryption.
|
||||
- Video messages.
|
||||
- Improved navigation and search in the conversation (expand and scroll to quoted message, scroll to search results, etc.).
|
||||
- Message delivery confirmation (with sender opt-in or opt-out per contact, TBC).
|
||||
- Large groups, communities and public channels.
|
||||
- Feeds/broadcasts.
|
||||
- Ephemeral/disappearing/OTR conversations with the existing contacts.
|
||||
- Privately share your location.
|
||||
- Web widgets for custom interactivity in the chats.
|
||||
- Programmable chat automations / rules (automatic replies/forward/deletion/sending, reminders, etc.).
|
||||
- Supporting the same profile on multiple devices.
|
||||
- Desktop client.
|
||||
- Privacy-preserving identity server for optional DNS-based contact/group addresses to simplify connection and discovery, but not used to deliver messages:
|
||||
- keep all your contacts and groups even if you lose the domain.
|
||||
- the server doesn't have information about your contacts and groups.
|
||||
- Hosting server for large groups, communities and public channels.
|
||||
- Message delivery relay for senders (to conceal IP address from the recipients' servers and to reduce the traffic).
|
||||
- High capacity multi-node SMP relays.
|
||||
|
||||
## Join a user group
|
||||
|
||||
You can join an English-speaking group if you want to ask any questions: [#SimpleX-Group-2](https://simplex.chat/contact#/?v=1-2&smp=smp%3A%2F%2Fhpq7_4gGJiilmz5Rf-CswuU5kZGkm_zOIooSw6yALRg%3D%40smp5.simplex.im%2FQP8zaGjjmlXV-ix_Er4JgJ0lNPYGS1KX%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEApAgBkRZ3x12ayZ7sHrjHQWNMvqzZpWUgM_fFCUdLXwo%253D%26srv%3Djjbyvoemxysm7qxap7m5d5m35jzv5qq6gnlv7s4rsn7tdwwmuqciwpid.onion&data=%7B%22type%22%3A%22group%22%2C%22groupLinkId%22%3A%22xWpPXEZZsQp_F7vwAcAYDw%3D%3D%22%7D)
|
||||
|
||||
There are also several groups in languages other than English, that we have the apps interface translated into. These groups are for testing, and asking questions to other SimpleX Chat users. We do not always answer questions there, so please ask them in one of the English-speaking groups.
|
||||
|
||||
- [\#SimpleX-DE](https://simplex.chat/contact#/?v=1-2&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2FkIEl7OQzcp-J6aDmjdlQbRJwqkcZE7XR%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAR16PCu02MobRmKAsjzhDWMZcWP9hS8l5AUZi-Gs8z18%253D%26srv%3Dbylepyau3ty4czmn77q4fglvperknl4bi2eb2fdy2bh4jxtf32kf73yd.onion&data=%7B%22type%22%3A%22group%22%2C%22groupLinkId%22%3A%22puYPMCQt11yPUvgmI5jCiw%3D%3D%22%7D) (German-speaking).
|
||||
- [\#SimpleX-FR](https://simplex.chat/contact#/?v=1-2&smp=smp%3A%2F%2Fhpq7_4gGJiilmz5Rf-CswuU5kZGkm_zOIooSw6yALRg%3D%40smp5.simplex.im%2FvIHQDxTor53nwnWWTy5cHNwQQAdWN5Hw%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAPdgK1eBnETmgiqEQufbUkydKBJafoRx4iRrtrC2NAGc%253D%26srv%3Djjbyvoemxysm7qxap7m5d5m35jzv5qq6gnlv7s4rsn7tdwwmuqciwpid.onion&data=%7B%22type%22%3A%22group%22%2C%22groupLinkId%22%3A%221FyUryBPza-1ZFFE80Ekbg%3D%3D%22%7D) (French-speaking).
|
||||
- [\#SimpleX-RU](https://simplex.chat/contact#/?v=1-2&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2FXZyt3hJmWsycpN7Dqve_wbrAqb6myk1R%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAMFVIoytozTEa_QXOgoZFq_oe0IwZBYKvW50trSFXzXo%253D%26srv%3Dbylepyau3ty4czmn77q4fglvperknl4bi2eb2fdy2bh4jxtf32kf73yd.onion&data=%7B%22type%22%3A%22group%22%2C%22groupLinkId%22%3A%22xz05ngjA3pNIxLZ32a8Vxg%3D%3D%22%7D) (Russian-speaking).
|
||||
- [\#SimpleX-IT](https://simplex.chat/contact#/?v=1-2&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2F0weR-ZgDUl7ruOtI_8TZwEsnJP6UiImA%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAq4PSThO9Fvb5ydF48wB0yNbpzCbuQJCW3vZ9BGUfcxk%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion&data=%7B%22type%22%3A%22group%22%2C%22groupLinkId%22%3A%22e-iceLA0SctC62eARgYDWg%3D%3D%22%7D) (Italian-speaking).
|
||||
|
||||
You can join these groups either by opening these links in the app or by opening them in a desktop browser and scanning QR code.
|
||||
|
||||
Join via the app to share what's going on and ask any questions!
|
||||
|
||||
## Translate the apps
|
||||
|
||||
Thanks to our users and [Weblate](https://hosted.weblate.org/engage/simplex-chat/), SimpleX Chat apps are translated to many other languages. Join our translators to help SimpleX grow faster!
|
||||
|
||||
Current interface languages:
|
||||
|
||||
- English (development language)
|
||||
- German: [@mlanp](https://github.com/mlanp)
|
||||
- French: [@ishi_sama](https://github.com/ishi-sama)
|
||||
- Italian: [@unbranched](https://github.com/unbranched)
|
||||
- Russian: project team
|
||||
|
||||
Languages in progress: Chinese, Hindi, Czech, Japanese, Dutch and [many others](https://hosted.weblate.org/projects/simplex-chat/#languages). We will be adding more languages as some of the already added are completed – please suggest new languages, review the [translation guide](./docs/TRANSLATIONS.md) and get in touch with us!
|
||||
|
||||
## Contribute
|
||||
|
||||
We would love to have you join the development! You can contribute to SimpleX Chat with:
|
||||
|
||||
- translate website homepage - there is a lot of content we would like to share, it would help to bring the new users.
|
||||
- writing a tutorial or recipes about hosting servers, chat bot automations, etc.
|
||||
- developing features - please connect to us via chat so we can help you get started.
|
||||
|
||||
## Help us with donations
|
||||
|
||||
Huge thank you to everybody who donated to SimpleX Chat!
|
||||
|
||||
We are prioritizing users privacy and security - it would be impossible without your support.
|
||||
|
||||
Our pledge to our users is that SimpleX protocols are and will remain open, and in public domain, - so anybody can build the future implementations of the clients and the servers. We are building SimpleX platform based on the same principles as email and web, but much more private and secure.
|
||||
|
||||
Your donations help us raise more funds – any amount, even the price of the cup of coffee, would make a big difference for us.
|
||||
|
||||
It is possible to donate via:
|
||||
|
||||
- [GitHub](https://github.com/sponsors/simplex-chat) - it is commission-free for us.
|
||||
- [OpenCollective](https://opencollective.com/simplex-chat) - it charges a commission, and also accepts donations in crypto-currencies.
|
||||
- Monero address: 8568eeVjaJ1RQ65ZUn9PRQ8ENtqeX9VVhcCYYhnVLxhV4JtBqw42so2VEUDQZNkFfsH5sXCuV7FN8VhRQ21DkNibTZP57Qt
|
||||
- Bitcoin address: 1bpefFkzuRoMY3ZuBbZNZxycbg7NYPYTG
|
||||
- BCH address: 1bpefFkzuRoMY3ZuBbZNZxycbg7NYPYTG
|
||||
- Ethereum address: 0x83fd788f7241a2be61780ea9dc72d2151e6843e2
|
||||
- Solana address: 43tWFWDczgAcn4Rzwkpqg2mqwnQETSiTwznmCgA2tf1L
|
||||
- please let us know, via GitHub issue or chat, if you want to create a donation in some other cryptocurrency - we will add the address to the list.
|
||||
|
||||
Thank you,
|
||||
|
||||
Evgeny
|
||||
|
||||
SimpleX Chat founder
|
||||
|
||||
## Disclaimers
|
||||
|
||||
[SimpleX protocols and security model](https://github.com/simplex-chat/simplexmq/blob/master/protocol/overview-tjr.md) was reviewed, and had many breaking changes and improvements in v1.0.0.
|
||||
|
||||
20
apps/android/.gitignore
vendored
@@ -1,20 +0,0 @@
|
||||
*.iml
|
||||
.gradle
|
||||
/local.properties
|
||||
/.idea/caches
|
||||
/.idea/libraries
|
||||
/.idea/modules.xml
|
||||
/.idea/workspace.xml
|
||||
/.idea/navEditor.xml
|
||||
/.idea/assetWizardSettings.xml
|
||||
/.idea/deploymentTargetDropDown.xml
|
||||
/.idea/misc.xml
|
||||
/.idea/uiDesigner.xml
|
||||
/.idea/kotlinc.xml
|
||||
.DS_Store
|
||||
/build
|
||||
/captures
|
||||
.externalNativeBuild
|
||||
.cxx
|
||||
local.properties
|
||||
app/src/main/cpp/libs/
|
||||
1
apps/android/.idea/.name
generated
@@ -1 +0,0 @@
|
||||
SimpleX
|
||||
6
apps/android/.idea/compiler.xml
generated
@@ -1,6 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="CompilerConfiguration">
|
||||
<bytecodeTargetLevel target="11" />
|
||||
</component>
|
||||
</project>
|
||||
19
apps/android/.idea/gradle.xml
generated
@@ -1,19 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="GradleMigrationSettings" migrationVersion="1" />
|
||||
<component name="GradleSettings">
|
||||
<option name="linkedExternalProjectsSettings">
|
||||
<GradleProjectSettings>
|
||||
<option name="testRunner" value="GRADLE" />
|
||||
<option name="distributionType" value="DEFAULT_WRAPPED" />
|
||||
<option name="externalProjectPath" value="$PROJECT_DIR$" />
|
||||
<option name="modules">
|
||||
<set>
|
||||
<option value="$PROJECT_DIR$" />
|
||||
<option value="$PROJECT_DIR$/app" />
|
||||
</set>
|
||||
</option>
|
||||
</GradleProjectSettings>
|
||||
</option>
|
||||
</component>
|
||||
</project>
|
||||
@@ -1,20 +0,0 @@
|
||||
<component name="InspectionProjectProfileManager">
|
||||
<profile version="1.0">
|
||||
<option name="myName" value="Project Default" />
|
||||
<inspection_tool class="PreviewAnnotationInFunctionWithParameters" enabled="true" level="ERROR" enabled_by_default="true">
|
||||
<option name="previewFile" value="true" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="PreviewDimensionRespectsLimit" enabled="true" level="WARNING" enabled_by_default="true">
|
||||
<option name="previewFile" value="true" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="PreviewMultipleParameterProviders" enabled="true" level="ERROR" enabled_by_default="true">
|
||||
<option name="previewFile" value="true" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="PreviewMustBeTopLevelFunction" enabled="true" level="ERROR" enabled_by_default="true">
|
||||
<option name="previewFile" value="true" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="PreviewNeedsComposableAnnotation" enabled="true" level="ERROR" enabled_by_default="true">
|
||||
<option name="previewFile" value="true" />
|
||||
</inspection_tool>
|
||||
</profile>
|
||||
</component>
|
||||
6
apps/android/.idea/vcs.xml
generated
@@ -1,6 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="$PROJECT_DIR$/../.." vcs="Git" />
|
||||
</component>
|
||||
</project>
|
||||
@@ -1,204 +0,0 @@
|
||||
plugins {
|
||||
id 'com.android.application'
|
||||
id 'org.jetbrains.kotlin.android'
|
||||
id 'org.jetbrains.kotlin.plugin.serialization'
|
||||
}
|
||||
|
||||
android {
|
||||
compileSdk 32
|
||||
|
||||
defaultConfig {
|
||||
applicationId "chat.simplex.app"
|
||||
minSdk 29
|
||||
targetSdk 32
|
||||
versionCode 100
|
||||
versionName "4.5.2"
|
||||
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
ndk {
|
||||
abiFilters 'arm64-v8a'
|
||||
}
|
||||
vectorDrawables {
|
||||
useSupportLibrary true
|
||||
}
|
||||
externalNativeBuild {
|
||||
cmake {
|
||||
cppFlags ''
|
||||
}
|
||||
}
|
||||
manifestPlaceholders.app_name = "@string/app_name"
|
||||
manifestPlaceholders.provider_authorities = "chat.simplex.app.provider"
|
||||
manifestPlaceholders.extract_native_libs = compression_level != "0"
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
debug {
|
||||
applicationIdSuffix "$application_id_suffix"
|
||||
debuggable new Boolean("$enable_debuggable")
|
||||
manifestPlaceholders.app_name = "$app_name"
|
||||
// Provider can't be the same for different apps on the same device
|
||||
manifestPlaceholders.provider_authorities = "chat.simplex.app${application_id_suffix}.provider"
|
||||
}
|
||||
release {
|
||||
minifyEnabled false
|
||||
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
||||
}
|
||||
}
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
}
|
||||
kotlinOptions {
|
||||
jvmTarget = '1.8'
|
||||
freeCompilerArgs += "-opt-in=kotlinx.coroutines.DelicateCoroutinesApi"
|
||||
freeCompilerArgs += "-opt-in=androidx.compose.foundation.ExperimentalFoundationApi"
|
||||
freeCompilerArgs += "-opt-in=androidx.compose.ui.text.ExperimentalTextApi"
|
||||
freeCompilerArgs += "-opt-in=androidx.compose.material.ExperimentalMaterialApi"
|
||||
freeCompilerArgs += "-opt-in=com.google.accompanist.insets.ExperimentalAnimatedInsets"
|
||||
freeCompilerArgs += "-opt-in=com.google.accompanist.permissions.ExperimentalPermissionsApi"
|
||||
freeCompilerArgs += "-opt-in=kotlinx.serialization.InternalSerializationApi"
|
||||
freeCompilerArgs += "-opt-in=kotlinx.serialization.ExperimentalSerializationApi"
|
||||
}
|
||||
externalNativeBuild {
|
||||
cmake {
|
||||
path file('src/main/cpp/CMakeLists.txt')
|
||||
}
|
||||
}
|
||||
buildFeatures {
|
||||
compose true
|
||||
}
|
||||
composeOptions {
|
||||
kotlinCompilerExtensionVersion compose_version
|
||||
}
|
||||
packagingOptions {
|
||||
resources {
|
||||
excludes += '/META-INF/{AL2.0,LGPL2.1}'
|
||||
}
|
||||
jniLibs.useLegacyPackaging = compression_level != "0"
|
||||
}
|
||||
def isRelease = gradle.getStartParameter().taskNames.find({ it.toLowerCase().contains("release") }) != null
|
||||
if (isRelease) {
|
||||
// Comma separated list of languages that will be included in the apk
|
||||
android.defaultConfig.resConfigs("en", "ru", "de", "fr", "it")
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation 'androidx.core:core-ktx:1.7.0'
|
||||
implementation "androidx.compose.ui:ui:$compose_version"
|
||||
implementation "androidx.compose.material:material:$compose_version"
|
||||
implementation "androidx.compose.ui:ui-tooling-preview:$compose_version"
|
||||
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.4.1'
|
||||
implementation 'androidx.lifecycle:lifecycle-process:2.4.1'
|
||||
implementation 'androidx.activity:activity-compose:1.4.0'
|
||||
implementation 'androidx.fragment:fragment:1.4.1'
|
||||
implementation 'org.jetbrains.kotlinx:kotlinx-datetime:0.3.2'
|
||||
implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json:1.3.2'
|
||||
implementation "androidx.compose.material:material-icons-extended:$compose_version"
|
||||
implementation "androidx.compose.ui:ui-util:$compose_version"
|
||||
implementation "androidx.navigation:navigation-compose:2.4.1"
|
||||
implementation "com.google.accompanist:accompanist-insets:0.23.0"
|
||||
implementation 'androidx.webkit:webkit:1.4.0'
|
||||
implementation "com.godaddy.android.colorpicker:compose-color-picker:0.4.2"
|
||||
|
||||
def work_version = "2.7.1"
|
||||
implementation "androidx.work:work-runtime-ktx:$work_version"
|
||||
implementation "androidx.work:work-multiprocess:$work_version"
|
||||
|
||||
def camerax_version = "1.1.0-beta01"
|
||||
implementation "androidx.camera:camera-core:${camerax_version}"
|
||||
implementation "androidx.camera:camera-camera2:${camerax_version}"
|
||||
implementation "androidx.camera:camera-lifecycle:${camerax_version}"
|
||||
implementation "androidx.camera:camera-view:${camerax_version}"
|
||||
|
||||
//Barcode
|
||||
implementation 'org.boofcv:boofcv-android:0.40.1'
|
||||
implementation 'org.boofcv:boofcv-core:0.40.1'
|
||||
|
||||
//Camera Permission
|
||||
implementation "com.google.accompanist:accompanist-permissions:0.23.0"
|
||||
implementation "com.google.accompanist:accompanist-pager:0.25.1"
|
||||
|
||||
// Link Previews
|
||||
implementation 'org.jsoup:jsoup:1.13.1'
|
||||
|
||||
// Biometric authentication
|
||||
implementation 'androidx.biometric:biometric:1.2.0-alpha04'
|
||||
|
||||
// GIFs support
|
||||
implementation "io.coil-kt:coil-compose:2.1.0"
|
||||
implementation "io.coil-kt:coil-gif:2.1.0"
|
||||
|
||||
testImplementation 'junit:junit:4.13.2'
|
||||
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
|
||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
|
||||
androidTestImplementation "androidx.compose.ui:ui-test-junit4:$compose_version"
|
||||
debugImplementation "androidx.compose.ui:ui-tooling:$compose_version"
|
||||
}
|
||||
|
||||
def buildType = "unknown"
|
||||
// Don't do anything if no compression is needed
|
||||
if (compression_level != "0") {
|
||||
tasks.whenTaskAdded { task ->
|
||||
if (task.name == 'packageDebug') {
|
||||
task.doLast {
|
||||
buildType = "debug"
|
||||
}
|
||||
task.finalizedBy compressApk
|
||||
} else if (task.name == 'packageRelease') {
|
||||
task.doLast {
|
||||
buildType = "release"
|
||||
}
|
||||
task.finalizedBy compressApk
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tasks.register("compressApk") {
|
||||
doLast {
|
||||
def javaHome = System.properties['java.home'] ?: org.gradle.internal.jvm.Jvm.current().getJavaHome()
|
||||
def sdkDir = android.getSdkDirectory().getAbsolutePath()
|
||||
def keyAlias = ""
|
||||
def keyPassword = ""
|
||||
def storeFile = ""
|
||||
def storePassword = ""
|
||||
if (project.properties['android.injected.signing.key.alias'] != null) {
|
||||
keyAlias = project.properties['android.injected.signing.key.alias']
|
||||
keyPassword = project.properties['android.injected.signing.key.password']
|
||||
storeFile = project.properties['android.injected.signing.store.file']
|
||||
storePassword = project.properties['android.injected.signing.store.password']
|
||||
} else if (android.signingConfigs.hasProperty(buildType)) {
|
||||
def gradleConfig = android.signingConfigs[buildType]
|
||||
keyAlias = gradleConfig.keyAlias
|
||||
keyPassword = gradleConfig.keyPassword
|
||||
storeFile = gradleConfig.storeFile
|
||||
storePassword = gradleConfig.storePassword
|
||||
} else {
|
||||
// There is no signing config for current build type, can't sign the apk
|
||||
println("No signing configs for this build type: $buildType")
|
||||
return
|
||||
}
|
||||
|
||||
def outputDir = tasks["package${buildType.capitalize()}"].outputs.files.last()
|
||||
|
||||
exec {
|
||||
workingDir '../../../scripts/android'
|
||||
setEnvironment(['JAVA_HOME': "$javaHome"])
|
||||
commandLine './compress-and-sign-apk.sh', \
|
||||
"$compression_level", \
|
||||
"$outputDir", \
|
||||
"$sdkDir", \
|
||||
"$storeFile", \
|
||||
"$storePassword", \
|
||||
"$keyAlias", \
|
||||
"$keyPassword"
|
||||
}
|
||||
|
||||
if (project.properties['android.injected.signing.key.alias'] != null && buildType == 'release') {
|
||||
new File(outputDir, "app-release.apk").renameTo(new File(outputDir, "simplex.apk"))
|
||||
}
|
||||
|
||||
// View all gradle properties set
|
||||
// project.properties.each { k, v -> println "$k -> $v" }
|
||||
}
|
||||
}
|
||||
@@ -1,516 +0,0 @@
|
||||
package chat.simplex.app
|
||||
|
||||
import android.app.Application
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.os.Parcelable
|
||||
import android.os.SystemClock.elapsedRealtime
|
||||
import android.util.Log
|
||||
import android.view.WindowManager
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.activity.viewModels
|
||||
import androidx.compose.animation.core.*
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.Surface
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.Lock
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.graphicsLayer
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import androidx.lifecycle.*
|
||||
import chat.simplex.app.model.ChatModel
|
||||
import chat.simplex.app.model.NtfManager
|
||||
import chat.simplex.app.model.NtfManager.Companion.getUserIdFromIntent
|
||||
import chat.simplex.app.ui.theme.SimpleButton
|
||||
import chat.simplex.app.ui.theme.SimpleXTheme
|
||||
import chat.simplex.app.views.SplashView
|
||||
import chat.simplex.app.views.call.ActiveCallView
|
||||
import chat.simplex.app.views.call.IncomingCallAlertView
|
||||
import chat.simplex.app.views.chat.ChatView
|
||||
import chat.simplex.app.views.chatlist.*
|
||||
import chat.simplex.app.views.database.DatabaseErrorView
|
||||
import chat.simplex.app.views.helpers.*
|
||||
import chat.simplex.app.views.newchat.*
|
||||
import chat.simplex.app.views.onboarding.*
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class MainActivity: FragmentActivity() {
|
||||
companion object {
|
||||
/**
|
||||
* We don't want these values to be bound to Activity lifecycle since activities are changed often, for example, when a user
|
||||
* clicks on new message in notification. In this case savedInstanceState will be null (this prevents restoring the values)
|
||||
* See [SimplexService.onTaskRemoved] for another part of the logic which nullifies the values when app closed by the user
|
||||
* */
|
||||
val userAuthorized = mutableStateOf<Boolean?>(null)
|
||||
val enteredBackground = mutableStateOf<Long?>(null)
|
||||
// Remember result and show it after orientation change
|
||||
private val laFailed = mutableStateOf(false)
|
||||
|
||||
fun clearAuthState() {
|
||||
userAuthorized.value = null
|
||||
enteredBackground.value = null
|
||||
}
|
||||
}
|
||||
private val vm by viewModels<SimplexViewModel>()
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
// testJson()
|
||||
val m = vm.chatModel
|
||||
// When call ended and orientation changes, it re-process old intent, it's unneeded.
|
||||
// Only needed to be processed on first creation of activity
|
||||
if (savedInstanceState == null) {
|
||||
processNotificationIntent(intent, m)
|
||||
processIntent(intent, m)
|
||||
processExternalIntent(intent, m)
|
||||
}
|
||||
if (m.controller.appPrefs.privacyProtectScreen.get()) {
|
||||
Log.d(TAG, "onCreate: set FLAG_SECURE")
|
||||
window.setFlags(
|
||||
WindowManager.LayoutParams.FLAG_SECURE,
|
||||
WindowManager.LayoutParams.FLAG_SECURE
|
||||
)
|
||||
}
|
||||
setContent {
|
||||
SimpleXTheme {
|
||||
Surface(
|
||||
Modifier
|
||||
.background(MaterialTheme.colors.background)
|
||||
.fillMaxSize()
|
||||
) {
|
||||
MainPage(
|
||||
m,
|
||||
userAuthorized,
|
||||
laFailed,
|
||||
::runAuthenticate,
|
||||
::setPerformLA,
|
||||
showLANotice = { m.controller.showLANotice(this) }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
SimplexApp.context.schedulePeriodicServiceRestartWorker()
|
||||
SimplexApp.context.schedulePeriodicWakeUp()
|
||||
}
|
||||
|
||||
override fun onNewIntent(intent: Intent?) {
|
||||
super.onNewIntent(intent)
|
||||
processIntent(intent, vm.chatModel)
|
||||
processExternalIntent(intent, vm.chatModel)
|
||||
}
|
||||
|
||||
override fun onStart() {
|
||||
super.onStart()
|
||||
val enteredBackgroundVal = enteredBackground.value
|
||||
if (enteredBackgroundVal == null || elapsedRealtime() - enteredBackgroundVal >= 30_000) {
|
||||
runAuthenticate()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
super.onPause()
|
||||
/**
|
||||
* When new activity is created after a click on notification, the old one receives onPause before
|
||||
* recreation but receives onStop after recreation. So using both (onPause and onStop) to prevent
|
||||
* unwanted multiple auth dialogs from [runAuthenticate]
|
||||
* */
|
||||
enteredBackground.value = elapsedRealtime()
|
||||
}
|
||||
|
||||
override fun onStop() {
|
||||
super.onStop()
|
||||
enteredBackground.value = elapsedRealtime()
|
||||
}
|
||||
|
||||
override fun onBackPressed() {
|
||||
if (
|
||||
onBackPressedDispatcher.hasEnabledCallbacks() // Has something to do in a backstack
|
||||
|| Build.VERSION.SDK_INT >= Build.VERSION_CODES.R // Android 11 or above
|
||||
|| isTaskRoot // there are still other tasks after we reach the main (home) activity
|
||||
) {
|
||||
// https://medium.com/mobile-app-development-publication/the-risk-of-android-strandhogg-security-issue-and-how-it-can-be-mitigated-80d2ddb4af06
|
||||
super.onBackPressed()
|
||||
}
|
||||
|
||||
if (!onBackPressedDispatcher.hasEnabledCallbacks() && vm.chatModel.controller.appPrefs.performLA.get()) {
|
||||
// When pressed Back and there is no one wants to process the back event, clear auth state to force re-auth on launch
|
||||
clearAuthState()
|
||||
laFailed.value = true
|
||||
}
|
||||
if (!onBackPressedDispatcher.hasEnabledCallbacks()) {
|
||||
// Drop shared content
|
||||
SimplexApp.context.chatModel.sharedContent.value = null
|
||||
}
|
||||
}
|
||||
|
||||
private fun runAuthenticate() {
|
||||
val m = vm.chatModel
|
||||
if (!m.controller.appPrefs.performLA.get()) {
|
||||
userAuthorized.value = true
|
||||
} else {
|
||||
userAuthorized.value = false
|
||||
ModalManager.shared.closeModals()
|
||||
authenticate(
|
||||
generalGetString(R.string.auth_unlock),
|
||||
generalGetString(R.string.auth_log_in_using_credential),
|
||||
this@MainActivity,
|
||||
completed = { laResult ->
|
||||
when (laResult) {
|
||||
LAResult.Success ->
|
||||
userAuthorized.value = true
|
||||
is LAResult.Error, LAResult.Failed ->
|
||||
laFailed.value = true
|
||||
LAResult.Unavailable -> {
|
||||
userAuthorized.value = true
|
||||
m.performLA.value = false
|
||||
m.controller.appPrefs.performLA.set(false)
|
||||
laUnavailableTurningOffAlert()
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun setPerformLA(on: Boolean) {
|
||||
vm.chatModel.controller.appPrefs.laNoticeShown.set(true)
|
||||
if (on) {
|
||||
enableLA()
|
||||
} else {
|
||||
disableLA()
|
||||
}
|
||||
}
|
||||
|
||||
private fun enableLA() {
|
||||
val m = vm.chatModel
|
||||
authenticate(
|
||||
generalGetString(R.string.auth_enable_simplex_lock),
|
||||
generalGetString(R.string.auth_confirm_credential),
|
||||
this@MainActivity,
|
||||
completed = { laResult ->
|
||||
val prefPerformLA = m.controller.appPrefs.performLA
|
||||
when (laResult) {
|
||||
LAResult.Success -> {
|
||||
m.performLA.value = true
|
||||
prefPerformLA.set(true)
|
||||
laTurnedOnAlert()
|
||||
}
|
||||
is LAResult.Error, LAResult.Failed -> {
|
||||
m.performLA.value = false
|
||||
prefPerformLA.set(false)
|
||||
}
|
||||
LAResult.Unavailable -> {
|
||||
m.performLA.value = false
|
||||
prefPerformLA.set(false)
|
||||
laUnavailableInstructionAlert()
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private fun disableLA() {
|
||||
val m = vm.chatModel
|
||||
authenticate(
|
||||
generalGetString(R.string.auth_disable_simplex_lock),
|
||||
generalGetString(R.string.auth_confirm_credential),
|
||||
this@MainActivity,
|
||||
completed = { laResult ->
|
||||
val prefPerformLA = m.controller.appPrefs.performLA
|
||||
when (laResult) {
|
||||
LAResult.Success -> {
|
||||
m.performLA.value = false
|
||||
prefPerformLA.set(false)
|
||||
}
|
||||
is LAResult.Error, LAResult.Failed -> {
|
||||
m.performLA.value = true
|
||||
prefPerformLA.set(true)
|
||||
}
|
||||
LAResult.Unavailable -> {
|
||||
m.performLA.value = false
|
||||
prefPerformLA.set(false)
|
||||
laUnavailableTurningOffAlert()
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
class SimplexViewModel(application: Application): AndroidViewModel(application) {
|
||||
val app = getApplication<SimplexApp>()
|
||||
val chatModel = app.chatModel
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun MainPage(
|
||||
chatModel: ChatModel,
|
||||
userAuthorized: MutableState<Boolean?>,
|
||||
laFailed: MutableState<Boolean>,
|
||||
runAuthenticate: () -> Unit,
|
||||
setPerformLA: (Boolean) -> Unit,
|
||||
showLANotice: () -> Unit
|
||||
) {
|
||||
// this with LaunchedEffect(userAuthorized.value) fixes bottom sheet visibly collapsing after authentication
|
||||
var chatsAccessAuthorized by rememberSaveable { mutableStateOf(false) }
|
||||
LaunchedEffect(userAuthorized.value) {
|
||||
if (chatModel.controller.appPrefs.performLA.get()) {
|
||||
delay(500L)
|
||||
}
|
||||
chatsAccessAuthorized = userAuthorized.value == true
|
||||
}
|
||||
var showChatDatabaseError by rememberSaveable {
|
||||
mutableStateOf(chatModel.chatDbStatus.value != DBMigrationResult.OK && chatModel.chatDbStatus.value != null)
|
||||
}
|
||||
LaunchedEffect(chatModel.chatDbStatus.value) {
|
||||
showChatDatabaseError = chatModel.chatDbStatus.value != DBMigrationResult.OK && chatModel.chatDbStatus.value != null
|
||||
}
|
||||
|
||||
var showAdvertiseLAAlert by remember { mutableStateOf(false) }
|
||||
LaunchedEffect(showAdvertiseLAAlert) {
|
||||
if (
|
||||
!chatModel.controller.appPrefs.laNoticeShown.get()
|
||||
&& showAdvertiseLAAlert
|
||||
&& chatModel.onboardingStage.value == OnboardingStage.OnboardingComplete
|
||||
&& chatModel.chats.isNotEmpty()
|
||||
&& chatModel.activeCallInvitation.value == null
|
||||
) {
|
||||
showLANotice()
|
||||
}
|
||||
}
|
||||
LaunchedEffect(chatModel.showAdvertiseLAUnavailableAlert.value) {
|
||||
if (chatModel.showAdvertiseLAUnavailableAlert.value) {
|
||||
laUnavailableInstructionAlert()
|
||||
}
|
||||
}
|
||||
LaunchedEffect(chatModel.clearOverlays.value) {
|
||||
if (chatModel.clearOverlays.value) {
|
||||
ModalManager.shared.closeModals()
|
||||
chatModel.clearOverlays.value = false
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun authView() {
|
||||
Box(
|
||||
Modifier.fillMaxSize(),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
SimpleButton(
|
||||
stringResource(R.string.auth_unlock),
|
||||
icon = Icons.Outlined.Lock,
|
||||
click = {
|
||||
laFailed.value = false
|
||||
runAuthenticate()
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Box {
|
||||
val onboarding = chatModel.onboardingStage.value
|
||||
val userCreated = chatModel.userCreated.value
|
||||
when {
|
||||
showChatDatabaseError -> {
|
||||
chatModel.chatDbStatus.value?.let {
|
||||
DatabaseErrorView(chatModel.chatDbStatus, chatModel.controller.appPrefs)
|
||||
}
|
||||
}
|
||||
onboarding == null || userCreated == null -> SplashView()
|
||||
!chatsAccessAuthorized -> {
|
||||
if (chatModel.controller.appPrefs.performLA.get() && laFailed.value) {
|
||||
authView()
|
||||
} else {
|
||||
SplashView()
|
||||
}
|
||||
}
|
||||
onboarding == OnboardingStage.OnboardingComplete && userCreated -> {
|
||||
Box {
|
||||
if (chatModel.showCallView.value) ActiveCallView(chatModel)
|
||||
else {
|
||||
showAdvertiseLAAlert = true
|
||||
BoxWithConstraints {
|
||||
var currentChatId by rememberSaveable { mutableStateOf(chatModel.chatId.value) }
|
||||
val offset = remember { Animatable(if (chatModel.chatId.value == null) 0f else maxWidth.value) }
|
||||
Box(
|
||||
Modifier
|
||||
.graphicsLayer {
|
||||
translationX = -offset.value.dp.toPx()
|
||||
}
|
||||
) {
|
||||
val stopped = chatModel.chatRunning.value == false
|
||||
if (chatModel.sharedContent.value == null)
|
||||
ChatListView(chatModel, setPerformLA, stopped)
|
||||
else
|
||||
ShareListView(chatModel, stopped)
|
||||
}
|
||||
val scope = rememberCoroutineScope()
|
||||
val onComposed: () -> Unit = {
|
||||
scope.launch {
|
||||
offset.animateTo(
|
||||
if (chatModel.chatId.value == null) 0f else maxWidth.value,
|
||||
chatListAnimationSpec()
|
||||
)
|
||||
if (offset.value == 0f) {
|
||||
currentChatId = null
|
||||
}
|
||||
}
|
||||
}
|
||||
LaunchedEffect(Unit) {
|
||||
launch {
|
||||
snapshotFlow { chatModel.chatId.value }
|
||||
.distinctUntilChanged()
|
||||
.collect {
|
||||
if (it != null) currentChatId = it
|
||||
else onComposed()
|
||||
}
|
||||
}
|
||||
}
|
||||
Box (Modifier.graphicsLayer { translationX = maxWidth.toPx() - offset.value.dp.toPx() }) Box2@ {
|
||||
currentChatId?.let {
|
||||
ChatView(it, chatModel, onComposed)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
onboarding == OnboardingStage.Step1_SimpleXInfo -> SimpleXInfo(chatModel, onboarding = true)
|
||||
onboarding == OnboardingStage.Step2_CreateProfile -> CreateProfile(chatModel) {}
|
||||
onboarding == OnboardingStage.Step3_SetNotificationsMode -> SetNotificationsMode(chatModel)
|
||||
}
|
||||
ModalManager.shared.showInView()
|
||||
val invitation = chatModel.activeCallInvitation.value
|
||||
if (invitation != null) IncomingCallAlertView(invitation, chatModel)
|
||||
AlertManager.shared.showInView()
|
||||
}
|
||||
}
|
||||
|
||||
fun processNotificationIntent(intent: Intent?, chatModel: ChatModel) {
|
||||
val userId = getUserIdFromIntent(intent)
|
||||
when (intent?.action) {
|
||||
NtfManager.OpenChatAction -> {
|
||||
val chatId = intent.getStringExtra("chatId")
|
||||
Log.d(TAG, "processNotificationIntent: OpenChatAction $chatId")
|
||||
if (chatId != null) {
|
||||
withBGApi {
|
||||
if (userId != null && userId != chatModel.currentUser.value?.userId) {
|
||||
chatModel.controller.changeActiveUser(userId)
|
||||
}
|
||||
val cInfo = chatModel.getChat(chatId)?.chatInfo
|
||||
chatModel.clearOverlays.value = true
|
||||
if (cInfo != null) openChat(cInfo, chatModel)
|
||||
}
|
||||
}
|
||||
}
|
||||
NtfManager.ShowChatsAction -> {
|
||||
Log.d(TAG, "processNotificationIntent: ShowChatsAction")
|
||||
withBGApi {
|
||||
if (userId != null && userId != chatModel.currentUser.value?.userId) {
|
||||
chatModel.controller.changeActiveUser(userId)
|
||||
}
|
||||
chatModel.chatId.value = null
|
||||
chatModel.clearOverlays.value = true
|
||||
}
|
||||
}
|
||||
NtfManager.AcceptCallAction -> {
|
||||
val chatId = intent.getStringExtra("chatId")
|
||||
if (chatId == null || chatId == "") return
|
||||
Log.d(TAG, "processNotificationIntent: AcceptCallAction $chatId")
|
||||
chatModel.clearOverlays.value = true
|
||||
val invitation = chatModel.callInvitations[chatId]
|
||||
if (invitation == null) {
|
||||
AlertManager.shared.showAlertMsg(generalGetString(R.string.call_already_ended))
|
||||
} else {
|
||||
chatModel.callManager.acceptIncomingCall(invitation = invitation)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun processIntent(intent: Intent?, chatModel: ChatModel) {
|
||||
when (intent?.action) {
|
||||
"android.intent.action.VIEW" -> {
|
||||
val uri = intent.data
|
||||
if (uri != null) connectIfOpenedViaUri(uri, chatModel)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun processExternalIntent(intent: Intent?, chatModel: ChatModel) {
|
||||
when (intent?.action) {
|
||||
Intent.ACTION_SEND -> {
|
||||
// Close active chat and show a list of chats
|
||||
chatModel.chatId.value = null
|
||||
chatModel.clearOverlays.value = true
|
||||
when {
|
||||
"text/plain" == intent.type -> intent.getStringExtra(Intent.EXTRA_TEXT)?.let {
|
||||
chatModel.sharedContent.value = SharedContent.Text(it)
|
||||
}
|
||||
intent.type?.startsWith("image/") == true -> (intent.getParcelableExtra<Parcelable>(Intent.EXTRA_STREAM) as? Uri)?.let {
|
||||
chatModel.sharedContent.value = SharedContent.Images(intent.getStringExtra(Intent.EXTRA_TEXT) ?: "", listOf(it))
|
||||
} // All other mime types
|
||||
else -> (intent.getParcelableExtra<Parcelable>(Intent.EXTRA_STREAM) as? Uri)?.let {
|
||||
chatModel.sharedContent.value = SharedContent.File(intent.getStringExtra(Intent.EXTRA_TEXT) ?: "", it)
|
||||
}
|
||||
}
|
||||
}
|
||||
Intent.ACTION_SEND_MULTIPLE -> {
|
||||
// Close active chat and show a list of chats
|
||||
chatModel.chatId.value = null
|
||||
chatModel.clearOverlays.value = true
|
||||
when {
|
||||
intent.type?.startsWith("image/") == true -> (intent.getParcelableArrayListExtra<Parcelable>(Intent.EXTRA_STREAM) as? List<Uri>)?.let {
|
||||
chatModel.sharedContent.value = SharedContent.Images(intent.getStringExtra(Intent.EXTRA_TEXT) ?: "", it)
|
||||
} // All other mime types
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun connectIfOpenedViaUri(uri: Uri, chatModel: ChatModel) {
|
||||
Log.d(TAG, "connectIfOpenedViaUri: opened via link")
|
||||
if (chatModel.currentUser.value == null) {
|
||||
chatModel.appOpenUrl.value = uri
|
||||
} else {
|
||||
withUriAction(uri) { linkType ->
|
||||
val title = when (linkType) {
|
||||
ConnectionLinkType.CONTACT -> generalGetString(R.string.connect_via_contact_link)
|
||||
ConnectionLinkType.INVITATION -> generalGetString(R.string.connect_via_invitation_link)
|
||||
ConnectionLinkType.GROUP -> generalGetString(R.string.connect_via_group_link)
|
||||
}
|
||||
AlertManager.shared.showAlertMsg(
|
||||
title = title,
|
||||
text = if (linkType == ConnectionLinkType.GROUP)
|
||||
generalGetString(R.string.you_will_join_group)
|
||||
else
|
||||
generalGetString(R.string.profile_will_be_sent_to_contact_sending_link),
|
||||
confirmText = generalGetString(R.string.connect_via_link_verb),
|
||||
onConfirm = {
|
||||
withApi {
|
||||
Log.d(TAG, "connectIfOpenedViaUri: connecting")
|
||||
connectViaUri(chatModel, linkType, uri)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
//fun testJson() {
|
||||
// val str: String = """
|
||||
// """.trimIndent()
|
||||
//
|
||||
// println(json.decodeFromString<APIResponse>(str))
|
||||
//}
|
||||
@@ -1,237 +0,0 @@
|
||||
package chat.simplex.app
|
||||
|
||||
import android.app.Application
|
||||
import android.net.LocalServerSocket
|
||||
import android.util.Log
|
||||
import androidx.lifecycle.*
|
||||
import androidx.work.*
|
||||
import chat.simplex.app.model.*
|
||||
import chat.simplex.app.views.helpers.*
|
||||
import chat.simplex.app.views.onboarding.OnboardingStage
|
||||
import chat.simplex.app.views.usersettings.NotificationsMode
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.serialization.decodeFromString
|
||||
import java.io.*
|
||||
import java.util.*
|
||||
import java.util.concurrent.Semaphore
|
||||
import java.util.concurrent.TimeUnit
|
||||
import kotlin.concurrent.thread
|
||||
|
||||
const val TAG = "SIMPLEX"
|
||||
|
||||
// ghc's rts
|
||||
external fun initHS()
|
||||
// android-support
|
||||
external fun pipeStdOutToSocket(socketName: String) : Int
|
||||
|
||||
// SimpleX API
|
||||
typealias ChatCtrl = Long
|
||||
external fun chatMigrateInit(dbPath: String, dbKey: String): Array<Any>
|
||||
external fun chatSendCmd(ctrl: ChatCtrl, msg: String): String
|
||||
external fun chatRecvMsg(ctrl: ChatCtrl): String
|
||||
external fun chatRecvMsgWait(ctrl: ChatCtrl, timeout: Int): String
|
||||
external fun chatParseMarkdown(str: String): String
|
||||
external fun chatParseServer(str: String): String
|
||||
|
||||
class SimplexApp: Application(), LifecycleEventObserver {
|
||||
lateinit var chatController: ChatController
|
||||
|
||||
var isAppOnForeground: Boolean = false
|
||||
|
||||
fun initChatController(useKey: String? = null, startChat: Boolean = true) {
|
||||
val dbKey = useKey ?: DatabaseUtils.useDatabaseKey()
|
||||
val dbAbsolutePathPrefix = getFilesDirectory(SimplexApp.context)
|
||||
val migrated: Array<Any> = chatMigrateInit(dbAbsolutePathPrefix, dbKey)
|
||||
val res: DBMigrationResult = kotlin.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
|
||||
if (::chatController.isInitialized) {
|
||||
chatController.ctrl = ctrl
|
||||
} else {
|
||||
chatController = ChatController(ctrl, ntfManager, applicationContext, appPreferences)
|
||||
}
|
||||
chatModel.chatDbEncrypted.value = dbKey != ""
|
||||
chatModel.chatDbStatus.value = res
|
||||
if (res != DBMigrationResult.OK) {
|
||||
Log.d(TAG, "Unable to migrate successfully: $res")
|
||||
} else if (startChat) {
|
||||
// If we migrated successfully means previous re-encryption process on database level finished successfully too
|
||||
if (appPreferences.encryptionStartedAt.get() != null) appPreferences.encryptionStartedAt.set(null)
|
||||
withApi {
|
||||
val user = chatController.apiGetActiveUser()
|
||||
if (user == null) {
|
||||
chatModel.onboardingStage.value = OnboardingStage.Step1_SimpleXInfo
|
||||
} else {
|
||||
chatController.startChat(user)
|
||||
chatController.showBackgroundServiceNoticeIfNeeded()
|
||||
if (appPreferences.notificationsMode.get() == NotificationsMode.SERVICE.name)
|
||||
SimplexService.start(applicationContext)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val chatModel: ChatModel
|
||||
get() = chatController.chatModel
|
||||
|
||||
private val ntfManager: NtfManager by lazy {
|
||||
NtfManager(applicationContext, appPreferences)
|
||||
}
|
||||
|
||||
private val appPreferences: AppPreferences by lazy {
|
||||
AppPreferences(applicationContext)
|
||||
}
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
context = this
|
||||
initChatController()
|
||||
ProcessLifecycleOwner.get().lifecycle.addObserver(this)
|
||||
context.getDir("temp", MODE_PRIVATE).deleteRecursively()
|
||||
}
|
||||
|
||||
override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) {
|
||||
Log.d(TAG, "onStateChanged: $event")
|
||||
withApi {
|
||||
when (event) {
|
||||
Lifecycle.Event.ON_START -> {
|
||||
isAppOnForeground = true
|
||||
if (chatModel.chatRunning.value == true) {
|
||||
kotlin.runCatching {
|
||||
val currentUserId = chatModel.currentUser.value?.userId
|
||||
val chats = ArrayList(chatController.apiGetChats())
|
||||
/** Active user can be changed in background while [ChatController.apiGetChats] is executing */
|
||||
if (chatModel.currentUser.value?.userId == currentUserId) {
|
||||
val currentChatId = chatModel.chatId.value
|
||||
val oldStats = if (currentChatId != null) chatModel.getChat(currentChatId)?.chatStats else null
|
||||
if (oldStats != null) {
|
||||
val indexOfCurrentChat = chats.indexOfFirst { it.id == currentChatId }
|
||||
/** Pass old chatStats because unreadCounter can be changed already while [ChatController.apiGetChats] is executing */
|
||||
if (indexOfCurrentChat >= 0) chats[indexOfCurrentChat] = chats[indexOfCurrentChat].copy(chatStats = oldStats)
|
||||
}
|
||||
chatModel.updateChats(chats)
|
||||
}
|
||||
}.onFailure { Log.e(TAG, it.stackTraceToString()) }
|
||||
}
|
||||
}
|
||||
Lifecycle.Event.ON_RESUME -> {
|
||||
isAppOnForeground = true
|
||||
if (chatModel.onboardingStage.value == OnboardingStage.OnboardingComplete) {
|
||||
chatController.showBackgroundServiceNoticeIfNeeded()
|
||||
}
|
||||
/**
|
||||
* We're starting service here instead of in [Lifecycle.Event.ON_START] because
|
||||
* after calling [ChatController.showBackgroundServiceNoticeIfNeeded] notification mode in prefs can be changed.
|
||||
* It can happen when app was started and a user enables battery optimization while app in background
|
||||
* */
|
||||
if (chatModel.chatRunning.value != false &&
|
||||
chatModel.onboardingStage.value == OnboardingStage.OnboardingComplete &&
|
||||
appPreferences.notificationsMode.get() == NotificationsMode.SERVICE.name
|
||||
) {
|
||||
SimplexService.start(applicationContext)
|
||||
}
|
||||
}
|
||||
else -> isAppOnForeground = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun allowToStartServiceAfterAppExit() = with(chatModel.controller) {
|
||||
appPrefs.notificationsMode.get() == NotificationsMode.SERVICE.name &&
|
||||
(!NotificationsMode.SERVICE.requiresIgnoringBattery || isIgnoringBatteryOptimizations(chatModel.controller.appContext))
|
||||
}
|
||||
|
||||
private fun allowToStartPeriodically() = with(chatModel.controller) {
|
||||
appPrefs.notificationsMode.get() == NotificationsMode.PERIODIC.name &&
|
||||
(!NotificationsMode.PERIODIC.requiresIgnoringBattery || isIgnoringBatteryOptimizations(chatModel.controller.appContext))
|
||||
}
|
||||
|
||||
/*
|
||||
* It takes 1-10 milliseconds to process this function. Better to do it in a background thread
|
||||
* */
|
||||
fun schedulePeriodicServiceRestartWorker() = CoroutineScope(Dispatchers.Default).launch {
|
||||
if (!allowToStartServiceAfterAppExit()) {
|
||||
return@launch
|
||||
}
|
||||
val workerVersion = chatController.appPrefs.autoRestartWorkerVersion.get()
|
||||
val workPolicy = if (workerVersion == SimplexService.SERVICE_START_WORKER_VERSION) {
|
||||
Log.d(TAG, "ServiceStartWorker version matches: choosing KEEP as existing work policy")
|
||||
ExistingPeriodicWorkPolicy.KEEP
|
||||
} else {
|
||||
Log.d(TAG, "ServiceStartWorker version DOES NOT MATCH: choosing REPLACE as existing work policy")
|
||||
chatController.appPrefs.autoRestartWorkerVersion.set(SimplexService.SERVICE_START_WORKER_VERSION)
|
||||
ExistingPeriodicWorkPolicy.REPLACE
|
||||
}
|
||||
val work = PeriodicWorkRequestBuilder<SimplexService.ServiceStartWorker>(SimplexService.SERVICE_START_WORKER_INTERVAL_MINUTES, TimeUnit.MINUTES)
|
||||
.addTag(SimplexService.TAG)
|
||||
.addTag(SimplexService.SERVICE_START_WORKER_WORK_NAME_PERIODIC)
|
||||
.build()
|
||||
Log.d(TAG, "ServiceStartWorker: Scheduling period work every ${SimplexService.SERVICE_START_WORKER_INTERVAL_MINUTES} minutes")
|
||||
WorkManager.getInstance(context)?.enqueueUniquePeriodicWork(SimplexService.SERVICE_START_WORKER_WORK_NAME_PERIODIC, workPolicy, work)
|
||||
}
|
||||
|
||||
fun schedulePeriodicWakeUp() = CoroutineScope(Dispatchers.Default).launch {
|
||||
if (!allowToStartPeriodically()) {
|
||||
return@launch
|
||||
}
|
||||
MessagesFetcherWorker.scheduleWork()
|
||||
}
|
||||
|
||||
companion object {
|
||||
lateinit var context: SimplexApp private set
|
||||
|
||||
init {
|
||||
val socketName = BuildConfig.APPLICATION_ID + ".local.socket.address.listen.native.cmd2"
|
||||
val s = Semaphore(0)
|
||||
thread(name="stdout/stderr pipe") {
|
||||
Log.d(TAG, "starting server")
|
||||
var server: LocalServerSocket? = null
|
||||
for (i in 0..100) {
|
||||
try {
|
||||
server = LocalServerSocket(socketName + i)
|
||||
break
|
||||
} catch (e: IOException) {
|
||||
Log.e(TAG, e.stackTraceToString())
|
||||
}
|
||||
}
|
||||
if (server == null) {
|
||||
throw Error("Unable to setup local server socket. Contact developers")
|
||||
}
|
||||
Log.d(TAG, "started server")
|
||||
s.release()
|
||||
val receiver = server.accept()
|
||||
Log.d(TAG, "started receiver")
|
||||
val logbuffer = FifoQueue<String>(500)
|
||||
if (receiver != null) {
|
||||
val inStream = receiver.inputStream
|
||||
val inStreamReader = InputStreamReader(inStream)
|
||||
val input = BufferedReader(inStreamReader)
|
||||
Log.d(TAG, "starting receiver loop")
|
||||
while (true) {
|
||||
val line = input.readLine() ?: break
|
||||
Log.w("$TAG (stdout/stderr)", line)
|
||||
logbuffer.add(line)
|
||||
}
|
||||
Log.w(TAG, "exited receiver loop")
|
||||
}
|
||||
}
|
||||
|
||||
System.loadLibrary("app-lib")
|
||||
|
||||
s.acquire()
|
||||
pipeStdOutToSocket(socketName)
|
||||
|
||||
initHS()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class FifoQueue<E>(private var capacity: Int) : LinkedList<E>() {
|
||||
override fun add(element: E): Boolean {
|
||||
if(size > capacity) removeFirst()
|
||||
return super.add(element)
|
||||
}
|
||||
}
|
||||
@@ -1,76 +0,0 @@
|
||||
package chat.simplex.app.ui.theme
|
||||
|
||||
import android.app.UiModeManager
|
||||
import android.content.Context
|
||||
import androidx.compose.foundation.isSystemInDarkTheme
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.unit.dp
|
||||
import chat.simplex.app.SimplexApp
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
|
||||
enum class DefaultTheme {
|
||||
SYSTEM, DARK, LIGHT
|
||||
}
|
||||
|
||||
val DEFAULT_PADDING = 16.dp
|
||||
val DEFAULT_SPACE_AFTER_ICON = 4.dp
|
||||
val DEFAULT_PADDING_HALF = DEFAULT_PADDING / 2
|
||||
|
||||
val DarkColorPalette = darkColors(
|
||||
primary = SimplexBlue, // If this value changes also need to update #0088ff in string resource files
|
||||
primaryVariant = SimplexGreen,
|
||||
secondary = DarkGray,
|
||||
// background = Color.Black,
|
||||
// surface = Color.Black,
|
||||
// background = Color(0xFF121212),
|
||||
// surface = Color(0xFF121212),
|
||||
// error = Color(0xFFCF6679),
|
||||
onBackground = Color(0xFFFFFBFA),
|
||||
onSurface = Color(0xFFFFFBFA),
|
||||
// onError: Color = Color.Black,
|
||||
)
|
||||
val LightColorPalette = lightColors(
|
||||
primary = SimplexBlue, // If this value changes also need to update #0088ff in string resource files
|
||||
primaryVariant = SimplexGreen,
|
||||
secondary = LightGray,
|
||||
// background = Color.White,
|
||||
// surface = Color.White
|
||||
// onPrimary = Color.White,
|
||||
// onSecondary = Color.Black,
|
||||
// onBackground = Color.Black,
|
||||
// onSurface = Color.Black,
|
||||
)
|
||||
|
||||
val CurrentColors: MutableStateFlow<Pair<Colors, DefaultTheme>> = MutableStateFlow(ThemeManager.currentColors(isInNightMode()))
|
||||
|
||||
// Non-@Composable implementation
|
||||
private fun isInNightMode() =
|
||||
(SimplexApp.context.getSystemService(Context.UI_MODE_SERVICE) as UiModeManager).nightMode == UiModeManager.MODE_NIGHT_YES
|
||||
|
||||
@Composable
|
||||
fun isInDarkTheme(): Boolean = !CurrentColors.collectAsState().value.first.isLight
|
||||
|
||||
@Composable
|
||||
fun SimpleXTheme(darkTheme: Boolean? = null, content: @Composable () -> Unit) {
|
||||
LaunchedEffect(darkTheme) {
|
||||
// For preview
|
||||
if (darkTheme != null)
|
||||
CurrentColors.value = ThemeManager.currentColors(darkTheme)
|
||||
}
|
||||
val systemDark = isSystemInDarkTheme()
|
||||
LaunchedEffect(systemDark) {
|
||||
if (CurrentColors.value.second == DefaultTheme.SYSTEM && CurrentColors.value.first.isLight == systemDark) {
|
||||
// Change active colors from light to dark and back based on system theme
|
||||
ThemeManager.applyTheme(DefaultTheme.SYSTEM.name, systemDark)
|
||||
}
|
||||
}
|
||||
val theme by CurrentColors.collectAsState()
|
||||
MaterialTheme(
|
||||
colors = theme.first,
|
||||
typography = Typography,
|
||||
shapes = Shapes,
|
||||
content = content
|
||||
)
|
||||
}
|
||||
@@ -1,64 +0,0 @@
|
||||
package chat.simplex.app.ui.theme
|
||||
|
||||
import androidx.compose.material.Colors
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.toArgb
|
||||
import chat.simplex.app.R
|
||||
import chat.simplex.app.SimplexApp
|
||||
import chat.simplex.app.model.AppPreferences
|
||||
import chat.simplex.app.views.helpers.generalGetString
|
||||
|
||||
object ThemeManager {
|
||||
private val appPrefs: AppPreferences by lazy {
|
||||
AppPreferences(SimplexApp.context)
|
||||
}
|
||||
|
||||
fun currentColors(darkForSystemTheme: Boolean): Pair<Colors, DefaultTheme> {
|
||||
val theme = appPrefs.currentTheme.get()!!
|
||||
val systemThemeColors = if (darkForSystemTheme) DarkColorPalette else LightColorPalette
|
||||
val res = when (theme) {
|
||||
DefaultTheme.SYSTEM.name -> Pair(systemThemeColors, DefaultTheme.SYSTEM)
|
||||
DefaultTheme.DARK.name -> Pair(DarkColorPalette, DefaultTheme.DARK)
|
||||
DefaultTheme.LIGHT.name -> Pair(LightColorPalette, DefaultTheme.LIGHT)
|
||||
else -> Pair(systemThemeColors, DefaultTheme.SYSTEM)
|
||||
}
|
||||
return res.copy(first = res.first.copy(primary = Color(appPrefs.primaryColor.get())))
|
||||
}
|
||||
|
||||
// colors, default theme enum, localized name of theme
|
||||
fun allThemes(darkForSystemTheme: Boolean): List<Triple<Colors, DefaultTheme, String>> {
|
||||
val allThemes = ArrayList<Triple<Colors, DefaultTheme, String>>()
|
||||
allThemes.add(
|
||||
Triple(
|
||||
if (darkForSystemTheme) DarkColorPalette else LightColorPalette,
|
||||
DefaultTheme.SYSTEM,
|
||||
generalGetString(R.string.theme_system)
|
||||
)
|
||||
)
|
||||
allThemes.add(
|
||||
Triple(
|
||||
LightColorPalette,
|
||||
DefaultTheme.LIGHT,
|
||||
generalGetString(R.string.theme_light)
|
||||
)
|
||||
)
|
||||
allThemes.add(
|
||||
Triple(
|
||||
DarkColorPalette,
|
||||
DefaultTheme.DARK,
|
||||
generalGetString(R.string.theme_dark)
|
||||
)
|
||||
)
|
||||
return allThemes
|
||||
}
|
||||
|
||||
fun applyTheme(name: String, darkForSystemTheme: Boolean) {
|
||||
appPrefs.currentTheme.set(name)
|
||||
CurrentColors.value = currentColors(darkForSystemTheme)
|
||||
}
|
||||
|
||||
fun saveAndApplyPrimaryColor(color: Color) {
|
||||
appPrefs.primaryColor.set(color.toArgb())
|
||||
CurrentColors.value = currentColors(!CurrentColors.value.first.isLight)
|
||||
}
|
||||
}
|
||||
@@ -1,151 +0,0 @@
|
||||
package chat.simplex.app.views
|
||||
|
||||
import androidx.compose.foundation.*
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.text.BasicTextField
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.ArrowBackIosNew
|
||||
import androidx.compose.material.icons.outlined.ArrowForwardIos
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.focus.focusRequester
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.SolidColor
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.input.KeyboardCapitalization
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import chat.simplex.app.*
|
||||
import chat.simplex.app.R
|
||||
import chat.simplex.app.model.ChatModel
|
||||
import chat.simplex.app.model.Profile
|
||||
import chat.simplex.app.ui.theme.*
|
||||
import chat.simplex.app.views.helpers.AppBarTitle
|
||||
import chat.simplex.app.views.helpers.withApi
|
||||
import chat.simplex.app.views.onboarding.OnboardingStage
|
||||
import chat.simplex.app.views.onboarding.ReadableText
|
||||
import com.google.accompanist.insets.navigationBarsWithImePadding
|
||||
import kotlinx.coroutines.delay
|
||||
|
||||
fun isValidDisplayName(name: String) : Boolean {
|
||||
return (name.firstOrNull { it.isWhitespace() }) == null
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun CreateProfilePanel(chatModel: ChatModel, close: () -> Unit) {
|
||||
val displayName = remember { mutableStateOf("") }
|
||||
val fullName = remember { mutableStateOf("") }
|
||||
val focusRequester = remember { FocusRequester() }
|
||||
|
||||
Surface(Modifier.background(MaterialTheme.colors.onBackground)) {
|
||||
Column(
|
||||
modifier = Modifier.fillMaxSize().verticalScroll(rememberScrollState())
|
||||
) {
|
||||
AppBarTitle(stringResource(R.string.create_profile), false)
|
||||
ReadableText(R.string.your_profile_is_stored_on_your_device)
|
||||
ReadableText(R.string.profile_is_only_shared_with_your_contacts)
|
||||
Spacer(Modifier.height(10.dp))
|
||||
Text(
|
||||
stringResource(R.string.display_name),
|
||||
style = MaterialTheme.typography.h6,
|
||||
modifier = Modifier.padding(bottom = 3.dp)
|
||||
)
|
||||
ProfileNameField(displayName, focusRequester)
|
||||
val errorText = if (!isValidDisplayName(displayName.value)) stringResource(R.string.display_name_cannot_contain_whitespace) else ""
|
||||
Text(
|
||||
errorText,
|
||||
fontSize = 15.sp,
|
||||
color = MaterialTheme.colors.error
|
||||
)
|
||||
Spacer(Modifier.height(3.dp))
|
||||
Text(
|
||||
stringResource(R.string.full_name_optional__prompt),
|
||||
style = MaterialTheme.typography.h6,
|
||||
modifier = Modifier.padding(bottom = 5.dp)
|
||||
)
|
||||
ProfileNameField(fullName)
|
||||
Spacer(Modifier.fillMaxHeight().weight(1f))
|
||||
Row {
|
||||
if (chatModel.users.isEmpty()) {
|
||||
SimpleButton(
|
||||
text = stringResource(R.string.about_simplex),
|
||||
icon = Icons.Outlined.ArrowBackIosNew
|
||||
) { chatModel.onboardingStage.value = OnboardingStage.Step1_SimpleXInfo }
|
||||
}
|
||||
|
||||
Spacer(Modifier.fillMaxWidth().weight(1f))
|
||||
|
||||
val enabled = displayName.value.isNotEmpty() && isValidDisplayName(displayName.value)
|
||||
val createModifier: Modifier
|
||||
val createColor: Color
|
||||
if (enabled) {
|
||||
createModifier = Modifier.clickable { createProfile(chatModel, displayName.value, fullName.value, close) }.padding(8.dp)
|
||||
createColor = MaterialTheme.colors.primary
|
||||
} else {
|
||||
createModifier = Modifier.padding(8.dp)
|
||||
createColor = HighOrLowlight
|
||||
}
|
||||
Surface(shape = RoundedCornerShape(20.dp)) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically, modifier = createModifier) {
|
||||
Text(stringResource(R.string.create_profile_button), style = MaterialTheme.typography.caption, color = createColor)
|
||||
Icon(Icons.Outlined.ArrowForwardIos, stringResource(R.string.create_profile_button), tint = createColor)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
delay(300)
|
||||
focusRequester.requestFocus()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun createProfile(chatModel: ChatModel, displayName: String, fullName: String, close: () -> Unit) {
|
||||
withApi {
|
||||
val user = chatModel.controller.apiCreateActiveUser(
|
||||
Profile(displayName, fullName, null)
|
||||
) ?: return@withApi
|
||||
chatModel.currentUser.value = user
|
||||
if (chatModel.users.isEmpty()) {
|
||||
chatModel.controller.startChat(user)
|
||||
chatModel.onboardingStage.value = OnboardingStage.Step3_SetNotificationsMode
|
||||
SimplexApp.context.chatModel.controller.ntfManager.createNtfChannelsMaybeShowAlert()
|
||||
} else {
|
||||
val users = chatModel.controller.listUsers()
|
||||
chatModel.users.clear()
|
||||
chatModel.users.addAll(users)
|
||||
chatModel.controller.getUserChatData()
|
||||
close()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ProfileNameField(name: MutableState<String>, focusRequester: FocusRequester? = null) {
|
||||
val modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.background(MaterialTheme.colors.secondary)
|
||||
.height(40.dp)
|
||||
.clip(RoundedCornerShape(5.dp))
|
||||
.padding(8.dp)
|
||||
.navigationBarsWithImePadding()
|
||||
BasicTextField(
|
||||
value = name.value,
|
||||
onValueChange = { name.value = it },
|
||||
modifier = if (focusRequester == null) modifier else modifier.focusRequester(focusRequester),
|
||||
textStyle = MaterialTheme.typography.body1.copy(color = MaterialTheme.colors.onBackground),
|
||||
keyboardOptions = KeyboardOptions(
|
||||
capitalization = KeyboardCapitalization.None,
|
||||
autoCorrect = false
|
||||
),
|
||||
singleLine = true,
|
||||
cursorBrush = SolidColor(HighOrLowlight)
|
||||
)
|
||||
}
|
||||
@@ -1,471 +0,0 @@
|
||||
package chat.simplex.app.views.chat
|
||||
|
||||
import InfoRow
|
||||
import InfoRowEllipsis
|
||||
import SectionDivider
|
||||
import SectionItemView
|
||||
import SectionSpacer
|
||||
import SectionView
|
||||
import android.widget.Toast
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.foundation.*
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.text.KeyboardActions
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.*
|
||||
import androidx.compose.material.icons.outlined.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.ClipboardManager
|
||||
import androidx.compose.ui.platform.LocalClipboardManager
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import chat.simplex.app.R
|
||||
import chat.simplex.app.SimplexApp
|
||||
import chat.simplex.app.model.*
|
||||
import chat.simplex.app.ui.theme.*
|
||||
import chat.simplex.app.views.helpers.*
|
||||
import chat.simplex.app.views.usersettings.*
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.datetime.Clock
|
||||
|
||||
@Composable
|
||||
fun ChatInfoView(
|
||||
chatModel: ChatModel,
|
||||
contact: Contact,
|
||||
connStats: ConnectionStats?,
|
||||
customUserProfile: Profile?,
|
||||
localAlias: String,
|
||||
connectionCode: String?,
|
||||
close: () -> Unit,
|
||||
) {
|
||||
BackHandler(onBack = close)
|
||||
val chat = chatModel.chats.firstOrNull { it.id == chatModel.chatId.value }
|
||||
val developerTools = chatModel.controller.appPrefs.developerTools.get()
|
||||
if (chat != null) {
|
||||
val contactNetworkStatus = remember(chatModel.networkStatuses.toMap()) {
|
||||
mutableStateOf(chatModel.contactNetworkStatus(contact))
|
||||
}
|
||||
ChatInfoLayout(
|
||||
chat,
|
||||
contact,
|
||||
connStats,
|
||||
contactNetworkStatus.value,
|
||||
customUserProfile,
|
||||
localAlias,
|
||||
connectionCode,
|
||||
developerTools,
|
||||
onLocalAliasChanged = {
|
||||
setContactAlias(chat.chatInfo.apiId, it, chatModel)
|
||||
},
|
||||
openPreferences = {
|
||||
ModalManager.shared.showCustomModal { close ->
|
||||
val user = chatModel.currentUser.value
|
||||
if (user != null) {
|
||||
ContactPreferencesView(chatModel, user, contact.contactId, close)
|
||||
}
|
||||
}
|
||||
},
|
||||
deleteContact = { deleteContactDialog(chat.chatInfo, chatModel, close) },
|
||||
clearChat = { clearChatDialog(chat.chatInfo, chatModel, close) },
|
||||
switchContactAddress = {
|
||||
showSwitchContactAddressAlert(chatModel, contact.contactId)
|
||||
},
|
||||
verifyClicked = {
|
||||
ModalManager.shared.showModalCloseable { close ->
|
||||
remember { derivedStateOf { (chatModel.getContactChat(contact.contactId)?.chatInfo as? ChatInfo.Direct)?.contact } }.value?.let { ct ->
|
||||
VerifyCodeView(
|
||||
ct.displayName,
|
||||
connectionCode,
|
||||
ct.verified,
|
||||
verify = { code ->
|
||||
chatModel.controller.apiVerifyContact(ct.contactId, code)?.let { r ->
|
||||
val (verified, existingCode) = r
|
||||
chatModel.updateContact(
|
||||
ct.copy(
|
||||
activeConn = ct.activeConn.copy(
|
||||
connectionCode = if (verified) SecurityCode(existingCode, Clock.System.now()) else null
|
||||
)
|
||||
)
|
||||
)
|
||||
r
|
||||
}
|
||||
},
|
||||
close,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun deleteContactDialog(chatInfo: ChatInfo, chatModel: ChatModel, close: (() -> Unit)? = null) {
|
||||
AlertManager.shared.showAlertMsg(
|
||||
title = generalGetString(R.string.delete_contact_question),
|
||||
text = generalGetString(R.string.delete_contact_all_messages_deleted_cannot_undo_warning),
|
||||
confirmText = generalGetString(R.string.delete_verb),
|
||||
onConfirm = {
|
||||
withApi {
|
||||
val r = chatModel.controller.apiDeleteChat(chatInfo.chatType, chatInfo.apiId)
|
||||
if (r) {
|
||||
chatModel.removeChat(chatInfo.id)
|
||||
chatModel.chatId.value = null
|
||||
chatModel.controller.ntfManager.cancelNotificationsForChat(chatInfo.id)
|
||||
close?.invoke()
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
fun clearChatDialog(chatInfo: ChatInfo, chatModel: ChatModel, close: (() -> Unit)? = null) {
|
||||
AlertManager.shared.showAlertMsg(
|
||||
title = generalGetString(R.string.clear_chat_question),
|
||||
text = generalGetString(R.string.clear_chat_warning),
|
||||
confirmText = generalGetString(R.string.clear_verb),
|
||||
onConfirm = {
|
||||
withApi {
|
||||
val updatedChatInfo = chatModel.controller.apiClearChat(chatInfo.chatType, chatInfo.apiId)
|
||||
if (updatedChatInfo != null) {
|
||||
chatModel.clearChat(updatedChatInfo)
|
||||
chatModel.controller.ntfManager.cancelNotificationsForChat(chatInfo.id)
|
||||
close?.invoke()
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ChatInfoLayout(
|
||||
chat: Chat,
|
||||
contact: Contact,
|
||||
connStats: ConnectionStats?,
|
||||
contactNetworkStatus: NetworkStatus,
|
||||
customUserProfile: Profile?,
|
||||
localAlias: String,
|
||||
connectionCode: String?,
|
||||
developerTools: Boolean,
|
||||
onLocalAliasChanged: (String) -> Unit,
|
||||
openPreferences: () -> Unit,
|
||||
deleteContact: () -> Unit,
|
||||
clearChat: () -> Unit,
|
||||
switchContactAddress: () -> Unit,
|
||||
verifyClicked: () -> Unit,
|
||||
) {
|
||||
Column(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.verticalScroll(rememberScrollState()),
|
||||
horizontalAlignment = Alignment.Start
|
||||
) {
|
||||
Row(
|
||||
Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.Center
|
||||
) {
|
||||
ChatInfoHeader(chat.chatInfo, contact)
|
||||
}
|
||||
|
||||
LocalAliasEditor(localAlias, updateValue = onLocalAliasChanged)
|
||||
|
||||
if (customUserProfile != null) {
|
||||
SectionSpacer()
|
||||
SectionView(generalGetString(R.string.incognito).uppercase()) {
|
||||
InfoRow(generalGetString(R.string.incognito_random_profile), customUserProfile.chatViewName)
|
||||
}
|
||||
}
|
||||
|
||||
SectionSpacer()
|
||||
SectionView {
|
||||
if (connectionCode != null) {
|
||||
VerifyCodeButton(contact.verified, verifyClicked)
|
||||
SectionDivider()
|
||||
}
|
||||
ContactPreferencesButton(openPreferences)
|
||||
}
|
||||
|
||||
SectionSpacer()
|
||||
|
||||
SectionView(title = stringResource(R.string.conn_stats_section_title_servers)) {
|
||||
SwitchAddressButton(switchContactAddress)
|
||||
SectionDivider()
|
||||
if (connStats != null) {
|
||||
SectionItemView({
|
||||
AlertManager.shared.showAlertMsg(
|
||||
generalGetString(R.string.network_status),
|
||||
contactNetworkStatus.statusExplanation
|
||||
)}) {
|
||||
NetworkStatusRow(contactNetworkStatus)
|
||||
}
|
||||
val rcvServers = connStats.rcvServers
|
||||
if (rcvServers != null && rcvServers.isNotEmpty()) {
|
||||
SectionDivider()
|
||||
SimplexServers(stringResource(R.string.receiving_via), rcvServers)
|
||||
}
|
||||
val sndServers = connStats.sndServers
|
||||
if (sndServers != null && sndServers.isNotEmpty()) {
|
||||
SectionDivider()
|
||||
SimplexServers(stringResource(R.string.sending_via), sndServers)
|
||||
}
|
||||
}
|
||||
}
|
||||
SectionSpacer()
|
||||
SectionView {
|
||||
ClearChatButton(clearChat)
|
||||
SectionDivider()
|
||||
DeleteContactButton(deleteContact)
|
||||
}
|
||||
SectionSpacer()
|
||||
|
||||
if (developerTools) {
|
||||
SectionView(title = stringResource(R.string.section_title_for_console)) {
|
||||
InfoRow(stringResource(R.string.info_row_local_name), chat.chatInfo.localDisplayName)
|
||||
SectionDivider()
|
||||
InfoRow(stringResource(R.string.info_row_database_id), chat.chatInfo.apiId.toString())
|
||||
}
|
||||
SectionSpacer()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ChatInfoHeader(cInfo: ChatInfo, contact: Contact) {
|
||||
Column(
|
||||
Modifier.padding(horizontal = 8.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
ChatInfoImage(cInfo, size = 192.dp, iconColor = if (isInDarkTheme()) GroupDark else SettingsSecondaryLight)
|
||||
Row(Modifier.padding(bottom = 8.dp), verticalAlignment = Alignment.CenterVertically) {
|
||||
if (contact.verified) {
|
||||
Icon(Icons.Outlined.VerifiedUser, null, Modifier.padding(end = 6.dp, top = 4.dp).size(24.dp), tint = HighOrLowlight)
|
||||
}
|
||||
Text(
|
||||
contact.profile.displayName, style = MaterialTheme.typography.h1.copy(fontWeight = FontWeight.Normal),
|
||||
color = MaterialTheme.colors.onBackground,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
}
|
||||
if (cInfo.fullName != "" && cInfo.fullName != cInfo.displayName && cInfo.fullName != contact.profile.displayName) {
|
||||
Text(
|
||||
cInfo.fullName, style = MaterialTheme.typography.h2,
|
||||
color = MaterialTheme.colors.onBackground,
|
||||
maxLines = 2,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun LocalAliasEditor(
|
||||
initialValue: String,
|
||||
center: Boolean = true,
|
||||
leadingIcon: Boolean = false,
|
||||
focus: Boolean = false,
|
||||
updateValue: (String) -> Unit
|
||||
) {
|
||||
var value by rememberSaveable { mutableStateOf(initialValue) }
|
||||
val modifier = if (center)
|
||||
Modifier.padding(horizontal = if (!leadingIcon) DEFAULT_PADDING else 0.dp).widthIn(min = 100.dp)
|
||||
else
|
||||
Modifier.padding(horizontal = if (!leadingIcon) DEFAULT_PADDING else 0.dp).fillMaxWidth()
|
||||
Row(Modifier.fillMaxWidth(), horizontalArrangement = if (center) Arrangement.Center else Arrangement.Start) {
|
||||
DefaultBasicTextField(
|
||||
modifier,
|
||||
value,
|
||||
{
|
||||
Text(
|
||||
generalGetString(R.string.text_field_set_contact_placeholder),
|
||||
textAlign = if (center) TextAlign.Center else TextAlign.Start,
|
||||
color = HighOrLowlight
|
||||
)
|
||||
},
|
||||
leadingIcon = if (leadingIcon) {
|
||||
{ Icon(Icons.Default.Edit, null, Modifier.padding(start = 7.dp)) }
|
||||
} else null,
|
||||
color = HighOrLowlight,
|
||||
focus = focus,
|
||||
textStyle = TextStyle.Default.copy(textAlign = if (value.isEmpty() || !center) TextAlign.Start else TextAlign.Center),
|
||||
keyboardActions = KeyboardActions(onDone = { updateValue(value) })
|
||||
) {
|
||||
value = it
|
||||
}
|
||||
}
|
||||
LaunchedEffect(Unit) {
|
||||
snapshotFlow { value }
|
||||
.onEach { delay(500) } // wait a little after every new character, don't emit until user stops typing
|
||||
.conflate() // get the latest value
|
||||
.filter { it == value } // don't process old ones
|
||||
.collect {
|
||||
updateValue(value)
|
||||
}
|
||||
}
|
||||
DisposableEffect(Unit) {
|
||||
onDispose { updateValue(value) } // just in case snapshotFlow will be canceled when user presses Back too fast
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun NetworkStatusRow(networkStatus: NetworkStatus) {
|
||||
Row(
|
||||
Modifier.fillMaxSize(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp)
|
||||
) {
|
||||
Text(stringResource(R.string.network_status))
|
||||
Icon(
|
||||
Icons.Outlined.Info,
|
||||
stringResource(R.string.network_status),
|
||||
tint = MaterialTheme.colors.primary
|
||||
)
|
||||
}
|
||||
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp)
|
||||
) {
|
||||
Text(
|
||||
networkStatus.statusString,
|
||||
color = HighOrLowlight
|
||||
)
|
||||
ServerImage(networkStatus)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ServerImage(networkStatus: NetworkStatus) {
|
||||
Box(Modifier.size(18.dp)) {
|
||||
when (networkStatus) {
|
||||
is NetworkStatus.Connected ->
|
||||
Icon(Icons.Filled.Circle, stringResource(R.string.icon_descr_server_status_connected), tint = MaterialTheme.colors.primaryVariant)
|
||||
is NetworkStatus.Disconnected ->
|
||||
Icon(Icons.Filled.Pending, stringResource(R.string.icon_descr_server_status_disconnected), tint = HighOrLowlight)
|
||||
is NetworkStatus.Error ->
|
||||
Icon(Icons.Filled.Error, stringResource(R.string.icon_descr_server_status_error), tint = HighOrLowlight)
|
||||
else -> Icon(Icons.Outlined.Circle, stringResource(R.string.icon_descr_server_status_pending), tint = HighOrLowlight)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SimplexServers(text: String, servers: List<String>) {
|
||||
val info = servers.joinToString(separator = ", ") { it.substringAfter("@") }
|
||||
val clipboardManager: ClipboardManager = LocalClipboardManager.current
|
||||
InfoRowEllipsis(text, info) {
|
||||
clipboardManager.setText(AnnotatedString(servers.joinToString(separator = ",")))
|
||||
Toast.makeText(SimplexApp.context, generalGetString(R.string.copied), Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SwitchAddressButton(onClick: () -> Unit) {
|
||||
SectionItemView(onClick) {
|
||||
Text(stringResource(R.string.switch_receiving_address), color = MaterialTheme.colors.primary)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun VerifyCodeButton(contactVerified: Boolean, onClick: () -> Unit) {
|
||||
SettingsActionItem(
|
||||
if (contactVerified) Icons.Outlined.VerifiedUser else Icons.Outlined.Shield,
|
||||
stringResource(if (contactVerified) R.string.view_security_code else R.string.verify_security_code),
|
||||
click = onClick,
|
||||
iconColor = HighOrLowlight,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ContactPreferencesButton(onClick: () -> Unit) {
|
||||
SettingsActionItem(
|
||||
Icons.Outlined.ToggleOn,
|
||||
stringResource(R.string.contact_preferences),
|
||||
click = onClick
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ClearChatButton(onClick: () -> Unit) {
|
||||
SettingsActionItem(
|
||||
Icons.Outlined.Restore,
|
||||
stringResource(R.string.clear_chat_button),
|
||||
click = onClick,
|
||||
textColor = WarningOrange,
|
||||
iconColor = WarningOrange,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun DeleteContactButton(onClick: () -> Unit) {
|
||||
SettingsActionItem(
|
||||
Icons.Outlined.Delete,
|
||||
stringResource(R.string.button_delete_contact),
|
||||
click = onClick,
|
||||
textColor = Color.Red,
|
||||
iconColor = Color.Red,
|
||||
)
|
||||
}
|
||||
|
||||
private fun setContactAlias(contactApiId: Long, localAlias: String, chatModel: ChatModel) = withApi {
|
||||
chatModel.controller.apiSetContactAlias(contactApiId, localAlias)?.let {
|
||||
chatModel.updateContact(it)
|
||||
}
|
||||
}
|
||||
|
||||
private fun showSwitchContactAddressAlert(m: ChatModel, contactId: Long) {
|
||||
AlertManager.shared.showAlertMsg(
|
||||
title = generalGetString(R.string.switch_receiving_address_question),
|
||||
text = generalGetString(R.string.switch_receiving_address_desc),
|
||||
confirmText = generalGetString(R.string.switch_verb),
|
||||
onConfirm = {
|
||||
switchContactAddress(m, contactId)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private fun switchContactAddress(m: ChatModel, contactId: Long) = withApi {
|
||||
m.controller.apiSwitchContact(contactId)
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun PreviewChatInfoLayout() {
|
||||
SimpleXTheme {
|
||||
ChatInfoLayout(
|
||||
chat = Chat(
|
||||
chatInfo = ChatInfo.Direct.sampleData,
|
||||
chatItems = arrayListOf()
|
||||
),
|
||||
Contact.sampleData,
|
||||
localAlias = "",
|
||||
connectionCode = "123",
|
||||
developerTools = false,
|
||||
connStats = null,
|
||||
contactNetworkStatus = NetworkStatus.Connected(),
|
||||
onLocalAliasChanged = {},
|
||||
customUserProfile = null,
|
||||
openPreferences = {},
|
||||
deleteContact = {},
|
||||
clearChat = {},
|
||||
switchContactAddress = {},
|
||||
verifyClicked = {},
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
package chat.simplex.app.views.chat
|
||||
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyRow
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.Close
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.asImageBitmap
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import chat.simplex.app.R
|
||||
import chat.simplex.app.ui.theme.DEFAULT_PADDING_HALF
|
||||
import chat.simplex.app.views.chat.item.SentColorLight
|
||||
import chat.simplex.app.views.helpers.base64ToBitmap
|
||||
|
||||
@Composable
|
||||
fun ComposeImageView(images: List<String>, cancelImages: () -> Unit, cancelEnabled: Boolean) {
|
||||
Row(
|
||||
Modifier
|
||||
.padding(top = 8.dp)
|
||||
.background(SentColorLight),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
LazyRow(
|
||||
Modifier.weight(1f).padding(start = DEFAULT_PADDING_HALF, end = if (cancelEnabled) 0.dp else DEFAULT_PADDING_HALF),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(DEFAULT_PADDING_HALF),
|
||||
) {
|
||||
items(images.size) { index ->
|
||||
val imageBitmap = base64ToBitmap(images[index]).asImageBitmap()
|
||||
Image(
|
||||
imageBitmap,
|
||||
"preview image",
|
||||
modifier = Modifier.widthIn(max = 80.dp).height(60.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
if (cancelEnabled) {
|
||||
IconButton(onClick = cancelImages) {
|
||||
Icon(
|
||||
Icons.Outlined.Close,
|
||||
contentDescription = stringResource(R.string.icon_descr_cancel_image_preview),
|
||||
tint = MaterialTheme.colors.primary,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,133 +0,0 @@
|
||||
import androidx.compose.animation.core.Animatable
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.*
|
||||
import androidx.compose.material.icons.outlined.Close
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import chat.simplex.app.R
|
||||
import chat.simplex.app.model.durationText
|
||||
import chat.simplex.app.ui.theme.*
|
||||
import chat.simplex.app.views.chat.item.SentColorLight
|
||||
import chat.simplex.app.views.helpers.*
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
|
||||
@Composable
|
||||
fun ComposeVoiceView(
|
||||
filePath: String,
|
||||
recordedDurationMs: Int,
|
||||
finishedRecording: Boolean,
|
||||
cancelEnabled: Boolean,
|
||||
cancelVoice: () -> Unit
|
||||
) {
|
||||
BoxWithConstraints(Modifier
|
||||
.fillMaxWidth()
|
||||
) {
|
||||
val audioPlaying = rememberSaveable { mutableStateOf(false) }
|
||||
val progress = rememberSaveable { mutableStateOf(0) }
|
||||
val duration = rememberSaveable(recordedDurationMs) { mutableStateOf(recordedDurationMs) }
|
||||
val progressBarWidth = remember { Animatable(0f) }
|
||||
LaunchedEffect(recordedDurationMs, finishedRecording) {
|
||||
snapshotFlow { progress.value }
|
||||
.distinctUntilChanged()
|
||||
.collect {
|
||||
val startTime = when {
|
||||
finishedRecording -> progress.value
|
||||
else -> recordedDurationMs
|
||||
}
|
||||
val endTime = when {
|
||||
finishedRecording -> duration.value
|
||||
audioPlaying.value -> recordedDurationMs
|
||||
else -> MAX_VOICE_MILLIS_FOR_SENDING
|
||||
}
|
||||
val to = ((startTime.toDouble() / endTime) * maxWidth.value).dp
|
||||
progressBarWidth.animateTo(to.value, audioProgressBarAnimationSpec())
|
||||
}
|
||||
}
|
||||
Spacer(
|
||||
Modifier
|
||||
.requiredWidth(progressBarWidth.value.dp)
|
||||
.padding(top = 58.dp)
|
||||
.height(3.dp)
|
||||
.background(MaterialTheme.colors.primary)
|
||||
)
|
||||
Row(
|
||||
Modifier
|
||||
.height(60.dp)
|
||||
.fillMaxWidth()
|
||||
.padding(top = 8.dp)
|
||||
.background(SentColorLight),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
IconButton(
|
||||
onClick = {
|
||||
if (!audioPlaying.value) {
|
||||
AudioPlayer.play(filePath, audioPlaying, progress, duration, false)
|
||||
} else {
|
||||
AudioPlayer.pause(audioPlaying, progress)
|
||||
}
|
||||
},
|
||||
enabled = finishedRecording) {
|
||||
Icon(
|
||||
if (audioPlaying.value) Icons.Filled.Pause else Icons.Filled.PlayArrow,
|
||||
stringResource(R.string.icon_descr_file),
|
||||
Modifier
|
||||
.padding(start = 4.dp, end = 2.dp)
|
||||
.size(36.dp),
|
||||
tint = if (finishedRecording) MaterialTheme.colors.primary else HighOrLowlight
|
||||
)
|
||||
}
|
||||
val numberInText = remember(recordedDurationMs, progress.value) {
|
||||
derivedStateOf {
|
||||
when {
|
||||
finishedRecording && progress.value == 0 && !audioPlaying.value -> duration.value / 1000
|
||||
finishedRecording -> progress.value / 1000
|
||||
else -> recordedDurationMs / 1000
|
||||
}
|
||||
}
|
||||
}
|
||||
Text(
|
||||
durationText(numberInText.value),
|
||||
fontSize = 18.sp,
|
||||
color = HighOrLowlight,
|
||||
)
|
||||
Spacer(Modifier.weight(1f))
|
||||
if (cancelEnabled) {
|
||||
IconButton(
|
||||
onClick = {
|
||||
AudioPlayer.stop(filePath)
|
||||
cancelVoice()
|
||||
},
|
||||
modifier = Modifier.padding(0.dp)
|
||||
) {
|
||||
Icon(
|
||||
Icons.Outlined.Close,
|
||||
contentDescription = stringResource(R.string.icon_descr_cancel_file_preview),
|
||||
tint = MaterialTheme.colors.primary,
|
||||
modifier = Modifier.padding(10.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun PreviewComposeAudioView() {
|
||||
SimpleXTheme {
|
||||
ComposeFileView(
|
||||
"test.txt",
|
||||
cancelFile = {},
|
||||
cancelEnabled = true
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
package chat.simplex.app.views.chat
|
||||
|
||||
import android.Manifest
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import chat.simplex.app.R
|
||||
import chat.simplex.app.ui.theme.DEFAULT_PADDING
|
||||
import chat.simplex.app.views.helpers.*
|
||||
import chat.simplex.app.views.newchat.QRCodeScanner
|
||||
import com.google.accompanist.permissions.rememberPermissionState
|
||||
|
||||
@Composable
|
||||
fun ScanCodeView(verifyCode: (String?, cb: (Boolean) -> Unit) -> Unit, close: () -> Unit) {
|
||||
val cameraPermissionState = rememberPermissionState(permission = Manifest.permission.CAMERA)
|
||||
LaunchedEffect(Unit) {
|
||||
cameraPermissionState.launchPermissionRequest()
|
||||
}
|
||||
ScanCodeLayout(verifyCode, close)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ScanCodeLayout(verifyCode: (String?, cb: (Boolean) -> Unit) -> Unit, close: () -> Unit) {
|
||||
Column(
|
||||
Modifier
|
||||
.fillMaxSize()
|
||||
.padding(horizontal = DEFAULT_PADDING)
|
||||
) {
|
||||
AppBarTitle(stringResource(R.string.scan_code), false)
|
||||
Box(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.aspectRatio(ratio = 1F)
|
||||
.padding(bottom = DEFAULT_PADDING)
|
||||
) {
|
||||
QRCodeScanner { text ->
|
||||
verifyCode(text) {
|
||||
if (it) {
|
||||
close()
|
||||
} else {
|
||||
AlertManager.shared.showAlertMsg(
|
||||
title = generalGetString(R.string.incorrect_code)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Text(stringResource(R.string.scan_code_from_contacts_app))
|
||||
}
|
||||
}
|
||||
@@ -1,133 +0,0 @@
|
||||
package chat.simplex.app.views.chat.group
|
||||
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material.CircularProgressIndicator
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import chat.simplex.app.R
|
||||
import chat.simplex.app.model.ChatModel
|
||||
import chat.simplex.app.model.GroupInfo
|
||||
import chat.simplex.app.ui.theme.*
|
||||
import chat.simplex.app.views.helpers.*
|
||||
import chat.simplex.app.views.newchat.QRCode
|
||||
|
||||
@Composable
|
||||
fun GroupLinkView(chatModel: ChatModel, groupInfo: GroupInfo, connReqContact: String?, onGroupLinkUpdated: (String?) -> Unit) {
|
||||
var groupLink by rememberSaveable { mutableStateOf(connReqContact) }
|
||||
var creatingLink by rememberSaveable { mutableStateOf(false) }
|
||||
val cxt = LocalContext.current
|
||||
fun createLink() {
|
||||
creatingLink = true
|
||||
withApi {
|
||||
groupLink = chatModel.controller.apiCreateGroupLink(groupInfo.groupId)
|
||||
onGroupLinkUpdated(groupLink)
|
||||
creatingLink = false
|
||||
}
|
||||
}
|
||||
LaunchedEffect(Unit) {
|
||||
if (groupLink == null && !creatingLink) {
|
||||
createLink()
|
||||
}
|
||||
}
|
||||
GroupLinkLayout(
|
||||
groupLink = groupLink,
|
||||
creatingLink,
|
||||
createLink = ::createLink,
|
||||
share = { shareText(cxt, groupLink ?: return@GroupLinkLayout) },
|
||||
deleteLink = {
|
||||
AlertManager.shared.showAlertMsg(
|
||||
title = generalGetString(R.string.delete_link_question),
|
||||
text = generalGetString(R.string.all_group_members_will_remain_connected),
|
||||
confirmText = generalGetString(R.string.delete_verb),
|
||||
onConfirm = {
|
||||
withApi {
|
||||
val r = chatModel.controller.apiDeleteGroupLink(groupInfo.groupId)
|
||||
if (r) {
|
||||
groupLink = null
|
||||
onGroupLinkUpdated(null)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
)
|
||||
if (creatingLink) {
|
||||
ProgressIndicator()
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun GroupLinkLayout(
|
||||
groupLink: String?,
|
||||
creatingLink: Boolean,
|
||||
createLink: () -> Unit,
|
||||
share: () -> Unit,
|
||||
deleteLink: () -> Unit
|
||||
) {
|
||||
Column(
|
||||
Modifier.padding(horizontal = DEFAULT_PADDING),
|
||||
horizontalAlignment = Alignment.Start,
|
||||
verticalArrangement = Arrangement.Top
|
||||
) {
|
||||
AppBarTitle(stringResource(R.string.group_link), false)
|
||||
Text(
|
||||
stringResource(R.string.you_can_share_group_link_anybody_will_be_able_to_connect),
|
||||
Modifier.padding(bottom = 12.dp),
|
||||
lineHeight = 22.sp
|
||||
)
|
||||
Column(
|
||||
Modifier.fillMaxWidth(),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.SpaceEvenly
|
||||
) {
|
||||
if (groupLink == null) {
|
||||
SimpleButton(stringResource(R.string.button_create_group_link), icon = Icons.Outlined.AddLink, disabled = creatingLink, click = createLink)
|
||||
} else {
|
||||
QRCode(groupLink, Modifier.weight(1f, fill = false).aspectRatio(1f))
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(10.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.padding(vertical = 10.dp)
|
||||
) {
|
||||
SimpleButton(
|
||||
stringResource(R.string.share_link),
|
||||
icon = Icons.Outlined.Share,
|
||||
click = share
|
||||
)
|
||||
SimpleButton(
|
||||
stringResource(R.string.delete_link),
|
||||
icon = Icons.Outlined.Delete,
|
||||
color = Color.Red,
|
||||
click = deleteLink
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ProgressIndicator() {
|
||||
Box(
|
||||
Modifier.fillMaxSize(),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
CircularProgressIndicator(
|
||||
Modifier
|
||||
.padding(horizontal = 2.dp)
|
||||
.size(30.dp),
|
||||
color = HighOrLowlight,
|
||||
strokeWidth = 2.5.dp
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,375 +0,0 @@
|
||||
package chat.simplex.app.views.chat.group
|
||||
|
||||
import InfoRow
|
||||
import SectionDivider
|
||||
import SectionItemView
|
||||
import SectionSpacer
|
||||
import SectionView
|
||||
import android.util.Log
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.foundation.*
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import chat.simplex.app.R
|
||||
import chat.simplex.app.TAG
|
||||
import chat.simplex.app.model.*
|
||||
import chat.simplex.app.ui.theme.*
|
||||
import chat.simplex.app.views.chat.*
|
||||
import chat.simplex.app.views.helpers.*
|
||||
import chat.simplex.app.views.usersettings.SettingsActionItem
|
||||
import kotlinx.datetime.Clock
|
||||
|
||||
@Composable
|
||||
fun GroupMemberInfoView(
|
||||
groupInfo: GroupInfo,
|
||||
member: GroupMember,
|
||||
connStats: ConnectionStats?,
|
||||
connectionCode: String?,
|
||||
chatModel: ChatModel,
|
||||
close: () -> Unit,
|
||||
closeAll: () -> Unit, // Close all open windows up to ChatView
|
||||
) {
|
||||
BackHandler(onBack = close)
|
||||
val chat = chatModel.chats.firstOrNull { it.id == chatModel.chatId.value }
|
||||
val developerTools = chatModel.controller.appPrefs.developerTools.get()
|
||||
if (chat != null) {
|
||||
val newRole = remember { mutableStateOf(member.memberRole) }
|
||||
GroupMemberInfoLayout(
|
||||
groupInfo,
|
||||
member,
|
||||
connStats,
|
||||
newRole,
|
||||
developerTools,
|
||||
connectionCode,
|
||||
getContactChat = { chatModel.getContactChat(it) },
|
||||
knownDirectChat = {
|
||||
withApi {
|
||||
chatModel.chatItems.clear()
|
||||
chatModel.chatItems.addAll(it.chatItems)
|
||||
chatModel.chatId.value = it.chatInfo.id
|
||||
closeAll()
|
||||
}
|
||||
},
|
||||
newDirectChat = {
|
||||
withApi {
|
||||
val c = chatModel.controller.apiGetChat(ChatType.Direct, it)
|
||||
if (c != null) {
|
||||
chatModel.addChat(c)
|
||||
chatModel.chatItems.clear()
|
||||
chatModel.chatId.value = c.id
|
||||
closeAll()
|
||||
}
|
||||
}
|
||||
},
|
||||
removeMember = { removeMemberDialog(groupInfo, member, chatModel, close) },
|
||||
onRoleSelected = {
|
||||
if (it == newRole.value) return@GroupMemberInfoLayout
|
||||
val prevValue = newRole.value
|
||||
newRole.value = it
|
||||
updateMemberRoleDialog(it, member, onDismiss = {
|
||||
newRole.value = prevValue
|
||||
}) {
|
||||
withApi {
|
||||
kotlin.runCatching {
|
||||
val mem = chatModel.controller.apiMemberRole(groupInfo.groupId, member.groupMemberId, it)
|
||||
chatModel.upsertGroupMember(groupInfo, mem)
|
||||
}.onFailure {
|
||||
newRole.value = prevValue
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
switchMemberAddress = {
|
||||
switchMemberAddress(chatModel, groupInfo, member)
|
||||
},
|
||||
verifyClicked = {
|
||||
ModalManager.shared.showModalCloseable { close ->
|
||||
remember { derivedStateOf { chatModel.groupMembers.firstOrNull { it.memberId == member.memberId } } }.value?.let { mem ->
|
||||
VerifyCodeView(
|
||||
mem.displayName,
|
||||
connectionCode,
|
||||
mem.verified,
|
||||
verify = { code ->
|
||||
chatModel.controller.apiVerifyGroupMember(mem.groupId, mem.groupMemberId, code)?.let { r ->
|
||||
val (verified, existingCode) = r
|
||||
chatModel.upsertGroupMember(
|
||||
groupInfo,
|
||||
mem.copy(
|
||||
activeConn = mem.activeConn?.copy(
|
||||
connectionCode = if (verified) SecurityCode(existingCode, Clock.System.now()) else null
|
||||
)
|
||||
)
|
||||
)
|
||||
r
|
||||
}
|
||||
},
|
||||
close,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun removeMemberDialog(groupInfo: GroupInfo, member: GroupMember, chatModel: ChatModel, close: (() -> Unit)? = null) {
|
||||
AlertManager.shared.showAlertMsg(
|
||||
title = generalGetString(R.string.button_remove_member),
|
||||
text = generalGetString(R.string.member_will_be_removed_from_group_cannot_be_undone),
|
||||
confirmText = generalGetString(R.string.remove_member_confirmation),
|
||||
onConfirm = {
|
||||
withApi {
|
||||
val removedMember = chatModel.controller.apiRemoveMember(member.groupId, member.groupMemberId)
|
||||
if (removedMember != null) {
|
||||
chatModel.upsertGroupMember(groupInfo, removedMember)
|
||||
}
|
||||
close?.invoke()
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun GroupMemberInfoLayout(
|
||||
groupInfo: GroupInfo,
|
||||
member: GroupMember,
|
||||
connStats: ConnectionStats?,
|
||||
newRole: MutableState<GroupMemberRole>,
|
||||
developerTools: Boolean,
|
||||
connectionCode: String?,
|
||||
getContactChat: (Long) -> Chat?,
|
||||
knownDirectChat: (Chat) -> Unit,
|
||||
newDirectChat: (Long) -> Unit,
|
||||
removeMember: () -> Unit,
|
||||
onRoleSelected: (GroupMemberRole) -> Unit,
|
||||
switchMemberAddress: () -> Unit,
|
||||
verifyClicked: () -> Unit,
|
||||
) {
|
||||
Column(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.verticalScroll(rememberScrollState()),
|
||||
horizontalAlignment = Alignment.Start
|
||||
) {
|
||||
Row(
|
||||
Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.Center
|
||||
) {
|
||||
GroupMemberInfoHeader(member)
|
||||
}
|
||||
SectionSpacer()
|
||||
|
||||
if (member.memberActive) {
|
||||
val contactId = member.memberContactId
|
||||
if (contactId != null) {
|
||||
SectionView {
|
||||
val chat = getContactChat(contactId)
|
||||
if (chat != null && chat.chatInfo is ChatInfo.Direct && chat.chatInfo.contact.directOrUsed) {
|
||||
OpenChatButton(onClick = { knownDirectChat(chat) })
|
||||
if (connectionCode != null) {
|
||||
SectionDivider()
|
||||
}
|
||||
} else if (groupInfo.fullGroupPreferences.directMessages.on) {
|
||||
OpenChatButton(onClick = { newDirectChat(contactId) })
|
||||
if (connectionCode != null) {
|
||||
SectionDivider()
|
||||
}
|
||||
}
|
||||
if (connectionCode != null) {
|
||||
VerifyCodeButton(member.verified, verifyClicked)
|
||||
}
|
||||
}
|
||||
SectionSpacer()
|
||||
}
|
||||
}
|
||||
|
||||
SectionView(title = stringResource(R.string.member_info_section_title_member)) {
|
||||
InfoRow(stringResource(R.string.info_row_group), groupInfo.displayName)
|
||||
SectionDivider()
|
||||
val roles = remember { member.canChangeRoleTo(groupInfo) }
|
||||
if (roles != null) {
|
||||
SectionItemView {
|
||||
RoleSelectionRow(roles, newRole, onRoleSelected)
|
||||
}
|
||||
} else {
|
||||
InfoRow(stringResource(R.string.role_in_group), member.memberRole.text)
|
||||
}
|
||||
val conn = member.activeConn
|
||||
if (conn != null) {
|
||||
SectionDivider()
|
||||
val connLevelDesc =
|
||||
if (conn.connLevel == 0) stringResource(R.string.conn_level_desc_direct)
|
||||
else String.format(generalGetString(R.string.conn_level_desc_indirect), conn.connLevel)
|
||||
InfoRow(stringResource(R.string.info_row_connection), connLevelDesc)
|
||||
}
|
||||
}
|
||||
SectionSpacer()
|
||||
if (connStats != null) {
|
||||
SectionView(title = stringResource(R.string.conn_stats_section_title_servers)) {
|
||||
SwitchAddressButton(switchMemberAddress)
|
||||
SectionDivider()
|
||||
val rcvServers = connStats.rcvServers
|
||||
val sndServers = connStats.sndServers
|
||||
if ((rcvServers != null && rcvServers.isNotEmpty()) || (sndServers != null && sndServers.isNotEmpty())) {
|
||||
if (rcvServers != null && rcvServers.isNotEmpty()) {
|
||||
SimplexServers(stringResource(R.string.receiving_via), rcvServers)
|
||||
if (sndServers != null && sndServers.isNotEmpty()) {
|
||||
SectionDivider()
|
||||
SimplexServers(stringResource(R.string.sending_via), sndServers)
|
||||
}
|
||||
} else if (sndServers != null && sndServers.isNotEmpty()) {
|
||||
SimplexServers(stringResource(R.string.sending_via), sndServers)
|
||||
}
|
||||
}
|
||||
}
|
||||
SectionSpacer()
|
||||
}
|
||||
|
||||
if (member.canBeRemoved(groupInfo)) {
|
||||
SectionView {
|
||||
RemoveMemberButton(removeMember)
|
||||
}
|
||||
SectionSpacer()
|
||||
}
|
||||
|
||||
if (developerTools) {
|
||||
SectionView(title = stringResource(R.string.section_title_for_console)) {
|
||||
InfoRow(stringResource(R.string.info_row_local_name), member.localDisplayName)
|
||||
SectionDivider()
|
||||
InfoRow(stringResource(R.string.info_row_database_id), member.groupMemberId.toString())
|
||||
}
|
||||
SectionSpacer()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun GroupMemberInfoHeader(member: GroupMember) {
|
||||
Column(
|
||||
Modifier.padding(horizontal = 8.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
ProfileImage(size = 192.dp, member.image, color = if (isInDarkTheme()) GroupDark else SettingsSecondaryLight)
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
if (member.verified) {
|
||||
Icon(Icons.Outlined.VerifiedUser, null, Modifier.padding(end = 6.dp, top = 4.dp).size(24.dp), tint = HighOrLowlight)
|
||||
}
|
||||
Text(
|
||||
member.displayName, style = MaterialTheme.typography.h1.copy(fontWeight = FontWeight.Normal),
|
||||
color = MaterialTheme.colors.onBackground,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
}
|
||||
if (member.fullName != "" && member.fullName != member.displayName) {
|
||||
Text(
|
||||
member.fullName, style = MaterialTheme.typography.h2,
|
||||
color = MaterialTheme.colors.onBackground,
|
||||
maxLines = 2,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun RemoveMemberButton(onClick: () -> Unit) {
|
||||
SettingsActionItem(
|
||||
Icons.Outlined.Delete,
|
||||
stringResource(R.string.button_remove_member),
|
||||
click = onClick,
|
||||
textColor = Color.Red,
|
||||
iconColor = Color.Red,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun OpenChatButton(onClick: () -> Unit) {
|
||||
SettingsActionItem(
|
||||
Icons.Outlined.Message,
|
||||
stringResource(R.string.button_send_direct_message),
|
||||
click = onClick,
|
||||
textColor = MaterialTheme.colors.primary,
|
||||
iconColor = MaterialTheme.colors.primary,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun RoleSelectionRow(
|
||||
roles: List<GroupMemberRole>,
|
||||
selectedRole: MutableState<GroupMemberRole>,
|
||||
onSelected: (GroupMemberRole) -> Unit
|
||||
) {
|
||||
Row(
|
||||
Modifier.fillMaxWidth(),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
val values = remember { roles.map { it to it.text } }
|
||||
ExposedDropDownSettingRow(
|
||||
generalGetString(R.string.change_role),
|
||||
values,
|
||||
selectedRole,
|
||||
icon = null,
|
||||
enabled = remember { mutableStateOf(true) },
|
||||
onSelected = onSelected
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateMemberRoleDialog(
|
||||
newRole: GroupMemberRole,
|
||||
member: GroupMember,
|
||||
onDismiss: () -> Unit,
|
||||
onConfirm: () -> Unit
|
||||
) {
|
||||
AlertManager.shared.showAlertDialog(
|
||||
title = generalGetString(R.string.change_member_role_question),
|
||||
text = if (member.memberCurrent)
|
||||
String.format(generalGetString(R.string.member_role_will_be_changed_with_notification), newRole.text)
|
||||
else
|
||||
String.format(generalGetString(R.string.member_role_will_be_changed_with_invitation), newRole.text),
|
||||
confirmText = generalGetString(R.string.change_verb),
|
||||
onDismiss = onDismiss,
|
||||
onConfirm = onConfirm,
|
||||
onDismissRequest = onDismiss
|
||||
)
|
||||
}
|
||||
|
||||
private fun switchMemberAddress(m: ChatModel, groupInfo: GroupInfo, member: GroupMember) = withApi {
|
||||
m.controller.apiSwitchGroupMember(groupInfo.apiId, member.groupMemberId)
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun PreviewGroupMemberInfoLayout() {
|
||||
SimpleXTheme {
|
||||
GroupMemberInfoLayout(
|
||||
groupInfo = GroupInfo.sampleData,
|
||||
member = GroupMember.sampleData,
|
||||
connStats = null,
|
||||
newRole = remember { mutableStateOf(GroupMemberRole.Member) },
|
||||
developerTools = false,
|
||||
connectionCode = "123",
|
||||
getContactChat = { Chat.sampleData },
|
||||
knownDirectChat = {},
|
||||
newDirectChat = {},
|
||||
removeMember = {},
|
||||
onRoleSelected = {},
|
||||
switchMemberAddress = {},
|
||||
verifyClicked = {},
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,182 +0,0 @@
|
||||
package chat.simplex.app.views.chat.group
|
||||
|
||||
import android.content.res.Configuration
|
||||
import android.graphics.Bitmap
|
||||
import android.net.Uri
|
||||
import androidx.compose.foundation.*
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import chat.simplex.app.R
|
||||
import chat.simplex.app.model.*
|
||||
import chat.simplex.app.ui.theme.*
|
||||
import chat.simplex.app.views.ProfileNameField
|
||||
import chat.simplex.app.views.helpers.*
|
||||
import chat.simplex.app.views.isValidDisplayName
|
||||
import chat.simplex.app.views.usersettings.*
|
||||
import com.google.accompanist.insets.ProvideWindowInsets
|
||||
import com.google.accompanist.insets.navigationBarsWithImePadding
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@Composable
|
||||
fun GroupProfileView(groupInfo: GroupInfo, chatModel: ChatModel, close: () -> Unit) {
|
||||
GroupProfileLayout(
|
||||
close = close,
|
||||
groupProfile = groupInfo.groupProfile,
|
||||
saveProfile = { p ->
|
||||
withApi {
|
||||
val gInfo = chatModel.controller.apiUpdateGroup(groupInfo.groupId, p)
|
||||
if (gInfo != null) {
|
||||
chatModel.updateGroup(gInfo)
|
||||
close.invoke()
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun GroupProfileLayout(
|
||||
close: () -> Unit,
|
||||
groupProfile: GroupProfile,
|
||||
saveProfile: (GroupProfile) -> Unit,
|
||||
) {
|
||||
val bottomSheetModalState = rememberModalBottomSheetState(initialValue = ModalBottomSheetValue.Hidden)
|
||||
val displayName = remember { mutableStateOf(groupProfile.displayName) }
|
||||
val fullName = remember { mutableStateOf(groupProfile.fullName) }
|
||||
val chosenImage = rememberSaveable { mutableStateOf<Uri?>(null) }
|
||||
val profileImage = rememberSaveable { mutableStateOf(groupProfile.image) }
|
||||
val scope = rememberCoroutineScope()
|
||||
val scrollState = rememberScrollState()
|
||||
val focusRequester = remember { FocusRequester() }
|
||||
|
||||
ProvideWindowInsets(windowInsetsAnimationsEnabled = true) {
|
||||
ModalBottomSheetLayout(
|
||||
scrimColor = Color.Black.copy(alpha = 0.12F),
|
||||
modifier = Modifier.navigationBarsWithImePadding(),
|
||||
sheetContent = {
|
||||
GetImageBottomSheet(
|
||||
chosenImage,
|
||||
onImageChange = { bitmap -> profileImage.value = resizeImageToStrSize(cropToSquare(bitmap), maxDataSize = 12500) },
|
||||
hideBottomSheet = {
|
||||
scope.launch { bottomSheetModalState.hide() }
|
||||
})
|
||||
},
|
||||
sheetState = bottomSheetModalState,
|
||||
sheetShape = RoundedCornerShape(topStart = 18.dp, topEnd = 18.dp)
|
||||
) {
|
||||
ModalView(close = close) {
|
||||
Column(
|
||||
Modifier
|
||||
.verticalScroll(scrollState)
|
||||
.padding(horizontal = DEFAULT_PADDING),
|
||||
horizontalAlignment = Alignment.Start
|
||||
) {
|
||||
Text(
|
||||
stringResource(R.string.group_profile_is_stored_on_members_devices),
|
||||
Modifier.padding(bottom = 24.dp),
|
||||
color = MaterialTheme.colors.onBackground,
|
||||
lineHeight = 22.sp
|
||||
)
|
||||
Column(
|
||||
Modifier.fillMaxWidth(),
|
||||
horizontalAlignment = Alignment.Start
|
||||
) {
|
||||
Box(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(bottom = 24.dp),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Box(contentAlignment = Alignment.TopEnd) {
|
||||
Box(contentAlignment = Alignment.Center) {
|
||||
ProfileImage(192.dp, profileImage.value)
|
||||
EditImageButton { scope.launch { bottomSheetModalState.show() } }
|
||||
}
|
||||
if (profileImage.value != null) {
|
||||
DeleteImageButton { profileImage.value = null }
|
||||
}
|
||||
}
|
||||
}
|
||||
Text(
|
||||
stringResource(R.string.group_display_name_field),
|
||||
Modifier.padding(bottom = 3.dp)
|
||||
)
|
||||
ProfileNameField(displayName, focusRequester)
|
||||
val errorText = if (!isValidDisplayName(displayName.value)) stringResource(R.string.display_name_cannot_contain_whitespace) else ""
|
||||
Text(
|
||||
errorText,
|
||||
fontSize = 15.sp,
|
||||
color = MaterialTheme.colors.error
|
||||
)
|
||||
Spacer(Modifier.height(3.dp))
|
||||
Text(
|
||||
stringResource(R.string.group_full_name_field),
|
||||
Modifier.padding(bottom = 5.dp)
|
||||
)
|
||||
ProfileNameField(fullName)
|
||||
Spacer(Modifier.height(16.dp))
|
||||
Row {
|
||||
TextButton(stringResource(R.string.cancel_verb)) {
|
||||
close.invoke()
|
||||
}
|
||||
Spacer(Modifier.padding(horizontal = 8.dp))
|
||||
val enabled = displayName.value.isNotEmpty() && isValidDisplayName(displayName.value)
|
||||
if (enabled) {
|
||||
Text(
|
||||
stringResource(R.string.save_group_profile),
|
||||
modifier = Modifier.clickable {
|
||||
saveProfile(groupProfile.copy(
|
||||
displayName = displayName.value,
|
||||
fullName = fullName.value,
|
||||
image = profileImage.value
|
||||
))
|
||||
},
|
||||
color = MaterialTheme.colors.primary
|
||||
)
|
||||
} else {
|
||||
Text(
|
||||
stringResource(R.string.save_group_profile),
|
||||
color = HighOrLowlight
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
delay(300)
|
||||
focusRequester.requestFocus()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(showBackground = true)
|
||||
@Preview(
|
||||
uiMode = Configuration.UI_MODE_NIGHT_YES,
|
||||
showBackground = true,
|
||||
name = "Dark Mode"
|
||||
)
|
||||
@Composable
|
||||
fun PreviewGroupProfileLayout() {
|
||||
SimpleXTheme {
|
||||
GroupProfileLayout(
|
||||
close = {},
|
||||
groupProfile = GroupProfile.sampleData,
|
||||
saveProfile = { _ -> }
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,210 +0,0 @@
|
||||
package chat.simplex.app.views.chat.item
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import android.os.Build.VERSION.SDK_INT
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.combinedClickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material.CircularProgressIndicator
|
||||
import androidx.compose.material.Icon
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Check
|
||||
import androidx.compose.material.icons.outlined.ArrowDownward
|
||||
import androidx.compose.material.icons.outlined.MoreHoriz
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.asImageBitmap
|
||||
import androidx.compose.ui.graphics.painter.BitmapPainter
|
||||
import androidx.compose.ui.graphics.painter.Painter
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.layout.layoutId
|
||||
import androidx.compose.ui.platform.*
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.core.content.FileProvider
|
||||
import chat.simplex.app.*
|
||||
import chat.simplex.app.R
|
||||
import chat.simplex.app.model.CIFile
|
||||
import chat.simplex.app.model.CIFileStatus
|
||||
import chat.simplex.app.views.helpers.*
|
||||
import coil.ImageLoader
|
||||
import coil.compose.rememberAsyncImagePainter
|
||||
import coil.decode.GifDecoder
|
||||
import coil.decode.ImageDecoderDecoder
|
||||
import coil.request.ImageRequest
|
||||
import java.io.File
|
||||
|
||||
@Composable
|
||||
fun CIImageView(
|
||||
image: String,
|
||||
file: CIFile?,
|
||||
imageProvider: () -> ImageGalleryProvider,
|
||||
showMenu: MutableState<Boolean>,
|
||||
receiveFile: (Long) -> Unit
|
||||
) {
|
||||
@Composable
|
||||
fun loadingIndicator() {
|
||||
if (file != null) {
|
||||
Box(
|
||||
Modifier
|
||||
.padding(8.dp)
|
||||
.size(20.dp),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
when (file.fileStatus) {
|
||||
CIFileStatus.SndTransfer ->
|
||||
CircularProgressIndicator(
|
||||
Modifier.size(16.dp),
|
||||
color = Color.White,
|
||||
strokeWidth = 2.dp
|
||||
)
|
||||
CIFileStatus.SndComplete ->
|
||||
Icon(
|
||||
Icons.Filled.Check,
|
||||
stringResource(R.string.icon_descr_image_snd_complete),
|
||||
Modifier.fillMaxSize(),
|
||||
tint = Color.White
|
||||
)
|
||||
CIFileStatus.RcvAccepted ->
|
||||
Icon(
|
||||
Icons.Outlined.MoreHoriz,
|
||||
stringResource(R.string.icon_descr_waiting_for_image),
|
||||
Modifier.fillMaxSize(),
|
||||
tint = Color.White
|
||||
)
|
||||
CIFileStatus.RcvTransfer ->
|
||||
CircularProgressIndicator(
|
||||
Modifier.size(16.dp),
|
||||
color = Color.White,
|
||||
strokeWidth = 2.dp
|
||||
)
|
||||
CIFileStatus.RcvInvitation ->
|
||||
Icon(
|
||||
Icons.Outlined.ArrowDownward,
|
||||
stringResource(R.string.icon_descr_asked_to_receive),
|
||||
Modifier.fillMaxSize(),
|
||||
tint = Color.White
|
||||
)
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun imageViewFullWidth(): Dp {
|
||||
val approximatePadding = 100.dp
|
||||
return with(LocalDensity.current) { minOf(1000.dp, LocalView.current.width.toDp() - approximatePadding) }
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun imageView(imageBitmap: Bitmap, onClick: () -> Unit) {
|
||||
Image(
|
||||
imageBitmap.asImageBitmap(),
|
||||
contentDescription = stringResource(R.string.image_descr),
|
||||
// .width(1000.dp) is a hack for image to increase IntrinsicSize of FramedItemView
|
||||
// if text is short and take all available width if text is long
|
||||
modifier = Modifier
|
||||
.width(if (imageBitmap.width * 0.97 <= imageBitmap.height) imageViewFullWidth() * 0.75f else 1000.dp)
|
||||
.combinedClickable(
|
||||
onLongClick = { showMenu.value = true },
|
||||
onClick = onClick
|
||||
),
|
||||
contentScale = ContentScale.FillWidth,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun imageView(painter: Painter, onClick: () -> Unit) {
|
||||
Image(
|
||||
painter,
|
||||
contentDescription = stringResource(R.string.image_descr),
|
||||
// .width(1000.dp) is a hack for image to increase IntrinsicSize of FramedItemView
|
||||
// if text is short and take all available width if text is long
|
||||
modifier = Modifier
|
||||
.width(if (painter.intrinsicSize.width * 0.97 <= painter.intrinsicSize.height) imageViewFullWidth() * 0.75f else 1000.dp)
|
||||
.combinedClickable(
|
||||
onLongClick = { showMenu.value = true },
|
||||
onClick = onClick
|
||||
),
|
||||
contentScale = ContentScale.FillWidth,
|
||||
)
|
||||
}
|
||||
|
||||
fun fileSizeValid(): Boolean {
|
||||
if (file != null) {
|
||||
return file.fileSize <= MAX_FILE_SIZE
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
fun imageAndFilePath(file: CIFile?): Pair<Bitmap?, String?> {
|
||||
val imageBitmap: Bitmap? = getLoadedImage(SimplexApp.context, file)
|
||||
val filePath = getLoadedFilePath(SimplexApp.context, file)
|
||||
return imageBitmap to filePath
|
||||
}
|
||||
|
||||
Box(
|
||||
Modifier.layoutId(CHAT_IMAGE_LAYOUT_ID),
|
||||
contentAlignment = Alignment.TopEnd
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val (imageBitmap, filePath) = remember(file) { imageAndFilePath(file) }
|
||||
if (imageBitmap != null && filePath != null) {
|
||||
val uri = remember(filePath) { FileProvider.getUriForFile(context, "${BuildConfig.APPLICATION_ID}.provider", File(filePath)) }
|
||||
val imagePainter = rememberAsyncImagePainter(
|
||||
ImageRequest.Builder(context).data(data = uri).size(coil.size.Size.ORIGINAL).build(),
|
||||
placeholder = BitmapPainter(imageBitmap.asImageBitmap()), // show original image while it's still loading by coil
|
||||
imageLoader = imageLoader
|
||||
)
|
||||
val view = LocalView.current
|
||||
imageView(imagePainter, onClick = {
|
||||
hideKeyboard(view)
|
||||
if (getLoadedFilePath(context, file) != null) {
|
||||
ModalManager.shared.showCustomModal(animated = false) { close ->
|
||||
ImageFullScreenView(imageProvider, close)
|
||||
}
|
||||
}
|
||||
})
|
||||
} else {
|
||||
imageView(base64ToBitmap(image), onClick = {
|
||||
if (file != null) {
|
||||
when (file.fileStatus) {
|
||||
CIFileStatus.RcvInvitation ->
|
||||
if (fileSizeValid()) {
|
||||
receiveFile(file.fileId)
|
||||
} else {
|
||||
AlertManager.shared.showAlertMsg(
|
||||
generalGetString(R.string.large_file),
|
||||
String.format(generalGetString(R.string.contact_sent_large_file), formatBytes(MAX_FILE_SIZE))
|
||||
)
|
||||
}
|
||||
CIFileStatus.RcvAccepted ->
|
||||
AlertManager.shared.showAlertMsg(
|
||||
generalGetString(R.string.waiting_for_image),
|
||||
generalGetString(R.string.image_will_be_received_when_contact_is_online)
|
||||
)
|
||||
CIFileStatus.RcvTransfer -> {} // ?
|
||||
CIFileStatus.RcvComplete -> {} // ?
|
||||
CIFileStatus.RcvCancelled -> {} // TODO
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
loadingIndicator()
|
||||
}
|
||||
}
|
||||
|
||||
private val imageLoader = ImageLoader.Builder(SimplexApp.context)
|
||||
.components {
|
||||
if (SDK_INT >= 28) {
|
||||
add(ImageDecoderDecoder.Factory())
|
||||
} else {
|
||||
add(GifDecoder.Factory())
|
||||
}
|
||||
}
|
||||
.build()
|
||||
@@ -1,358 +0,0 @@
|
||||
package chat.simplex.app.views.chat.item
|
||||
|
||||
import androidx.compose.foundation.combinedClickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Edit
|
||||
import androidx.compose.material.icons.outlined.*
|
||||
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.vector.ImageVector
|
||||
import androidx.compose.ui.platform.*
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import chat.simplex.app.*
|
||||
import chat.simplex.app.R
|
||||
import chat.simplex.app.model.*
|
||||
import chat.simplex.app.ui.theme.HighOrLowlight
|
||||
import chat.simplex.app.ui.theme.SimpleXTheme
|
||||
import chat.simplex.app.views.chat.ComposeContextItem
|
||||
import chat.simplex.app.views.chat.ComposeState
|
||||
import chat.simplex.app.views.helpers.*
|
||||
import kotlinx.datetime.Clock
|
||||
|
||||
// TODO refactor so that FramedItemView can show all CIContent items if they're deleted (see Swift code)
|
||||
|
||||
@Composable
|
||||
fun ChatItemView(
|
||||
cInfo: ChatInfo,
|
||||
cItem: ChatItem,
|
||||
composeState: MutableState<ComposeState>,
|
||||
imageProvider: (() -> ImageGalleryProvider)? = null,
|
||||
showMember: Boolean = false,
|
||||
useLinkPreviews: Boolean,
|
||||
linkMode: SimplexLinkMode,
|
||||
deleteMessage: (Long, CIDeleteMode) -> Unit,
|
||||
receiveFile: (Long) -> Unit,
|
||||
joinGroup: (Long) -> Unit,
|
||||
acceptCall: (Contact) -> Unit,
|
||||
scrollToItem: (Long) -> Unit,
|
||||
acceptFeature: (Contact, ChatFeature, Int?) -> Unit
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val uriHandler = LocalUriHandler.current
|
||||
val sent = cItem.chatDir.sent
|
||||
val alignment = if (sent) Alignment.CenterEnd else Alignment.CenterStart
|
||||
val showMenu = remember { mutableStateOf(false) }
|
||||
val revealed = remember { mutableStateOf(false) }
|
||||
val fullDeleteAllowed = remember(cInfo) { cInfo.featureEnabled(ChatFeature.FullDelete) }
|
||||
val saveFileLauncher = rememberSaveFileLauncher(cxt = context, ciFile = cItem.file)
|
||||
val onLinkLongClick = { _: String -> showMenu.value = true }
|
||||
val live = composeState.value.liveMessage != null
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.padding(bottom = 4.dp)
|
||||
.fillMaxWidth(),
|
||||
contentAlignment = alignment,
|
||||
) {
|
||||
val onClick = {
|
||||
when (cItem.meta.itemStatus) {
|
||||
is CIStatus.SndErrorAuth -> {
|
||||
showMsgDeliveryErrorAlert(generalGetString(R.string.message_delivery_error_desc))
|
||||
}
|
||||
is CIStatus.SndError -> {
|
||||
showMsgDeliveryErrorAlert(generalGetString(R.string.unknown_error) + ": ${cItem.meta.itemStatus.agentError}")
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
Column(
|
||||
Modifier
|
||||
.clip(RoundedCornerShape(18.dp))
|
||||
.combinedClickable(onLongClick = { showMenu.value = true }, onClick = onClick),
|
||||
) {
|
||||
@Composable
|
||||
fun framedItemView() {
|
||||
FramedItemView(cInfo, cItem, uriHandler, imageProvider, showMember = showMember, linkMode = linkMode, showMenu, receiveFile, onLinkLongClick, scrollToItem)
|
||||
}
|
||||
|
||||
fun deleteMessageQuestionText(): String {
|
||||
return if (fullDeleteAllowed) {
|
||||
generalGetString(R.string.delete_message_cannot_be_undone_warning)
|
||||
} else {
|
||||
generalGetString(R.string.delete_message_mark_deleted_warning)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun MsgContentItemDropdownMenu() {
|
||||
DropdownMenu(
|
||||
expanded = showMenu.value,
|
||||
onDismissRequest = { showMenu.value = false },
|
||||
Modifier.width(220.dp)
|
||||
) {
|
||||
if (cItem.meta.itemDeleted == null && !live) {
|
||||
ItemAction(stringResource(R.string.reply_verb), Icons.Outlined.Reply, onClick = {
|
||||
if (composeState.value.editing) {
|
||||
composeState.value = ComposeState(contextItem = ComposeContextItem.QuotedItem(cItem), useLinkPreviews = useLinkPreviews)
|
||||
} else {
|
||||
composeState.value = composeState.value.copy(contextItem = ComposeContextItem.QuotedItem(cItem))
|
||||
}
|
||||
showMenu.value = false
|
||||
})
|
||||
}
|
||||
ItemAction(stringResource(R.string.share_verb), Icons.Outlined.Share, onClick = {
|
||||
val filePath = getLoadedFilePath(SimplexApp.context, cItem.file)
|
||||
when {
|
||||
filePath != null -> shareFile(context, cItem.text, filePath)
|
||||
else -> shareText(context, cItem.content.text)
|
||||
}
|
||||
showMenu.value = false
|
||||
})
|
||||
ItemAction(stringResource(R.string.copy_verb), Icons.Outlined.ContentCopy, onClick = {
|
||||
copyText(context, cItem.content.text)
|
||||
showMenu.value = false
|
||||
})
|
||||
if (cItem.content.msgContent is MsgContent.MCImage || cItem.content.msgContent is MsgContent.MCFile || cItem.content.msgContent is MsgContent.MCVoice) {
|
||||
val filePath = getLoadedFilePath(context, cItem.file)
|
||||
if (filePath != null) {
|
||||
ItemAction(stringResource(R.string.save_verb), Icons.Outlined.SaveAlt, onClick = {
|
||||
when (cItem.content.msgContent) {
|
||||
is MsgContent.MCImage -> saveImage(context, cItem.file)
|
||||
is MsgContent.MCFile -> saveFileLauncher.launch(cItem.file?.fileName)
|
||||
is MsgContent.MCVoice -> saveFileLauncher.launch(cItem.file?.fileName)
|
||||
else -> {}
|
||||
}
|
||||
showMenu.value = false
|
||||
})
|
||||
}
|
||||
}
|
||||
if (cItem.meta.editable && cItem.content.msgContent !is MsgContent.MCVoice && !live) {
|
||||
ItemAction(stringResource(R.string.edit_verb), Icons.Filled.Edit, onClick = {
|
||||
composeState.value = ComposeState(editingItem = cItem, useLinkPreviews = useLinkPreviews)
|
||||
showMenu.value = false
|
||||
})
|
||||
}
|
||||
if (cItem.meta.itemDeleted != null && revealed.value) {
|
||||
ItemAction(
|
||||
stringResource(R.string.hide_verb),
|
||||
Icons.Outlined.VisibilityOff,
|
||||
onClick = {
|
||||
revealed.value = false
|
||||
showMenu.value = false
|
||||
}
|
||||
)
|
||||
}
|
||||
if (!(live && cItem.meta.isLive)) {
|
||||
DeleteItemAction(cItem, showMenu, questionText = deleteMessageQuestionText(), deleteMessage)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun MarkedDeletedItemDropdownMenu() {
|
||||
DropdownMenu(
|
||||
expanded = showMenu.value,
|
||||
onDismissRequest = { showMenu.value = false },
|
||||
Modifier.width(220.dp)
|
||||
) {
|
||||
ItemAction(
|
||||
stringResource(R.string.reveal_verb),
|
||||
Icons.Outlined.Visibility,
|
||||
onClick = {
|
||||
revealed.value = true
|
||||
showMenu.value = false
|
||||
}
|
||||
)
|
||||
DeleteItemAction(cItem, showMenu, questionText = deleteMessageQuestionText(), deleteMessage)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ContentItem() {
|
||||
val mc = cItem.content.msgContent
|
||||
if (cItem.meta.itemDeleted != null && !revealed.value) {
|
||||
MarkedDeletedItemView(cItem, cInfo.timedMessagesTTL, showMember = showMember)
|
||||
MarkedDeletedItemDropdownMenu()
|
||||
} else if (cItem.quotedItem == null && cItem.meta.itemDeleted == null && !cItem.meta.isLive) {
|
||||
if (mc is MsgContent.MCText && isShortEmoji(cItem.content.text)) {
|
||||
EmojiItemView(cItem, cInfo.timedMessagesTTL)
|
||||
MsgContentItemDropdownMenu()
|
||||
} else if (mc is MsgContent.MCVoice && cItem.content.text.isEmpty()) {
|
||||
CIVoiceView(mc.duration, cItem.file, cItem.meta.itemEdited, cItem.chatDir.sent, hasText = false, cItem, cInfo.timedMessagesTTL, longClick = { onLinkLongClick("") })
|
||||
MsgContentItemDropdownMenu()
|
||||
} else {
|
||||
framedItemView()
|
||||
MsgContentItemDropdownMenu()
|
||||
}
|
||||
} else {
|
||||
framedItemView()
|
||||
MsgContentItemDropdownMenu()
|
||||
}
|
||||
}
|
||||
|
||||
@Composable fun DeletedItem() {
|
||||
DeletedItemView(cItem, cInfo.timedMessagesTTL, showMember = showMember)
|
||||
DropdownMenu(
|
||||
expanded = showMenu.value,
|
||||
onDismissRequest = { showMenu.value = false },
|
||||
Modifier.width(220.dp)
|
||||
) {
|
||||
DeleteItemAction(cItem, showMenu, questionText = deleteMessageQuestionText(), deleteMessage)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable fun CallItem(status: CICallStatus, duration: Int) {
|
||||
CICallItemView(cInfo, cItem, status, duration, acceptCall)
|
||||
}
|
||||
|
||||
when (val c = cItem.content) {
|
||||
is CIContent.SndMsgContent -> ContentItem()
|
||||
is CIContent.RcvMsgContent -> ContentItem()
|
||||
is CIContent.SndDeleted -> DeletedItem()
|
||||
is CIContent.RcvDeleted -> DeletedItem()
|
||||
is CIContent.SndCall -> CallItem(c.status, c.duration)
|
||||
is CIContent.RcvCall -> CallItem(c.status, c.duration)
|
||||
is CIContent.RcvIntegrityError -> IntegrityErrorItemView(cItem, cInfo.timedMessagesTTL, showMember = showMember)
|
||||
is CIContent.RcvGroupInvitation -> CIGroupInvitationView(cItem, c.groupInvitation, c.memberRole, joinGroup = joinGroup, chatIncognito = cInfo.incognito)
|
||||
is CIContent.SndGroupInvitation -> CIGroupInvitationView(cItem, c.groupInvitation, c.memberRole, joinGroup = joinGroup, chatIncognito = cInfo.incognito)
|
||||
is CIContent.RcvGroupEventContent -> CIEventView(cItem)
|
||||
is CIContent.SndGroupEventContent -> CIEventView(cItem)
|
||||
is CIContent.RcvConnEventContent -> CIEventView(cItem)
|
||||
is CIContent.SndConnEventContent -> CIEventView(cItem)
|
||||
is CIContent.RcvChatFeature -> CIChatFeatureView(cItem, c.feature, c.enabled.iconColor)
|
||||
is CIContent.SndChatFeature -> CIChatFeatureView(cItem, c.feature, c.enabled.iconColor)
|
||||
is CIContent.RcvChatPreference -> {
|
||||
val ct = if (cInfo is ChatInfo.Direct) cInfo.contact else null
|
||||
CIFeaturePreferenceView(cItem, ct, c.feature, c.allowed, acceptFeature)
|
||||
}
|
||||
is CIContent.SndChatPreference -> CIChatFeatureView(cItem, c.feature, HighOrLowlight, icon = c.feature.icon,)
|
||||
is CIContent.RcvGroupFeature -> CIChatFeatureView(cItem, c.groupFeature, c.preference.enable.iconColor)
|
||||
is CIContent.SndGroupFeature -> CIChatFeatureView(cItem, c.groupFeature, c.preference.enable.iconColor)
|
||||
is CIContent.RcvChatFeatureRejected -> CIChatFeatureView(cItem, c.feature, Color.Red)
|
||||
is CIContent.RcvGroupFeatureRejected -> CIChatFeatureView(cItem, c.groupFeature, Color.Red)
|
||||
is CIContent.SndModerated -> DeletedItem()
|
||||
is CIContent.RcvModerated -> DeletedItem()
|
||||
is CIContent.InvalidJSON -> CIInvalidJSONView(c.json)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun DeleteItemAction(
|
||||
cItem: ChatItem,
|
||||
showMenu: MutableState<Boolean>,
|
||||
questionText: String,
|
||||
deleteMessage: (Long, CIDeleteMode) -> Unit
|
||||
) {
|
||||
ItemAction(
|
||||
stringResource(R.string.delete_verb),
|
||||
Icons.Outlined.Delete,
|
||||
onClick = {
|
||||
showMenu.value = false
|
||||
deleteMessageAlertDialog(cItem, questionText, deleteMessage = deleteMessage)
|
||||
},
|
||||
color = Color.Red
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ItemAction(text: String, icon: ImageVector, onClick: () -> Unit, color: Color = MaterialTheme.colors.onBackground) {
|
||||
DropdownMenuItem(onClick) {
|
||||
Row {
|
||||
Text(
|
||||
text,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.weight(1F)
|
||||
.padding(end = 15.dp),
|
||||
color = color
|
||||
)
|
||||
Icon(icon, text, tint = color)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun deleteMessageAlertDialog(chatItem: ChatItem, questionText: String, deleteMessage: (Long, CIDeleteMode) -> Unit) {
|
||||
AlertManager.shared.showAlertDialogButtons(
|
||||
title = generalGetString(R.string.delete_message__question),
|
||||
text = questionText,
|
||||
buttons = {
|
||||
Row(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 8.dp, vertical = 2.dp),
|
||||
horizontalArrangement = Arrangement.End,
|
||||
) {
|
||||
TextButton(onClick = {
|
||||
deleteMessage(chatItem.id, CIDeleteMode.cidmInternal)
|
||||
AlertManager.shared.hideAlert()
|
||||
}) { Text(stringResource(R.string.for_me_only)) }
|
||||
if (chatItem.meta.editable) {
|
||||
Spacer(Modifier.padding(horizontal = 4.dp))
|
||||
TextButton(onClick = {
|
||||
deleteMessage(chatItem.id, CIDeleteMode.cidmBroadcast)
|
||||
AlertManager.shared.hideAlert()
|
||||
}) { Text(stringResource(R.string.for_everybody)) }
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private fun showMsgDeliveryErrorAlert(description: String) {
|
||||
AlertManager.shared.showAlertMsg(
|
||||
title = generalGetString(R.string.message_delivery_error_title),
|
||||
text = description,
|
||||
)
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun PreviewChatItemView() {
|
||||
SimpleXTheme {
|
||||
ChatItemView(
|
||||
ChatInfo.Direct.sampleData,
|
||||
ChatItem.getSampleData(
|
||||
1, CIDirection.DirectSnd(), Clock.System.now(), "hello"
|
||||
),
|
||||
useLinkPreviews = true,
|
||||
linkMode = SimplexLinkMode.DESCRIPTION,
|
||||
composeState = remember { mutableStateOf(ComposeState(useLinkPreviews = true)) },
|
||||
deleteMessage = { _, _ -> },
|
||||
receiveFile = {},
|
||||
joinGroup = {},
|
||||
acceptCall = { _ -> },
|
||||
scrollToItem = {},
|
||||
acceptFeature = { _, _, _ -> }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun PreviewChatItemViewDeletedContent() {
|
||||
SimpleXTheme {
|
||||
ChatItemView(
|
||||
ChatInfo.Direct.sampleData,
|
||||
ChatItem.getDeletedContentSampleData(),
|
||||
useLinkPreviews = true,
|
||||
linkMode = SimplexLinkMode.DESCRIPTION,
|
||||
composeState = remember { mutableStateOf(ComposeState(useLinkPreviews = true)) },
|
||||
deleteMessage = { _, _ -> },
|
||||
receiveFile = {},
|
||||
joinGroup = {},
|
||||
acceptCall = { _ -> },
|
||||
scrollToItem = {},
|
||||
acceptFeature = { _, _, _ -> }
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,153 +0,0 @@
|
||||
package chat.simplex.app.views.chat.item
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.foundation.*
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.*
|
||||
import androidx.compose.ui.graphics.painter.BitmapPainter
|
||||
import androidx.compose.ui.input.pointer.*
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.layout.onGloballyPositioned
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalView
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import chat.simplex.app.R
|
||||
import chat.simplex.app.views.helpers.*
|
||||
import coil.ImageLoader
|
||||
import coil.compose.rememberAsyncImagePainter
|
||||
import coil.decode.GifDecoder
|
||||
import coil.decode.ImageDecoderDecoder
|
||||
import coil.request.ImageRequest
|
||||
import coil.size.Size
|
||||
import com.google.accompanist.pager.*
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlin.math.absoluteValue
|
||||
|
||||
interface ImageGalleryProvider {
|
||||
val initialIndex: Int
|
||||
val totalImagesSize: MutableState<Int>
|
||||
fun getImage(index: Int): Pair<Bitmap, Uri>?
|
||||
fun currentPageChanged(index: Int)
|
||||
fun scrollToStart()
|
||||
fun onDismiss(index: Int)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalPagerApi::class)
|
||||
@Composable
|
||||
fun ImageFullScreenView(imageProvider: () -> ImageGalleryProvider, close: () -> Unit) {
|
||||
val provider = remember { imageProvider() }
|
||||
val pagerState = rememberPagerState(provider.initialIndex)
|
||||
val goBack = { provider.onDismiss(pagerState.currentPage); close() }
|
||||
BackHandler(onBack = goBack)
|
||||
// Pager doesn't ask previous page at initialization step who knows why. By not doing this, prev page is not checked and can be blank,
|
||||
// which makes this blank page visible for a moment. Prevent it by doing the check ourselves
|
||||
LaunchedEffect(Unit) {
|
||||
if (provider.getImage(provider.initialIndex - 1) == null) {
|
||||
provider.scrollToStart()
|
||||
pagerState.scrollToPage(0)
|
||||
}
|
||||
}
|
||||
val scope = rememberCoroutineScope()
|
||||
HorizontalPager(count = remember { provider.totalImagesSize }.value, state = pagerState) { index ->
|
||||
Column(
|
||||
Modifier
|
||||
.fillMaxSize()
|
||||
.background(Color.Black)
|
||||
.clickable(interactionSource = remember { MutableInteractionSource() }, indication = null, onClick = goBack)
|
||||
) {
|
||||
var settledCurrentPage by remember { mutableStateOf(pagerState.currentPage) }
|
||||
LaunchedEffect(pagerState) {
|
||||
snapshotFlow {
|
||||
if (!pagerState.isScrollInProgress) pagerState.currentPage else settledCurrentPage
|
||||
}.collect {
|
||||
settledCurrentPage = it
|
||||
}
|
||||
}
|
||||
LaunchedEffect(settledCurrentPage) {
|
||||
// Make this pager with infinity scrolling with only 3 pages at a time when left and right pages constructs in real time
|
||||
if (settledCurrentPage != provider.initialIndex)
|
||||
provider.currentPageChanged(index)
|
||||
}
|
||||
val image = provider.getImage(index)
|
||||
if (image == null) {
|
||||
// No such image. Let's shrink total pages size or scroll to start of the list of pages to remove blank page automatically
|
||||
SideEffect {
|
||||
scope.launch {
|
||||
when (settledCurrentPage) {
|
||||
index - 1 -> provider.totalImagesSize.value = settledCurrentPage + 1
|
||||
index + 1 -> {
|
||||
provider.scrollToStart()
|
||||
pagerState.scrollToPage(0)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
val (imageBitmap: Bitmap, uri: Uri) = image
|
||||
var scale by remember { mutableStateOf(1f) }
|
||||
var translationX by remember { mutableStateOf(0f) }
|
||||
var translationY by remember { mutableStateOf(0f) }
|
||||
var viewWidth by remember { mutableStateOf(0) }
|
||||
var allowTranslate by remember { mutableStateOf(true) }
|
||||
LaunchedEffect(settledCurrentPage) {
|
||||
scale = 1f
|
||||
translationX = 0f
|
||||
translationY = 0f
|
||||
}
|
||||
// I'm making a new instance of imageLoader here because if I use one instance in multiple places
|
||||
// after end of composition here a GIF from the first instance will be paused automatically which isn't what I want
|
||||
val imageLoader = ImageLoader.Builder(LocalContext.current)
|
||||
.components {
|
||||
if (Build.VERSION.SDK_INT >= 28) {
|
||||
add(ImageDecoderDecoder.Factory())
|
||||
} else {
|
||||
add(GifDecoder.Factory())
|
||||
}
|
||||
}
|
||||
.build()
|
||||
Image(
|
||||
rememberAsyncImagePainter(
|
||||
ImageRequest.Builder(LocalContext.current).data(data = uri).size(Size.ORIGINAL).build(),
|
||||
placeholder = BitmapPainter(imageBitmap.asImageBitmap()), // show original image while it's still loading by coil
|
||||
imageLoader = imageLoader
|
||||
),
|
||||
contentDescription = stringResource(R.string.image_descr),
|
||||
contentScale = ContentScale.Fit,
|
||||
modifier = Modifier
|
||||
.onGloballyPositioned {
|
||||
viewWidth = it.size.width
|
||||
}
|
||||
.graphicsLayer(
|
||||
scaleX = scale,
|
||||
scaleY = scale,
|
||||
translationX = translationX,
|
||||
translationY = translationY,
|
||||
)
|
||||
.pointerInput(Unit) {
|
||||
detectTransformGestures(
|
||||
{ allowTranslate },
|
||||
onGesture = { _, pan, gestureZoom, _ ->
|
||||
scale = (scale * gestureZoom).coerceIn(1f, 20f)
|
||||
allowTranslate = viewWidth * (scale - 1f) - ((translationX + pan.x * scale).absoluteValue * 2) > 0
|
||||
if (scale > 1 && allowTranslate) {
|
||||
translationX += pan.x * scale
|
||||
translationY += pan.y * scale
|
||||
} else if (allowTranslate) {
|
||||
translationX = 0f
|
||||
translationY = 0f
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
.fillMaxSize(),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,66 +0,0 @@
|
||||
package chat.simplex.app.views.chat.item
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.*
|
||||
import androidx.compose.ui.text.font.FontStyle
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import chat.simplex.app.R
|
||||
import chat.simplex.app.model.ChatItem
|
||||
import chat.simplex.app.ui.theme.SimpleXTheme
|
||||
import chat.simplex.app.views.helpers.AlertManager
|
||||
import chat.simplex.app.views.helpers.generalGetString
|
||||
|
||||
@Composable
|
||||
fun IntegrityErrorItemView(ci: ChatItem, timedMessagesTTL: Int?, showMember: Boolean = false) {
|
||||
Surface(
|
||||
Modifier.clickable(onClick = {
|
||||
AlertManager.shared.showAlertMsg(
|
||||
title = generalGetString(R.string.alert_title_skipped_messages),
|
||||
text = generalGetString(R.string.alert_text_skipped_messages_it_can_happen_when)
|
||||
)
|
||||
}),
|
||||
shape = RoundedCornerShape(18.dp),
|
||||
color = ReceivedColorLight,
|
||||
) {
|
||||
Row(
|
||||
Modifier.padding(horizontal = 12.dp, vertical = 6.dp),
|
||||
verticalAlignment = Alignment.Bottom
|
||||
) {
|
||||
Text(
|
||||
buildAnnotatedString {
|
||||
appendSender(this, if (showMember) ci.memberDisplayName else null, true)
|
||||
withStyle(SpanStyle(fontStyle = FontStyle.Italic, color = Color.Red)) { append(ci.content.text) }
|
||||
},
|
||||
style = MaterialTheme.typography.body1.copy(lineHeight = 22.sp),
|
||||
modifier = Modifier.padding(end = 8.dp)
|
||||
)
|
||||
CIMetaView(ci, timedMessagesTTL)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(showBackground = true)
|
||||
@Preview(
|
||||
uiMode = Configuration.UI_MODE_NIGHT_YES,
|
||||
name = "Dark Mode"
|
||||
)
|
||||
@Composable
|
||||
fun IntegrityErrorItemViewPreview() {
|
||||
SimpleXTheme {
|
||||
IntegrityErrorItemView(
|
||||
ChatItem.getDeletedContentSampleData(),
|
||||
null
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,59 +0,0 @@
|
||||
package chat.simplex.app.views.chat.item
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.*
|
||||
import androidx.compose.ui.text.font.FontStyle
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import chat.simplex.app.R
|
||||
import chat.simplex.app.model.CIDeleted
|
||||
import chat.simplex.app.model.ChatItem
|
||||
import chat.simplex.app.ui.theme.HighOrLowlight
|
||||
import chat.simplex.app.ui.theme.SimpleXTheme
|
||||
import chat.simplex.app.views.helpers.generalGetString
|
||||
|
||||
@Composable
|
||||
fun MarkedDeletedItemView(ci: ChatItem, timedMessagesTTL: Int?, showMember: Boolean = false) {
|
||||
Surface(
|
||||
shape = RoundedCornerShape(18.dp),
|
||||
color = if (ci.chatDir.sent) SentColorLight else ReceivedColorLight,
|
||||
) {
|
||||
Row(
|
||||
Modifier.padding(horizontal = 12.dp, vertical = 6.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(
|
||||
buildAnnotatedString {
|
||||
// appendSender(this, if (showMember) ci.memberDisplayName else null, true) // TODO font size
|
||||
withStyle(SpanStyle(fontSize = 12.sp, fontStyle = FontStyle.Italic, color = HighOrLowlight)) { append(generalGetString(R.string.marked_deleted_description)) }
|
||||
},
|
||||
style = MaterialTheme.typography.body1.copy(lineHeight = 22.sp),
|
||||
modifier = Modifier.padding(end = 8.dp)
|
||||
)
|
||||
CIMetaView(ci, timedMessagesTTL)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(showBackground = true)
|
||||
@Preview(
|
||||
uiMode = Configuration.UI_MODE_NIGHT_YES,
|
||||
name = "Dark Mode"
|
||||
)
|
||||
@Composable
|
||||
fun PreviewMarkedDeletedItemView() {
|
||||
SimpleXTheme {
|
||||
DeletedItemView(
|
||||
ChatItem.getSampleData(itemDeleted = CIDeleted.Deleted()),
|
||||
null
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,103 +0,0 @@
|
||||
package chat.simplex.app.views.chatlist
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.PersonAdd
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalUriHandler
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.SpanStyle
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import chat.simplex.app.R
|
||||
import chat.simplex.app.ui.theme.SimpleXTheme
|
||||
import chat.simplex.app.views.helpers.annotatedStringResource
|
||||
import chat.simplex.app.views.usersettings.MarkdownHelpView
|
||||
import chat.simplex.app.views.usersettings.simplexTeamUri
|
||||
|
||||
val bold = SpanStyle(fontWeight = FontWeight.Bold)
|
||||
|
||||
@Composable
|
||||
fun ChatHelpView(addContact: (() -> Unit)? = null) {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.Start,
|
||||
verticalArrangement = Arrangement.spacedBy(10.dp)
|
||||
) {
|
||||
val uriHandler = LocalUriHandler.current
|
||||
|
||||
Text(stringResource(R.string.thank_you_for_installing_simplex), lineHeight = 22.sp)
|
||||
Text(
|
||||
annotatedStringResource(R.string.you_can_connect_to_simplex_chat_founder),
|
||||
modifier = Modifier.clickable(onClick = {
|
||||
uriHandler.openUri(simplexTeamUri)
|
||||
}),
|
||||
lineHeight = 22.sp
|
||||
)
|
||||
|
||||
Column(
|
||||
Modifier.padding(top = 24.dp),
|
||||
horizontalAlignment = Alignment.Start,
|
||||
verticalArrangement = Arrangement.spacedBy(10.dp)
|
||||
) {
|
||||
Text(
|
||||
stringResource(R.string.to_start_a_new_chat_help_header),
|
||||
style = MaterialTheme.typography.h2,
|
||||
lineHeight = 22.sp
|
||||
)
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
Text(stringResource(R.string.chat_help_tap_button))
|
||||
Icon(
|
||||
Icons.Outlined.PersonAdd,
|
||||
stringResource(R.string.add_contact),
|
||||
modifier = if (addContact != null) Modifier.clickable(onClick = addContact) else Modifier,
|
||||
)
|
||||
Text(stringResource(R.string.above_then_preposition_continuation))
|
||||
}
|
||||
Text(annotatedStringResource(R.string.add_new_contact_to_create_one_time_QR_code), lineHeight = 22.sp)
|
||||
Text(annotatedStringResource(R.string.scan_QR_code_to_connect_to_contact_who_shows_QR_code), lineHeight = 22.sp)
|
||||
}
|
||||
|
||||
Column(
|
||||
Modifier.padding(top = 24.dp),
|
||||
horizontalAlignment = Alignment.Start,
|
||||
verticalArrangement = Arrangement.spacedBy(10.dp)
|
||||
) {
|
||||
Text(stringResource(R.string.to_connect_via_link_title), style = MaterialTheme.typography.h2)
|
||||
Text(stringResource(R.string.if_you_received_simplex_invitation_link_you_can_open_in_browser), lineHeight = 22.sp)
|
||||
Text(annotatedStringResource(R.string.desktop_scan_QR_code_from_app_via_scan_QR_code), lineHeight = 22.sp)
|
||||
Text(annotatedStringResource(R.string.mobile_tap_open_in_mobile_app_then_tap_connect_in_app), lineHeight = 22.sp)
|
||||
}
|
||||
|
||||
Column(
|
||||
Modifier.padding(vertical = 24.dp),
|
||||
horizontalAlignment = Alignment.Start,
|
||||
verticalArrangement = Arrangement.spacedBy(10.dp)
|
||||
) {
|
||||
Text(stringResource(R.string.markdown_in_messages), style = MaterialTheme.typography.h2)
|
||||
MarkdownHelpView()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(showBackground = true)
|
||||
@Preview(
|
||||
uiMode = Configuration.UI_MODE_NIGHT_YES,
|
||||
showBackground = true,
|
||||
name = "Dark Mode"
|
||||
)
|
||||
@Composable
|
||||
fun PreviewChatHelpLayout() {
|
||||
SimpleXTheme {
|
||||
ChatHelpView {}
|
||||
}
|
||||
}
|
||||
@@ -1,138 +0,0 @@
|
||||
package chat.simplex.app.views.chatlist
|
||||
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.foundation.*
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.*
|
||||
import androidx.compose.material.icons.outlined.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.capitalize
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.intl.Locale
|
||||
import androidx.compose.ui.unit.dp
|
||||
import chat.simplex.app.R
|
||||
import chat.simplex.app.model.*
|
||||
import chat.simplex.app.ui.theme.HighOrLowlight
|
||||
import chat.simplex.app.ui.theme.Indigo
|
||||
import chat.simplex.app.views.helpers.*
|
||||
|
||||
@Composable
|
||||
fun ShareListView(chatModel: ChatModel, stopped: Boolean) {
|
||||
var searchInList by rememberSaveable { mutableStateOf("") }
|
||||
Scaffold(
|
||||
topBar = { Column { ShareListToolbar(chatModel, stopped) { searchInList = it.trim() } } },
|
||||
) {
|
||||
Box(Modifier.padding(it)) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(MaterialTheme.colors.background)
|
||||
) {
|
||||
if (chatModel.chats.isNotEmpty()) {
|
||||
ShareList(chatModel, search = searchInList)
|
||||
} else {
|
||||
EmptyList()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun EmptyList() {
|
||||
Box {
|
||||
Text(stringResource(R.string.you_have_no_chats), Modifier.align(Alignment.Center), color = HighOrLowlight)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ShareListToolbar(chatModel: ChatModel, stopped: Boolean, onSearchValueChanged: (String) -> Unit) {
|
||||
var showSearch by rememberSaveable { mutableStateOf(false) }
|
||||
val hideSearchOnBack = { onSearchValueChanged(""); showSearch = false }
|
||||
if (showSearch) {
|
||||
BackHandler(onBack = hideSearchOnBack)
|
||||
}
|
||||
val barButtons = arrayListOf<@Composable RowScope.() -> Unit>()
|
||||
if (chatModel.chats.size >= 8) {
|
||||
barButtons.add {
|
||||
IconButton({ showSearch = true }) {
|
||||
Icon(Icons.Outlined.Search, stringResource(android.R.string.search_go).capitalize(Locale.current), tint = MaterialTheme.colors.primary)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (stopped) {
|
||||
barButtons.add {
|
||||
IconButton(onClick = {
|
||||
AlertManager.shared.showAlertMsg(
|
||||
generalGetString(R.string.chat_is_stopped_indication),
|
||||
generalGetString(R.string.you_can_start_chat_via_setting_or_by_restarting_the_app)
|
||||
)
|
||||
}) {
|
||||
Icon(
|
||||
Icons.Filled.Report,
|
||||
generalGetString(R.string.chat_is_stopped_indication),
|
||||
tint = Color.Red,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DefaultTopAppBar(
|
||||
navigationButton = { if (showSearch) NavigationButtonBack(hideSearchOnBack) else NavigationButtonBack { chatModel.sharedContent.value = null } },
|
||||
title = {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Text(
|
||||
when (chatModel.sharedContent.value) {
|
||||
is SharedContent.Text -> stringResource(R.string.share_message)
|
||||
is SharedContent.Images -> stringResource(R.string.share_image)
|
||||
is SharedContent.File -> stringResource(R.string.share_file)
|
||||
else -> stringResource(R.string.share_message)
|
||||
},
|
||||
color = MaterialTheme.colors.onBackground,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
)
|
||||
if (chatModel.incognito.value) {
|
||||
Icon(
|
||||
Icons.Filled.TheaterComedy,
|
||||
stringResource(R.string.incognito),
|
||||
tint = Indigo,
|
||||
modifier = Modifier.padding(10.dp).size(26.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
onTitleClick = null,
|
||||
showSearch = showSearch,
|
||||
onSearchValueChanged = onSearchValueChanged,
|
||||
buttons = barButtons
|
||||
)
|
||||
Divider()
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ShareList(chatModel: ChatModel, search: String) {
|
||||
val filter: (Chat) -> Boolean = { chat: Chat ->
|
||||
chat.chatInfo.chatViewName.lowercase().contains(search.lowercase())
|
||||
}
|
||||
val chats by remember(search) {
|
||||
derivedStateOf {
|
||||
if (search.isEmpty()) chatModel.chats.filter { it.chatInfo.ready } else chatModel.chats.filter { it.chatInfo.ready }.filter(filter)
|
||||
}
|
||||
}
|
||||
LazyColumn(
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
items(chats) { chat ->
|
||||
ShareListNavLinkView(chat, chatModel)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,198 +0,0 @@
|
||||
package chat.simplex.app.views.chatlist
|
||||
|
||||
import SectionItemViewSpaceBetween
|
||||
import android.util.Log
|
||||
import androidx.compose.animation.core.*
|
||||
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.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Done
|
||||
import androidx.compose.material.icons.outlined.Settings
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.*
|
||||
import androidx.compose.ui.draw.shadow
|
||||
import androidx.compose.ui.graphics.graphicsLayer
|
||||
import androidx.compose.ui.platform.LocalConfiguration
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.text.capitalize
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.intl.Locale
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.*
|
||||
import chat.simplex.app.R
|
||||
import chat.simplex.app.TAG
|
||||
import chat.simplex.app.model.*
|
||||
import chat.simplex.app.ui.theme.*
|
||||
import chat.simplex.app.views.helpers.*
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
@Composable
|
||||
fun UserPicker(chatModel: ChatModel, userPickerState: MutableStateFlow<AnimatedViewState>, switchingUsers: MutableState<Boolean>, openSettings: () -> Unit) {
|
||||
val scope = rememberCoroutineScope()
|
||||
var newChat by remember { mutableStateOf(userPickerState.value) }
|
||||
val users by remember { derivedStateOf { chatModel.users.sortedByDescending { it.user.activeUser } } }
|
||||
val animatedFloat = remember { Animatable(if (newChat.isVisible()) 0f else 1f) }
|
||||
LaunchedEffect(Unit) {
|
||||
launch {
|
||||
userPickerState.collect {
|
||||
newChat = it
|
||||
launch {
|
||||
animatedFloat.animateTo(if (newChat.isVisible()) 1f else 0f, newChatSheetAnimSpec())
|
||||
if (newChat.isHiding()) userPickerState.value = AnimatedViewState.GONE
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
LaunchedEffect(Unit) {
|
||||
snapshotFlow { newChat.isVisible() }
|
||||
.distinctUntilChanged()
|
||||
.filter { it }
|
||||
.collect {
|
||||
try {
|
||||
val updatedUsers = chatModel.controller.listUsers().sortedByDescending { it.user.activeUser }
|
||||
var same = users.size == updatedUsers.size
|
||||
if (same) {
|
||||
for (i in 0 until minOf(users.size, updatedUsers.size)) {
|
||||
val prev = updatedUsers[i].user
|
||||
val next = users[i].user
|
||||
if (prev.userId != next.userId || prev.activeUser != next.activeUser || prev.chatViewName != next.chatViewName || prev.image != next.image) {
|
||||
same = false
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!same) {
|
||||
chatModel.users.clear()
|
||||
chatModel.users.addAll(updatedUsers)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Error updating users ${e.stackTraceToString()}")
|
||||
}
|
||||
}
|
||||
}
|
||||
val xOffset = with(LocalDensity.current) { 10.dp.roundToPx() }
|
||||
val maxWidth = with(LocalDensity.current) { LocalConfiguration.current.screenWidthDp * density }
|
||||
Box(Modifier
|
||||
.fillMaxSize()
|
||||
.offset { IntOffset(if (newChat.isGone()) -maxWidth.roundToInt() else xOffset, 0) }
|
||||
.clickable(interactionSource = remember { MutableInteractionSource() }, indication = null, onClick = { userPickerState.value = AnimatedViewState.HIDING })
|
||||
.padding(bottom = 10.dp, top = 10.dp)
|
||||
.graphicsLayer {
|
||||
alpha = animatedFloat.value
|
||||
translationY = (animatedFloat.value - 1) * xOffset
|
||||
}
|
||||
) {
|
||||
Column(
|
||||
Modifier
|
||||
.widthIn(min = 220.dp)
|
||||
.width(IntrinsicSize.Min)
|
||||
.height(IntrinsicSize.Min)
|
||||
.shadow(8.dp, MaterialTheme.shapes.medium, clip = false)
|
||||
.background(MaterialTheme.colors.background, MaterialTheme.shapes.medium)
|
||||
) {
|
||||
Column(Modifier.weight(1f).verticalScroll(rememberScrollState())) {
|
||||
users.forEach { u ->
|
||||
UserProfilePickerItem(u.user, u.unreadCount, openSettings = {
|
||||
openSettings()
|
||||
userPickerState.value = AnimatedViewState.GONE
|
||||
}) {
|
||||
userPickerState.value = AnimatedViewState.HIDING
|
||||
if (!u.user.activeUser) {
|
||||
chatModel.chats.clear()
|
||||
scope.launch {
|
||||
val job = launch {
|
||||
delay(500)
|
||||
switchingUsers.value = true
|
||||
}
|
||||
chatModel.controller.changeActiveUser(u.user.userId)
|
||||
job.cancel()
|
||||
switchingUsers.value = false
|
||||
}
|
||||
}
|
||||
}
|
||||
Divider(Modifier.requiredHeight(1.dp))
|
||||
if (u.user.activeUser) Divider(Modifier.requiredHeight(0.5.dp))
|
||||
}
|
||||
}
|
||||
SettingsPickerItem {
|
||||
openSettings()
|
||||
userPickerState.value = AnimatedViewState.GONE
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun UserProfilePickerItem(u: User, unreadCount: Int = 0, onLongClick: () -> Unit = {}, openSettings: () -> Unit = {}, onClick: () -> Unit) {
|
||||
Row(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.sizeIn(minHeight = 46.dp)
|
||||
.combinedClickable(
|
||||
onClick = if (u.activeUser) openSettings else onClick,
|
||||
onLongClick = onLongClick,
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
indication = if (!u.activeUser) LocalIndication.current else null
|
||||
)
|
||||
.padding(PaddingValues(start = 8.dp, end = DEFAULT_PADDING)),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Row(
|
||||
Modifier
|
||||
.widthIn(max = LocalConfiguration.current.screenWidthDp.dp * 0.7f)
|
||||
.padding(vertical = 8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
ProfileImage(
|
||||
image = u.image,
|
||||
size = 54.dp
|
||||
)
|
||||
Text(
|
||||
u.displayName,
|
||||
modifier = Modifier
|
||||
.padding(start = 8.dp, end = 8.dp),
|
||||
fontWeight = if (u.activeUser) FontWeight.Medium else FontWeight.Normal
|
||||
)
|
||||
}
|
||||
if (u.activeUser) {
|
||||
Icon(Icons.Filled.Done, null, Modifier.size(20.dp), tint = MaterialTheme.colors.onBackground)
|
||||
} else if (unreadCount > 0) {
|
||||
Row {
|
||||
Text(
|
||||
unreadCountStr(unreadCount),
|
||||
color = MaterialTheme.colors.onPrimary,
|
||||
fontSize = 11.sp,
|
||||
modifier = Modifier
|
||||
.background(MaterialTheme.colors.primary, shape = CircleShape)
|
||||
.sizeIn(minWidth = 20.dp, minHeight = 20.dp)
|
||||
.padding(horizontal = 3.dp)
|
||||
.padding(vertical = 1.dp),
|
||||
textAlign = TextAlign.Center,
|
||||
maxLines = 1
|
||||
)
|
||||
Spacer(Modifier.width(2.dp))
|
||||
}
|
||||
} else {
|
||||
Box(Modifier.size(20.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SettingsPickerItem(onClick: () -> Unit) {
|
||||
SectionItemViewSpaceBetween(onClick, minHeight = 68.dp) {
|
||||
val text = generalGetString(R.string.settings_section_title_settings).lowercase().capitalize(Locale.current)
|
||||
Text(
|
||||
text,
|
||||
color = MaterialTheme.colors.onBackground,
|
||||
)
|
||||
Icon(Icons.Outlined.Settings, text, Modifier.size(20.dp), tint = MaterialTheme.colors.onBackground)
|
||||
}
|
||||
}
|
||||
@@ -1,142 +0,0 @@
|
||||
package chat.simplex.app.views.database
|
||||
|
||||
import SectionDivider
|
||||
import SectionTextFooter
|
||||
import SectionView
|
||||
import android.content.Context
|
||||
import android.content.res.Configuration
|
||||
import android.net.Uri
|
||||
import android.util.Log
|
||||
import android.widget.Toast
|
||||
import androidx.activity.compose.ManagedActivityResultLauncher
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import chat.simplex.app.R
|
||||
import chat.simplex.app.TAG
|
||||
import chat.simplex.app.model.ChatModel
|
||||
import chat.simplex.app.ui.theme.SimpleXTheme
|
||||
import chat.simplex.app.views.helpers.*
|
||||
import chat.simplex.app.views.usersettings.*
|
||||
import kotlinx.datetime.*
|
||||
import java.io.BufferedOutputStream
|
||||
import java.io.File
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
|
||||
@Composable
|
||||
fun ChatArchiveView(m: ChatModel, title: String, archiveName: String, archiveTime: Instant) {
|
||||
val context = LocalContext.current
|
||||
val archivePath = "${getFilesDirectory(context)}/$archiveName"
|
||||
val saveArchiveLauncher = rememberSaveArchiveLauncher(cxt = context, archivePath)
|
||||
ChatArchiveLayout(
|
||||
title,
|
||||
archiveTime,
|
||||
saveArchive = { saveArchiveLauncher.launch(archivePath.substringAfterLast("/")) },
|
||||
deleteArchiveAlert = { deleteArchiveAlert(m, archivePath) }
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ChatArchiveLayout(
|
||||
title: String,
|
||||
archiveTime: Instant,
|
||||
saveArchive: () -> Unit,
|
||||
deleteArchiveAlert: () -> Unit
|
||||
) {
|
||||
Column(
|
||||
Modifier.fillMaxWidth(),
|
||||
horizontalAlignment = Alignment.Start,
|
||||
) {
|
||||
AppBarTitle(title)
|
||||
SectionView(stringResource(R.string.chat_archive_section)) {
|
||||
SettingsActionItem(
|
||||
Icons.Outlined.IosShare,
|
||||
stringResource(R.string.save_archive),
|
||||
saveArchive,
|
||||
textColor = MaterialTheme.colors.primary,
|
||||
iconColor = MaterialTheme.colors.primary,
|
||||
)
|
||||
SectionDivider()
|
||||
SettingsActionItem(
|
||||
Icons.Outlined.Delete,
|
||||
stringResource(R.string.delete_archive),
|
||||
deleteArchiveAlert,
|
||||
textColor = Color.Red,
|
||||
iconColor = Color.Red,
|
||||
)
|
||||
}
|
||||
val archiveTs = SimpleDateFormat("yyyy-MM-dd HH:mm:ss Z", Locale.US).format(Date.from(archiveTime.toJavaInstant()))
|
||||
SectionTextFooter(
|
||||
String.format(generalGetString(R.string.archive_created_on_ts), archiveTs)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun rememberSaveArchiveLauncher(cxt: Context, chatArchivePath: String): ManagedActivityResultLauncher<String, Uri?> =
|
||||
rememberLauncherForActivityResult(
|
||||
contract = ActivityResultContracts.CreateDocument(),
|
||||
onResult = { destination ->
|
||||
try {
|
||||
destination?.let {
|
||||
val contentResolver = cxt.contentResolver
|
||||
contentResolver.openOutputStream(destination)?.let { stream ->
|
||||
val outputStream = BufferedOutputStream(stream)
|
||||
File(chatArchivePath).inputStream().use { it.copyTo(outputStream) }
|
||||
outputStream.close()
|
||||
Toast.makeText(cxt, generalGetString(R.string.file_saved), Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
} catch (e: Error) {
|
||||
Toast.makeText(cxt, generalGetString(R.string.error_saving_file), Toast.LENGTH_SHORT).show()
|
||||
Log.e(TAG, "rememberSaveArchiveLauncher error saving archive $e")
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
private fun deleteArchiveAlert(m: ChatModel, archivePath: String) {
|
||||
AlertManager.shared.showAlertDialog(
|
||||
title = generalGetString(R.string.delete_chat_archive_question),
|
||||
confirmText = generalGetString(R.string.delete_verb),
|
||||
onConfirm = {
|
||||
val fileDeleted = File(archivePath).delete()
|
||||
if (fileDeleted) {
|
||||
m.controller.appPrefs.chatArchiveName.set(null)
|
||||
m.controller.appPrefs.chatArchiveTime.set(null)
|
||||
ModalManager.shared.closeModal()
|
||||
} else {
|
||||
Log.e(TAG, "deleteArchiveAlert delete() error")
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@Preview(showBackground = true)
|
||||
@Preview(
|
||||
uiMode = Configuration.UI_MODE_NIGHT_YES,
|
||||
showBackground = true,
|
||||
name = "Dark Mode"
|
||||
)
|
||||
@Composable
|
||||
fun PreviewChatArchiveLayout() {
|
||||
SimpleXTheme {
|
||||
ChatArchiveLayout(
|
||||
"New database archive",
|
||||
archiveTime = Clock.System.now(),
|
||||
saveArchive = {},
|
||||
deleteArchiveAlert = {}
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,250 +0,0 @@
|
||||
package chat.simplex.app.views.database
|
||||
|
||||
import SectionSpacer
|
||||
import SectionView
|
||||
import android.content.Context
|
||||
import android.util.Log
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.text.KeyboardActions
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import chat.simplex.app.*
|
||||
import chat.simplex.app.R
|
||||
import chat.simplex.app.model.AppPreferences
|
||||
import chat.simplex.app.ui.theme.*
|
||||
import chat.simplex.app.views.helpers.*
|
||||
import chat.simplex.app.views.usersettings.NotificationsMode
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.datetime.Clock
|
||||
import java.io.File
|
||||
import java.nio.file.Files
|
||||
import java.nio.file.StandardCopyOption
|
||||
import kotlin.io.path.Path
|
||||
|
||||
@Composable
|
||||
fun DatabaseErrorView(
|
||||
chatDbStatus: State<DBMigrationResult?>,
|
||||
appPreferences: AppPreferences,
|
||||
) {
|
||||
val progressIndicator = remember { mutableStateOf(false) }
|
||||
val dbKey = remember { mutableStateOf("") }
|
||||
var storedDBKey by remember { mutableStateOf(DatabaseUtils.getDatabaseKey()) }
|
||||
var useKeychain by remember { mutableStateOf(appPreferences.storeDBPassphrase.get()) }
|
||||
val context = LocalContext.current
|
||||
val restoreDbFromBackup = remember { mutableStateOf(shouldShowRestoreDbButton(appPreferences, context)) }
|
||||
val saveAndRunChatOnClick: () -> Unit = {
|
||||
DatabaseUtils.setDatabaseKey(dbKey.value)
|
||||
storedDBKey = dbKey.value
|
||||
appPreferences.storeDBPassphrase.set(true)
|
||||
useKeychain = true
|
||||
appPreferences.initialRandomDBPassphrase.set(false)
|
||||
runChat(dbKey.value, chatDbStatus, progressIndicator, appPreferences)
|
||||
}
|
||||
val title = when (chatDbStatus.value) {
|
||||
is DBMigrationResult.OK -> ""
|
||||
is DBMigrationResult.ErrorNotADatabase -> if (useKeychain && !storedDBKey.isNullOrEmpty())
|
||||
generalGetString(R.string.wrong_passphrase)
|
||||
else
|
||||
generalGetString(R.string.encrypted_database)
|
||||
is DBMigrationResult.Error -> generalGetString(R.string.database_error)
|
||||
is DBMigrationResult.ErrorKeychain -> generalGetString(R.string.keychain_error)
|
||||
is DBMigrationResult.Unknown -> generalGetString(R.string.database_error)
|
||||
null -> "" // should never be here
|
||||
}
|
||||
|
||||
Column(
|
||||
Modifier.fillMaxSize().verticalScroll(rememberScrollState()),
|
||||
horizontalAlignment = Alignment.Start,
|
||||
verticalArrangement = Arrangement.Center,
|
||||
) {
|
||||
Text(
|
||||
title,
|
||||
Modifier.padding(start = 16.dp, top = 16.dp, bottom = 24.dp),
|
||||
style = MaterialTheme.typography.h1
|
||||
)
|
||||
SectionView(null, padding = PaddingValues(horizontal = DEFAULT_PADDING, vertical = DEFAULT_PADDING_HALF)) {
|
||||
val buttonEnabled = validKey(dbKey.value) && !progressIndicator.value
|
||||
when (val status = chatDbStatus.value) {
|
||||
is DBMigrationResult.ErrorNotADatabase -> {
|
||||
if (useKeychain && !storedDBKey.isNullOrEmpty()) {
|
||||
Text(generalGetString(R.string.passphrase_is_different))
|
||||
DatabaseKeyField(dbKey, buttonEnabled) {
|
||||
saveAndRunChatOnClick()
|
||||
}
|
||||
SaveAndOpenButton(buttonEnabled, saveAndRunChatOnClick)
|
||||
SectionSpacer()
|
||||
Text(String.format(generalGetString(R.string.file_with_path), status.dbFile))
|
||||
} else {
|
||||
Text(generalGetString(R.string.database_passphrase_is_required))
|
||||
DatabaseKeyField(dbKey, buttonEnabled) {
|
||||
if (useKeychain) saveAndRunChatOnClick() else runChat(dbKey.value, chatDbStatus, progressIndicator, appPreferences)
|
||||
}
|
||||
if (useKeychain) {
|
||||
SaveAndOpenButton(buttonEnabled, saveAndRunChatOnClick)
|
||||
} else {
|
||||
OpenChatButton(buttonEnabled) { runChat(dbKey.value, chatDbStatus, progressIndicator, appPreferences) }
|
||||
}
|
||||
}
|
||||
}
|
||||
is DBMigrationResult.Error -> {
|
||||
Text(String.format(generalGetString(R.string.file_with_path), status.dbFile))
|
||||
Text(String.format(generalGetString(R.string.error_with_info), status.migrationError))
|
||||
}
|
||||
is DBMigrationResult.ErrorKeychain -> {
|
||||
Text(generalGetString(R.string.cannot_access_keychain))
|
||||
}
|
||||
is DBMigrationResult.Unknown -> {
|
||||
Text(String.format(generalGetString(R.string.unknown_database_error_with_info), status.json))
|
||||
}
|
||||
is DBMigrationResult.OK -> {
|
||||
}
|
||||
null -> {
|
||||
}
|
||||
}
|
||||
if (restoreDbFromBackup.value) {
|
||||
SectionSpacer()
|
||||
Text(generalGetString(R.string.database_backup_can_be_restored))
|
||||
Spacer(Modifier.size(16.dp))
|
||||
RestoreDbButton {
|
||||
AlertManager.shared.showAlertDialog(
|
||||
title = generalGetString(R.string.restore_database_alert_title),
|
||||
text = generalGetString(R.string.restore_database_alert_desc),
|
||||
confirmText = generalGetString(R.string.restore_database_alert_confirm),
|
||||
onConfirm = { restoreDb(restoreDbFromBackup, appPreferences, context) },
|
||||
destructive = true,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (progressIndicator.value) {
|
||||
Box(
|
||||
Modifier.fillMaxSize(),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
CircularProgressIndicator(
|
||||
Modifier
|
||||
.padding(horizontal = 2.dp)
|
||||
.size(30.dp),
|
||||
color = HighOrLowlight,
|
||||
strokeWidth = 2.5.dp
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun runChat(
|
||||
dbKey: String,
|
||||
chatDbStatus: State<DBMigrationResult?>,
|
||||
progressIndicator: MutableState<Boolean>,
|
||||
prefs: AppPreferences
|
||||
) = CoroutineScope(Dispatchers.Default).launch {
|
||||
// Don't do things concurrently. Shouldn't be here concurrently, just in case
|
||||
if (progressIndicator.value) return@launch
|
||||
progressIndicator.value = true
|
||||
try {
|
||||
SimplexApp.context.initChatController(dbKey)
|
||||
} catch (e: Exception) {
|
||||
Log.d(TAG, "initializeChat ${e.stackTraceToString()}")
|
||||
}
|
||||
progressIndicator.value = false
|
||||
when (val status = chatDbStatus.value) {
|
||||
is DBMigrationResult.OK -> {
|
||||
SimplexService.cancelPassphraseNotification()
|
||||
when (prefs.notificationsMode.get()) {
|
||||
NotificationsMode.SERVICE.name -> CoroutineScope(Dispatchers.Default).launch { SimplexService.start(SimplexApp.context) }
|
||||
NotificationsMode.PERIODIC.name -> SimplexApp.context.schedulePeriodicWakeUp()
|
||||
}
|
||||
}
|
||||
is DBMigrationResult.ErrorNotADatabase -> {
|
||||
AlertManager.shared.showAlertMsg(generalGetString(R.string.wrong_passphrase_title), generalGetString(R.string.enter_correct_passphrase))
|
||||
}
|
||||
is DBMigrationResult.Error -> {
|
||||
AlertManager.shared.showAlertMsg(generalGetString(R.string.database_error), status.migrationError)
|
||||
}
|
||||
is DBMigrationResult.ErrorKeychain -> {
|
||||
AlertManager.shared.showAlertMsg(generalGetString(R.string.keychain_error))
|
||||
}
|
||||
is DBMigrationResult.Unknown -> {
|
||||
AlertManager.shared.showAlertMsg(generalGetString(R.string.unknown_error), status.json)
|
||||
}
|
||||
null -> {}
|
||||
}
|
||||
}
|
||||
|
||||
private fun shouldShowRestoreDbButton(prefs: AppPreferences, context: Context): Boolean {
|
||||
val startedAt = prefs.encryptionStartedAt.get() ?: return false
|
||||
/** Just in case there is any small difference between reported Java's [Clock.System.now] and Linux's time on a file */
|
||||
val safeDiffInTime = 10_000L
|
||||
val filesChat = File(context.dataDir.absolutePath + File.separator + "files_chat.db.bak")
|
||||
val filesAgent = File(context.dataDir.absolutePath + File.separator + "files_agent.db.bak")
|
||||
return filesChat.exists() &&
|
||||
filesAgent.exists() &&
|
||||
startedAt.toEpochMilliseconds() - safeDiffInTime <= filesChat.lastModified() &&
|
||||
startedAt.toEpochMilliseconds() - safeDiffInTime <= filesAgent.lastModified()
|
||||
}
|
||||
|
||||
private fun restoreDb(restoreDbFromBackup: MutableState<Boolean>, prefs: AppPreferences, context: Context) {
|
||||
val filesChatBase = context.dataDir.absolutePath + File.separator + "files_chat.db"
|
||||
val filesAgentBase = context.dataDir.absolutePath + File.separator + "files_agent.db"
|
||||
try {
|
||||
Files.copy(Path("$filesChatBase.bak"), Path(filesChatBase), StandardCopyOption.REPLACE_EXISTING)
|
||||
Files.copy(Path("$filesAgentBase.bak"), Path(filesAgentBase), StandardCopyOption.REPLACE_EXISTING)
|
||||
restoreDbFromBackup.value = false
|
||||
prefs.encryptionStartedAt.set(null)
|
||||
} catch (e: Exception) {
|
||||
AlertManager.shared.showAlertMsg(generalGetString(R.string.database_restore_error), e.stackTraceToString())
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun DatabaseKeyField(text: MutableState<String>, enabled: Boolean, onClick: (() -> Unit)? = null) {
|
||||
DatabaseKeyField(
|
||||
text,
|
||||
generalGetString(R.string.enter_passphrase),
|
||||
isValid = ::validKey,
|
||||
keyboardActions = KeyboardActions(onDone = if (enabled) {
|
||||
{ onClick?.invoke() }
|
||||
} else null
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ColumnScope.SaveAndOpenButton(enabled: Boolean, onClick: () -> Unit) {
|
||||
TextButton(onClick, Modifier.align(Alignment.CenterHorizontally), enabled = enabled) {
|
||||
Text(generalGetString(R.string.save_passphrase_and_open_chat))
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ColumnScope.OpenChatButton(enabled: Boolean, onClick: () -> Unit) {
|
||||
TextButton(onClick, Modifier.align(Alignment.CenterHorizontally), enabled = enabled) {
|
||||
Text(generalGetString(R.string.open_chat))
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ColumnScope.RestoreDbButton(onClick: () -> Unit) {
|
||||
TextButton(onClick, Modifier.align(Alignment.CenterHorizontally)) {
|
||||
Text(generalGetString(R.string.restore_database), color = MaterialTheme.colors.error)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun PreviewChatInfoLayout() {
|
||||
SimpleXTheme {
|
||||
DatabaseErrorView(
|
||||
remember { mutableStateOf(DBMigrationResult.ErrorNotADatabase("simplex_v1_chat.db")) },
|
||||
AppPreferences(SimplexApp.context)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,179 +0,0 @@
|
||||
package chat.simplex.app.views.helpers
|
||||
|
||||
import android.util.Log
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.*
|
||||
import androidx.compose.ui.window.Dialog
|
||||
import chat.simplex.app.R
|
||||
import chat.simplex.app.TAG
|
||||
import chat.simplex.app.ui.theme.*
|
||||
|
||||
class AlertManager {
|
||||
var alertViews = mutableStateListOf<(@Composable () -> Unit)>()
|
||||
|
||||
fun showAlert(alert: @Composable () -> Unit) {
|
||||
Log.d(TAG, "AlertManager.showAlert")
|
||||
alertViews.add(alert)
|
||||
}
|
||||
|
||||
fun hideAlert() {
|
||||
alertViews.removeLastOrNull()
|
||||
}
|
||||
|
||||
fun showAlertDialogButtons(
|
||||
title: String,
|
||||
text: String? = null,
|
||||
buttons: @Composable () -> Unit,
|
||||
) {
|
||||
val alertText: (@Composable () -> Unit)? = if (text == null) null else { -> Text(text) }
|
||||
showAlert {
|
||||
AlertDialog(
|
||||
onDismissRequest = this::hideAlert,
|
||||
title = { Text(title) },
|
||||
text = alertText,
|
||||
buttons = buttons
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun showAlertDialogButtonsColumn(
|
||||
title: String,
|
||||
text: AnnotatedString? = null,
|
||||
buttons: @Composable () -> Unit,
|
||||
) {
|
||||
showAlert {
|
||||
Dialog(onDismissRequest = this::hideAlert) {
|
||||
Column(Modifier.background(MaterialTheme.colors.background, MaterialTheme.shapes.medium)) {
|
||||
Text(title,
|
||||
Modifier.padding(start = DEFAULT_PADDING, end = DEFAULT_PADDING, top = DEFAULT_PADDING, bottom = if (text == null) DEFAULT_PADDING else DEFAULT_PADDING_HALF),
|
||||
fontSize = 15.sp,
|
||||
fontWeight = FontWeight.SemiBold
|
||||
)
|
||||
if (text != null) {
|
||||
CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.high) {
|
||||
Text(
|
||||
text,
|
||||
Modifier.padding(start = DEFAULT_PADDING, end = DEFAULT_PADDING, bottom = DEFAULT_PADDING),
|
||||
fontSize = 14.sp,
|
||||
)
|
||||
}
|
||||
}
|
||||
CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.high) {
|
||||
buttons()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun showAlertDialog(
|
||||
title: String,
|
||||
text: String? = null,
|
||||
confirmText: String = generalGetString(R.string.ok),
|
||||
onConfirm: (() -> Unit)? = null,
|
||||
dismissText: String = generalGetString(R.string.cancel_verb),
|
||||
onDismiss: (() -> Unit)? = null,
|
||||
onDismissRequest: (() -> Unit)? = null,
|
||||
destructive: Boolean = false
|
||||
) {
|
||||
val alertText: (@Composable () -> Unit)? = if (text == null) null else { -> Text(text) }
|
||||
showAlert {
|
||||
AlertDialog(
|
||||
onDismissRequest = { onDismissRequest?.invoke(); hideAlert() },
|
||||
title = { Text(title) },
|
||||
text = alertText,
|
||||
confirmButton = {
|
||||
TextButton(onClick = {
|
||||
onConfirm?.invoke()
|
||||
hideAlert()
|
||||
}) { Text(confirmText, color = if (destructive) MaterialTheme.colors.error else Color.Unspecified) }
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = {
|
||||
onDismiss?.invoke()
|
||||
hideAlert()
|
||||
}) { Text(dismissText) }
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun showAlertDialogStacked(
|
||||
title: String,
|
||||
text: String? = null,
|
||||
confirmText: String = generalGetString(R.string.ok),
|
||||
onConfirm: (() -> Unit)? = null,
|
||||
dismissText: String = generalGetString(R.string.cancel_verb),
|
||||
onDismiss: (() -> Unit)? = null,
|
||||
onDismissRequest: (() -> Unit)? = null,
|
||||
destructive: Boolean = false
|
||||
) {
|
||||
val alertText: (@Composable () -> Unit)? = if (text == null) null else { -> Text(text) }
|
||||
showAlert {
|
||||
AlertDialog(
|
||||
onDismissRequest = { onDismissRequest?.invoke(); hideAlert() },
|
||||
title = { Text(title) },
|
||||
text = alertText,
|
||||
buttons = {
|
||||
Column(
|
||||
Modifier.fillMaxWidth().padding(horizontal = 8.dp).padding(top = 16.dp, bottom = 2.dp),
|
||||
horizontalAlignment = Alignment.End
|
||||
) {
|
||||
TextButton(onClick = {
|
||||
onDismiss?.invoke()
|
||||
hideAlert()
|
||||
}) { Text(dismissText) }
|
||||
TextButton(onClick = {
|
||||
onConfirm?.invoke()
|
||||
hideAlert()
|
||||
}) { Text(confirmText, color = if (destructive) MaterialTheme.colors.error else Color.Unspecified) }
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun showAlertMsg(
|
||||
title: String, text: String? = null,
|
||||
confirmText: String = generalGetString(R.string.ok), onConfirm: (() -> Unit)? = null
|
||||
) {
|
||||
val alertText: (@Composable () -> Unit)? = if (text == null) null else { -> Text(text) }
|
||||
showAlert {
|
||||
AlertDialog(
|
||||
onDismissRequest = this::hideAlert,
|
||||
title = { Text(title) },
|
||||
text = alertText,
|
||||
confirmButton = {
|
||||
TextButton(onClick = {
|
||||
onConfirm?.invoke()
|
||||
hideAlert()
|
||||
}) { Text(confirmText) }
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun showAlertMsg(
|
||||
title: Int,
|
||||
text: Int? = null,
|
||||
confirmText: Int = R.string.ok,
|
||||
onConfirm: (() -> Unit)? = null
|
||||
) = showAlertMsg(generalGetString(title), if (text != null) generalGetString(text) else null, generalGetString(confirmText), onConfirm)
|
||||
|
||||
@Composable
|
||||
fun showInView() {
|
||||
remember { alertViews }.lastOrNull()?.invoke()
|
||||
}
|
||||
|
||||
companion object {
|
||||
val shared = AlertManager()
|
||||
}
|
||||
}
|
||||
@@ -1,80 +0,0 @@
|
||||
package chat.simplex.app.views.helpers
|
||||
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material.Icon
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.asImageBitmap
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import chat.simplex.app.R
|
||||
import chat.simplex.app.model.ChatInfo
|
||||
import chat.simplex.app.ui.theme.SimpleXTheme
|
||||
|
||||
@Composable
|
||||
fun ChatInfoImage(chatInfo: ChatInfo, size: Dp, iconColor: Color = MaterialTheme.colors.secondary) {
|
||||
val icon =
|
||||
if (chatInfo is ChatInfo.Group) Icons.Filled.SupervisedUserCircle
|
||||
else Icons.Filled.AccountCircle
|
||||
ProfileImage(size, chatInfo.image, icon, iconColor)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun IncognitoImage(size: Dp, iconColor: Color = MaterialTheme.colors.secondary) {
|
||||
Box(Modifier.size(size)) {
|
||||
Icon(
|
||||
Icons.Filled.TheaterComedy, stringResource(R.string.incognito),
|
||||
modifier = Modifier.size(size).padding(size / 12),
|
||||
iconColor
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ProfileImage(
|
||||
size: Dp,
|
||||
image: String? = null,
|
||||
icon: ImageVector = Icons.Filled.AccountCircle,
|
||||
color: Color = MaterialTheme.colors.secondary
|
||||
) {
|
||||
Box(Modifier.size(size)) {
|
||||
if (image == null) {
|
||||
Icon(
|
||||
icon,
|
||||
contentDescription = stringResource(R.string.icon_descr_profile_image_placeholder),
|
||||
tint = color,
|
||||
modifier = Modifier.fillMaxSize()
|
||||
)
|
||||
} else {
|
||||
val imageBitmap = base64ToBitmap(image).asImageBitmap()
|
||||
Image(
|
||||
imageBitmap,
|
||||
stringResource(R.string.image_descr_profile_image),
|
||||
contentScale = ContentScale.Crop,
|
||||
modifier = Modifier.size(size).padding(size / 12).clip(CircleShape)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun PreviewChatInfoImage() {
|
||||
SimpleXTheme {
|
||||
ChatInfoImage(
|
||||
chatInfo = ChatInfo.Direct.sampleData,
|
||||
size = 55.dp
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,54 +0,0 @@
|
||||
package chat.simplex.app.views.helpers
|
||||
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.focus.onFocusChanged
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import chat.simplex.app.R
|
||||
import chat.simplex.app.views.newchat.ActionButton
|
||||
|
||||
sealed class AttachmentOption {
|
||||
object TakePhoto: AttachmentOption()
|
||||
object PickImage: AttachmentOption()
|
||||
object PickFile: AttachmentOption()
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ChooseAttachmentView(
|
||||
attachmentOption: MutableState<AttachmentOption?>,
|
||||
hide: () -> Unit
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.wrapContentHeight()
|
||||
.onFocusChanged { focusState ->
|
||||
if (!focusState.hasFocus) hide()
|
||||
}
|
||||
) {
|
||||
Row(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 8.dp, vertical = 30.dp),
|
||||
horizontalArrangement = Arrangement.SpaceEvenly
|
||||
) {
|
||||
ActionButton(null, stringResource(R.string.use_camera_button), icon = Icons.Outlined.PhotoCamera) {
|
||||
attachmentOption.value = AttachmentOption.TakePhoto
|
||||
hide()
|
||||
}
|
||||
ActionButton(null, stringResource(R.string.from_gallery_button), icon = Icons.Outlined.Collections) {
|
||||
attachmentOption.value = AttachmentOption.PickImage
|
||||
hide()
|
||||
}
|
||||
ActionButton(null, stringResource(R.string.choose_file), icon = Icons.Outlined.InsertDriveFile) {
|
||||
attachmentOption.value = AttachmentOption.PickFile
|
||||
hide()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,63 +0,0 @@
|
||||
package chat.simplex.app.views.helpers
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import chat.simplex.app.ui.theme.*
|
||||
|
||||
@Composable
|
||||
fun CloseSheetBar(close: () -> Unit, endButtons: @Composable RowScope.() -> Unit = {}) {
|
||||
Column(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.heightIn(min = AppBarHeight)
|
||||
.padding(horizontal = AppBarHorizontalPadding),
|
||||
) {
|
||||
Row(
|
||||
Modifier
|
||||
.padding(top = 4.dp), // Like in DefaultAppBar
|
||||
content = {
|
||||
Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) {
|
||||
NavigationButtonBack(close)
|
||||
Row {
|
||||
endButtons()
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun AppBarTitle(title: String, withPadding: Boolean = true) {
|
||||
val padding = if (withPadding)
|
||||
PaddingValues(start = DEFAULT_PADDING, end = DEFAULT_PADDING, bottom = DEFAULT_PADDING)
|
||||
else
|
||||
PaddingValues(bottom = DEFAULT_PADDING)
|
||||
Text(
|
||||
title,
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(padding),
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
style = MaterialTheme.typography.h1
|
||||
)
|
||||
}
|
||||
|
||||
@Preview(showBackground = true)
|
||||
@Preview(
|
||||
uiMode = Configuration.UI_MODE_NIGHT_YES,
|
||||
showBackground = true,
|
||||
name = "Dark Mode"
|
||||
)
|
||||
@Composable
|
||||
fun PreviewCloseSheetBar() {
|
||||
SimpleXTheme {
|
||||
CloseSheetBar(close = {})
|
||||
}
|
||||
}
|
||||
@@ -1,73 +0,0 @@
|
||||
package chat.simplex.app.views.helpers
|
||||
|
||||
import android.util.Log
|
||||
import chat.simplex.app.*
|
||||
import chat.simplex.app.model.AppPreferences
|
||||
import chat.simplex.app.views.usersettings.Cryptor
|
||||
import kotlinx.serialization.*
|
||||
import java.io.File
|
||||
import java.security.SecureRandom
|
||||
|
||||
object DatabaseUtils {
|
||||
private val cryptor = Cryptor()
|
||||
|
||||
private val appPreferences: AppPreferences by lazy {
|
||||
AppPreferences(SimplexApp.context)
|
||||
}
|
||||
|
||||
private const val DATABASE_PASSWORD_ALIAS: String = "databasePassword"
|
||||
|
||||
private fun hasDatabase(rootDir: String): Boolean =
|
||||
File(rootDir + File.separator + "files_chat.db").exists() && File(rootDir + File.separator + "files_agent.db").exists()
|
||||
|
||||
fun getDatabaseKey(): String? {
|
||||
return cryptor.decryptData(
|
||||
appPreferences.encryptedDBPassphrase.get()?.toByteArrayFromBase64() ?: return null,
|
||||
appPreferences.initializationVectorDBPassphrase.get()?.toByteArrayFromBase64() ?: return null,
|
||||
DATABASE_PASSWORD_ALIAS,
|
||||
)
|
||||
}
|
||||
|
||||
fun setDatabaseKey(key: String) {
|
||||
val data = cryptor.encryptText(key, DATABASE_PASSWORD_ALIAS)
|
||||
appPreferences.encryptedDBPassphrase.set(data.first.toBase64String())
|
||||
appPreferences.initializationVectorDBPassphrase.set(data.second.toBase64String())
|
||||
}
|
||||
|
||||
fun removeDatabaseKey() {
|
||||
cryptor.deleteKey(DATABASE_PASSWORD_ALIAS)
|
||||
appPreferences.encryptedDBPassphrase.set(null)
|
||||
appPreferences.initializationVectorDBPassphrase.set(null)
|
||||
}
|
||||
|
||||
fun useDatabaseKey(): String {
|
||||
Log.d(TAG, "useDatabaseKey ${appPreferences.storeDBPassphrase.get()}")
|
||||
var dbKey = ""
|
||||
val useKeychain = appPreferences.storeDBPassphrase.get()
|
||||
if (useKeychain) {
|
||||
if (!hasDatabase(SimplexApp.context.dataDir.absolutePath)) {
|
||||
dbKey = randomDatabasePassword()
|
||||
setDatabaseKey(dbKey)
|
||||
appPreferences.initialRandomDBPassphrase.set(true)
|
||||
} else {
|
||||
dbKey = getDatabaseKey() ?: ""
|
||||
}
|
||||
}
|
||||
return dbKey
|
||||
}
|
||||
|
||||
private fun randomDatabasePassword(): String {
|
||||
val s = ByteArray(32)
|
||||
SecureRandom().nextBytes(s)
|
||||
return s.toBase64String().replace("\n", "")
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
sealed class DBMigrationResult {
|
||||
@Serializable @SerialName("ok") object OK: DBMigrationResult()
|
||||
@Serializable @SerialName("errorNotADatabase") class ErrorNotADatabase(val dbFile: String): DBMigrationResult()
|
||||
@Serializable @SerialName("error") class Error(val dbFile: String, val migrationError: String): DBMigrationResult()
|
||||
@Serializable @SerialName("errorKeychain") object ErrorKeychain: DBMigrationResult()
|
||||
@Serializable @SerialName("unknown") class Unknown(val json: String): DBMigrationResult()
|
||||
}
|
||||
@@ -1,112 +0,0 @@
|
||||
package chat.simplex.app.views.helpers
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.defaultMinSize
|
||||
import androidx.compose.foundation.shape.ZeroCornerSize
|
||||
import androidx.compose.foundation.text.*
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.material.TextFieldDefaults.indicatorLine
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.ExperimentalComposeUiApi
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.focus.*
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.SolidColor
|
||||
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
|
||||
import androidx.compose.ui.text.TextRange
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.input.*
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import kotlinx.coroutines.delay
|
||||
|
||||
@OptIn(ExperimentalComposeUiApi::class)
|
||||
@Composable
|
||||
fun DefaultBasicTextField(
|
||||
modifier: Modifier,
|
||||
initialValue: String,
|
||||
placeholder: (@Composable () -> Unit)? = null,
|
||||
leadingIcon: (@Composable () -> Unit)? = null,
|
||||
focus: Boolean = false,
|
||||
color: Color = MaterialTheme.colors.onBackground,
|
||||
textStyle: TextStyle = TextStyle.Default,
|
||||
selectTextOnFocus: Boolean = false,
|
||||
keyboardOptions: KeyboardOptions = KeyboardOptions(imeAction = ImeAction.Done),
|
||||
keyboardActions: KeyboardActions = KeyboardActions(),
|
||||
onValueChange: (String) -> Unit,
|
||||
) {
|
||||
val state = remember {
|
||||
mutableStateOf(TextFieldValue(initialValue))
|
||||
}
|
||||
val focusRequester = remember { FocusRequester() }
|
||||
val keyboard = LocalSoftwareKeyboardController.current
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
if (!focus) return@LaunchedEffect
|
||||
delay(300)
|
||||
focusRequester.requestFocus()
|
||||
keyboard?.show()
|
||||
}
|
||||
val enabled = true
|
||||
val colors = TextFieldDefaults.textFieldColors(
|
||||
backgroundColor = Color.Unspecified,
|
||||
textColor = MaterialTheme.colors.onBackground,
|
||||
focusedIndicatorColor = Color.Unspecified,
|
||||
unfocusedIndicatorColor = Color.Unspecified,
|
||||
)
|
||||
val shape = MaterialTheme.shapes.small.copy(bottomEnd = ZeroCornerSize, bottomStart = ZeroCornerSize)
|
||||
val interactionSource = remember { MutableInteractionSource() }
|
||||
BasicTextField(
|
||||
value = state.value,
|
||||
modifier = modifier
|
||||
.background(colors.backgroundColor(enabled).value, shape)
|
||||
.indicatorLine(enabled, false, interactionSource, colors)
|
||||
.focusRequester(focusRequester)
|
||||
.onFocusChanged { focusState ->
|
||||
if (focusState.isFocused && selectTextOnFocus) {
|
||||
val text = state.value.text
|
||||
state.value = state.value.copy(
|
||||
selection = TextRange(0, text.length)
|
||||
)
|
||||
}
|
||||
}
|
||||
.defaultMinSize(
|
||||
minWidth = TextFieldDefaults.MinWidth,
|
||||
minHeight = TextFieldDefaults.MinHeight
|
||||
),
|
||||
onValueChange = {
|
||||
state.value = it
|
||||
onValueChange(it.text)
|
||||
},
|
||||
cursorBrush = SolidColor(colors.cursorColor(false).value),
|
||||
visualTransformation = VisualTransformation.None,
|
||||
keyboardOptions = keyboardOptions,
|
||||
keyboardActions = KeyboardActions(onDone = {
|
||||
keyboard?.hide()
|
||||
keyboardActions.onDone?.invoke(this)
|
||||
}),
|
||||
singleLine = true,
|
||||
textStyle = textStyle.copy(
|
||||
color = color,
|
||||
fontWeight = FontWeight.Normal,
|
||||
fontSize = 16.sp
|
||||
),
|
||||
interactionSource = interactionSource,
|
||||
decorationBox = @Composable { innerTextField ->
|
||||
TextFieldDefaults.TextFieldDecorationBox(
|
||||
value = state.value.text,
|
||||
innerTextField = innerTextField,
|
||||
placeholder = placeholder,
|
||||
singleLine = true,
|
||||
enabled = enabled,
|
||||
leadingIcon = leadingIcon,
|
||||
interactionSource = interactionSource,
|
||||
contentPadding = TextFieldDefaults.textFieldWithLabelPadding(start = 0.dp, end = 0.dp),
|
||||
visualTransformation = VisualTransformation.None,
|
||||
colors = colors
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -1,105 +0,0 @@
|
||||
package chat.simplex.app.views.helpers
|
||||
|
||||
import android.content.*
|
||||
import android.net.Uri
|
||||
import android.provider.MediaStore
|
||||
import android.webkit.MimeTypeMap
|
||||
import android.widget.Toast
|
||||
import androidx.activity.compose.ManagedActivityResultLauncher
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.content.FileProvider
|
||||
import chat.simplex.app.*
|
||||
import chat.simplex.app.model.CIFile
|
||||
import java.io.BufferedOutputStream
|
||||
import java.io.File
|
||||
|
||||
fun shareText(cxt: Context, text: String) {
|
||||
val sendIntent: Intent = Intent().apply {
|
||||
action = Intent.ACTION_SEND
|
||||
putExtra(Intent.EXTRA_TEXT, text)
|
||||
type = "text/plain"
|
||||
}
|
||||
val shareIntent = Intent.createChooser(sendIntent, null)
|
||||
cxt.startActivity(shareIntent)
|
||||
}
|
||||
|
||||
fun shareFile(cxt: Context, text: String, filePath: String) {
|
||||
val uri = FileProvider.getUriForFile(SimplexApp.context, "${BuildConfig.APPLICATION_ID}.provider", File(filePath))
|
||||
val ext = filePath.substringAfterLast(".")
|
||||
val mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(ext) ?: return
|
||||
val sendIntent: Intent = Intent().apply {
|
||||
action = Intent.ACTION_SEND
|
||||
/*if (text.isNotEmpty()) {
|
||||
putExtra(Intent.EXTRA_TEXT, text)
|
||||
}*/
|
||||
putExtra(Intent.EXTRA_STREAM, uri)
|
||||
type = mimeType
|
||||
}
|
||||
val shareIntent = Intent.createChooser(sendIntent, null)
|
||||
cxt.startActivity(shareIntent)
|
||||
}
|
||||
|
||||
fun copyText(cxt: Context, text: String) {
|
||||
val clipboard = ContextCompat.getSystemService(cxt, ClipboardManager::class.java)
|
||||
clipboard?.setPrimaryClip(ClipData.newPlainText("text", text))
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun rememberSaveFileLauncher(cxt: Context, ciFile: CIFile?): ManagedActivityResultLauncher<String, Uri?> =
|
||||
rememberLauncherForActivityResult(
|
||||
contract = ActivityResultContracts.CreateDocument(),
|
||||
onResult = { destination ->
|
||||
destination?.let {
|
||||
val filePath = getLoadedFilePath(cxt, ciFile)
|
||||
if (filePath != null) {
|
||||
val contentResolver = cxt.contentResolver
|
||||
contentResolver.openOutputStream(destination)?.let { stream ->
|
||||
val outputStream = BufferedOutputStream(stream)
|
||||
File(filePath).inputStream().use { it.copyTo(outputStream) }
|
||||
outputStream.close()
|
||||
Toast.makeText(cxt, generalGetString(R.string.file_saved), Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
} else {
|
||||
Toast.makeText(cxt, generalGetString(R.string.file_not_found), Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
fun imageMimeType(fileName: String): String {
|
||||
val lowercaseName = fileName.lowercase()
|
||||
return when {
|
||||
lowercaseName.endsWith(".png") -> "image/png"
|
||||
lowercaseName.endsWith(".gif") -> "image/gif"
|
||||
lowercaseName.endsWith(".webp") -> "image/webp"
|
||||
lowercaseName.endsWith(".avif") -> "image/avif"
|
||||
lowercaseName.endsWith(".svg") -> "image/svg+xml"
|
||||
else -> "image/jpeg"
|
||||
}
|
||||
}
|
||||
|
||||
fun saveImage(cxt: Context, ciFile: CIFile?) {
|
||||
val filePath = getLoadedFilePath(cxt, ciFile)
|
||||
val fileName = ciFile?.fileName
|
||||
if (filePath != null && fileName != null) {
|
||||
val values = ContentValues()
|
||||
values.put(MediaStore.Images.Media.DATE_TAKEN, System.currentTimeMillis())
|
||||
values.put(MediaStore.Images.Media.MIME_TYPE, imageMimeType(fileName))
|
||||
values.put(MediaStore.MediaColumns.DISPLAY_NAME, fileName)
|
||||
values.put(MediaStore.MediaColumns.TITLE, fileName)
|
||||
val uri = cxt.contentResolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values)
|
||||
uri?.let {
|
||||
cxt.contentResolver.openOutputStream(uri)?.let { stream ->
|
||||
val outputStream = BufferedOutputStream(stream)
|
||||
File(filePath).inputStream().use { it.copyTo(outputStream) }
|
||||
outputStream.close()
|
||||
Toast.makeText(cxt, generalGetString(R.string.image_saved), Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Toast.makeText(cxt, generalGetString(R.string.file_not_found), Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
@@ -1,80 +0,0 @@
|
||||
package chat.simplex.app.ui.theme
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.Share
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
@Composable
|
||||
fun SimpleButton(text: String, icon: ImageVector,
|
||||
color: Color = MaterialTheme.colors.primary,
|
||||
click: () -> Unit) {
|
||||
SimpleButtonFrame(click) {
|
||||
Icon(
|
||||
icon, text, tint = color,
|
||||
modifier = Modifier.padding(end = 8.dp)
|
||||
)
|
||||
Text(text, style = MaterialTheme.typography.caption, color = color)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SimpleButton(
|
||||
text: String, icon: ImageVector,
|
||||
color: Color = MaterialTheme.colors.primary,
|
||||
disabled: Boolean,
|
||||
click: () -> Unit
|
||||
) {
|
||||
SimpleButtonFrame(click, disabled = disabled) {
|
||||
Icon(
|
||||
icon, text, tint = if (disabled) HighOrLowlight else color,
|
||||
modifier = Modifier.padding(end = 8.dp)
|
||||
)
|
||||
Text(text, style = MaterialTheme.typography.caption, color = if (disabled) HighOrLowlight else color)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SimpleButtonIconEnded(
|
||||
text: String,
|
||||
icon: ImageVector,
|
||||
color: Color = MaterialTheme.colors.primary,
|
||||
click: () -> Unit
|
||||
) {
|
||||
SimpleButtonFrame(click) {
|
||||
Text(text, style = MaterialTheme.typography.caption, color = color)
|
||||
Icon(
|
||||
icon, text, tint = color,
|
||||
modifier = Modifier.padding(start = 8.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SimpleButtonFrame(click: () -> Unit, disabled: Boolean = false, content: @Composable () -> Unit) {
|
||||
Surface(shape = RoundedCornerShape(20.dp)) {
|
||||
val modifier = if (disabled) Modifier else Modifier.clickable { click() }
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = modifier.padding(8.dp)
|
||||
) { content() }
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun PreviewCloseSheetBar() {
|
||||
SimpleXTheme {
|
||||
SimpleButton(text = "Share", icon = Icons.Outlined.Share, click = {})
|
||||
}
|
||||
}
|
||||
@@ -1,65 +0,0 @@
|
||||
package chat.simplex.app.views.helpers
|
||||
|
||||
import androidx.compose.foundation.BorderStroke
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.text.BasicTextField
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.Surface
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.*
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.font.FontFamily
|
||||
import androidx.compose.ui.text.input.KeyboardCapitalization
|
||||
import androidx.compose.ui.unit.*
|
||||
import chat.simplex.app.ui.theme.DEFAULT_PADDING
|
||||
import chat.simplex.app.ui.theme.HighOrLowlight
|
||||
|
||||
@Composable
|
||||
fun TextEditor(
|
||||
modifier: Modifier,
|
||||
text: MutableState<String>,
|
||||
border: Boolean = true,
|
||||
fontSize: TextUnit = 14.sp,
|
||||
background: Color = MaterialTheme.colors.background,
|
||||
onChange: ((String) -> Unit)? = null
|
||||
) {
|
||||
BasicTextField(
|
||||
value = text.value,
|
||||
onValueChange = { text.value = it; onChange?.invoke(it) },
|
||||
textStyle = TextStyle(
|
||||
fontFamily = FontFamily.Monospace, fontSize = fontSize,
|
||||
color = MaterialTheme.colors.onBackground
|
||||
),
|
||||
keyboardOptions = KeyboardOptions.Default.copy(
|
||||
capitalization = KeyboardCapitalization.None,
|
||||
autoCorrect = false
|
||||
),
|
||||
modifier = modifier,
|
||||
cursorBrush = SolidColor(HighOrLowlight),
|
||||
decorationBox = { innerTextField ->
|
||||
Surface(
|
||||
shape = if (border) RoundedCornerShape(10.dp) else RectangleShape,
|
||||
border = if (border) BorderStroke(1.dp, MaterialTheme.colors.secondary) else null
|
||||
) {
|
||||
Row(
|
||||
Modifier.background(background),
|
||||
verticalAlignment = Alignment.Top
|
||||
) {
|
||||
Box(
|
||||
Modifier
|
||||
.weight(1f)
|
||||
.padding(vertical = 5.dp, horizontal = if (border) 7.dp else DEFAULT_PADDING)
|
||||
) {
|
||||
innerTextField()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -1,503 +0,0 @@
|
||||
package chat.simplex.app.views.helpers
|
||||
|
||||
import android.app.Application
|
||||
import android.content.Context
|
||||
import android.content.res.Resources
|
||||
import android.graphics.*
|
||||
import android.graphics.Typeface
|
||||
import android.net.Uri
|
||||
import android.os.FileUtils
|
||||
import android.provider.OpenableColumns
|
||||
import android.text.Spanned
|
||||
import android.text.SpannedString
|
||||
import android.text.style.*
|
||||
import android.util.Base64
|
||||
import android.util.Log
|
||||
import android.view.View
|
||||
import android.view.ViewTreeObserver
|
||||
import android.view.inputmethod.InputMethodManager
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.runtime.saveable.Saver
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.*
|
||||
import androidx.compose.ui.text.*
|
||||
import androidx.compose.ui.text.font.*
|
||||
import androidx.compose.ui.text.style.BaselineShift
|
||||
import androidx.compose.ui.text.style.TextDecoration
|
||||
import androidx.compose.ui.unit.*
|
||||
import androidx.core.content.FileProvider
|
||||
import androidx.core.text.HtmlCompat
|
||||
import chat.simplex.app.*
|
||||
import chat.simplex.app.model.CIFile
|
||||
import chat.simplex.app.model.json
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.serialization.decodeFromString
|
||||
import kotlinx.serialization.encodeToString
|
||||
import java.io.*
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
import kotlin.math.*
|
||||
|
||||
fun withApi(action: suspend CoroutineScope.() -> Unit): Job = withScope(GlobalScope, action)
|
||||
|
||||
fun withScope(scope: CoroutineScope, action: suspend CoroutineScope.() -> Unit): Job =
|
||||
scope.launch { withContext(Dispatchers.Main, action) }
|
||||
|
||||
fun withBGApi(action: suspend CoroutineScope.() -> Unit): Job =
|
||||
CoroutineScope(Dispatchers.Default).launch(block = action)
|
||||
|
||||
enum class KeyboardState {
|
||||
Opened, Closed
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun getKeyboardState(): State<KeyboardState> {
|
||||
val keyboardState = remember { mutableStateOf(KeyboardState.Closed) }
|
||||
val view = LocalView.current
|
||||
DisposableEffect(view) {
|
||||
val onGlobalListener = ViewTreeObserver.OnGlobalLayoutListener {
|
||||
val rect = Rect()
|
||||
view.getWindowVisibleDisplayFrame(rect)
|
||||
val screenHeight = view.rootView.height
|
||||
val keypadHeight = screenHeight - rect.bottom
|
||||
keyboardState.value = if (keypadHeight > screenHeight * 0.15) {
|
||||
KeyboardState.Opened
|
||||
} else {
|
||||
KeyboardState.Closed
|
||||
}
|
||||
}
|
||||
view.viewTreeObserver.addOnGlobalLayoutListener(onGlobalListener)
|
||||
|
||||
onDispose {
|
||||
view.viewTreeObserver.removeOnGlobalLayoutListener(onGlobalListener)
|
||||
}
|
||||
}
|
||||
|
||||
return keyboardState
|
||||
}
|
||||
|
||||
fun hideKeyboard(view: View) =
|
||||
(SimplexApp.context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager).hideSoftInputFromWindow(view.windowToken, 0)
|
||||
|
||||
// Resource to annotated string from
|
||||
// https://stackoverflow.com/questions/68549248/android-jetpack-compose-how-to-show-styled-text-from-string-resources
|
||||
fun generalGetString(id: Int): String {
|
||||
// prefer stringResource in Composable items to retain preview abilities
|
||||
return SimplexApp.context.getString(id)
|
||||
}
|
||||
|
||||
@Composable
|
||||
@ReadOnlyComposable
|
||||
private fun resources(): Resources {
|
||||
LocalConfiguration.current
|
||||
return LocalContext.current.resources
|
||||
}
|
||||
|
||||
fun Spanned.toHtmlWithoutParagraphs(): String {
|
||||
return HtmlCompat.toHtml(this, HtmlCompat.TO_HTML_PARAGRAPH_LINES_CONSECUTIVE)
|
||||
.substringAfter("<p dir=\"ltr\">").substringBeforeLast("</p>")
|
||||
}
|
||||
|
||||
fun Resources.getText(@StringRes id: Int, vararg args: Any): CharSequence {
|
||||
val escapedArgs = args.map {
|
||||
if (it is Spanned) it.toHtmlWithoutParagraphs() else it
|
||||
}.toTypedArray()
|
||||
val resource = SpannedString(getText(id))
|
||||
val htmlResource = resource.toHtmlWithoutParagraphs()
|
||||
val formattedHtml = String.format(htmlResource, *escapedArgs)
|
||||
return HtmlCompat.fromHtml(formattedHtml, HtmlCompat.FROM_HTML_MODE_LEGACY)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun annotatedStringResource(@StringRes id: Int): AnnotatedString {
|
||||
val resources = resources()
|
||||
val density = LocalDensity.current
|
||||
return remember(id) {
|
||||
val text = resources.getText(id)
|
||||
spannableStringToAnnotatedString(text, density)
|
||||
}
|
||||
}
|
||||
|
||||
private fun spannableStringToAnnotatedString(
|
||||
text: CharSequence,
|
||||
density: Density,
|
||||
): AnnotatedString {
|
||||
return if (text is Spanned) {
|
||||
with(density) {
|
||||
buildAnnotatedString {
|
||||
append((text.toString()))
|
||||
text.getSpans(0, text.length, Any::class.java).forEach {
|
||||
val start = text.getSpanStart(it)
|
||||
val end = text.getSpanEnd(it)
|
||||
when (it) {
|
||||
is StyleSpan -> when (it.style) {
|
||||
Typeface.NORMAL -> addStyle(
|
||||
SpanStyle(
|
||||
fontWeight = FontWeight.Normal,
|
||||
fontStyle = FontStyle.Normal,
|
||||
),
|
||||
start,
|
||||
end
|
||||
)
|
||||
Typeface.BOLD -> addStyle(
|
||||
SpanStyle(
|
||||
fontWeight = FontWeight.Bold,
|
||||
fontStyle = FontStyle.Normal
|
||||
),
|
||||
start,
|
||||
end
|
||||
)
|
||||
Typeface.ITALIC -> addStyle(
|
||||
SpanStyle(
|
||||
fontWeight = FontWeight.Normal,
|
||||
fontStyle = FontStyle.Italic
|
||||
),
|
||||
start,
|
||||
end
|
||||
)
|
||||
Typeface.BOLD_ITALIC -> addStyle(
|
||||
SpanStyle(
|
||||
fontWeight = FontWeight.Bold,
|
||||
fontStyle = FontStyle.Italic
|
||||
),
|
||||
start,
|
||||
end
|
||||
)
|
||||
}
|
||||
is TypefaceSpan -> addStyle(
|
||||
SpanStyle(
|
||||
fontFamily = when (it.family) {
|
||||
FontFamily.SansSerif.name -> FontFamily.SansSerif
|
||||
FontFamily.Serif.name -> FontFamily.Serif
|
||||
FontFamily.Monospace.name -> FontFamily.Monospace
|
||||
FontFamily.Cursive.name -> FontFamily.Cursive
|
||||
else -> FontFamily.Default
|
||||
}
|
||||
),
|
||||
start,
|
||||
end
|
||||
)
|
||||
is AbsoluteSizeSpan -> addStyle(
|
||||
SpanStyle(fontSize = if (it.dip) it.size.dp.toSp() else it.size.toSp()),
|
||||
start,
|
||||
end
|
||||
)
|
||||
is RelativeSizeSpan -> addStyle(
|
||||
SpanStyle(fontSize = it.sizeChange.em),
|
||||
start,
|
||||
end
|
||||
)
|
||||
is StrikethroughSpan -> addStyle(
|
||||
SpanStyle(textDecoration = TextDecoration.LineThrough),
|
||||
start,
|
||||
end
|
||||
)
|
||||
is UnderlineSpan -> addStyle(
|
||||
SpanStyle(textDecoration = TextDecoration.Underline),
|
||||
start,
|
||||
end
|
||||
)
|
||||
is SuperscriptSpan -> addStyle(
|
||||
SpanStyle(baselineShift = BaselineShift.Superscript),
|
||||
start,
|
||||
end
|
||||
)
|
||||
is SubscriptSpan -> addStyle(
|
||||
SpanStyle(baselineShift = BaselineShift.Subscript),
|
||||
start,
|
||||
end
|
||||
)
|
||||
is ForegroundColorSpan -> addStyle(
|
||||
SpanStyle(color = Color(it.foregroundColor)),
|
||||
start,
|
||||
end
|
||||
)
|
||||
else -> addStyle(SpanStyle(color = Color.White), start, end)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
AnnotatedString(text.toString())
|
||||
}
|
||||
}
|
||||
|
||||
// maximum image file size to be auto-accepted
|
||||
const val MAX_IMAGE_SIZE: Long = 236700
|
||||
const val MAX_IMAGE_SIZE_AUTO_RCV: Long = MAX_IMAGE_SIZE * 2
|
||||
const val MAX_VOICE_SIZE_AUTO_RCV: Long = MAX_IMAGE_SIZE
|
||||
|
||||
const val MAX_VOICE_SIZE_FOR_SENDING: Long = 94680 // 6 chunks * 15780 bytes per chunk
|
||||
const val MAX_VOICE_MILLIS_FOR_SENDING: Int = 43_000
|
||||
|
||||
const val MAX_FILE_SIZE: Long = 8000000
|
||||
|
||||
fun getFilesDirectory(context: Context): String {
|
||||
return context.filesDir.toString()
|
||||
}
|
||||
|
||||
fun getAppFilesDirectory(context: Context): String {
|
||||
return "${getFilesDirectory(context)}/app_files"
|
||||
}
|
||||
|
||||
fun getAppFilePath(context: Context, fileName: String): String {
|
||||
return "${getAppFilesDirectory(context)}/$fileName"
|
||||
}
|
||||
|
||||
fun getAppFileUri(fileName: String): Uri {
|
||||
return Uri.parse("${getAppFilesDirectory(SimplexApp.context)}/$fileName")
|
||||
}
|
||||
|
||||
|
||||
fun getLoadedFilePath(context: Context, file: CIFile?): String? {
|
||||
return if (file?.filePath != null && file.loaded) {
|
||||
val filePath = getAppFilePath(context, file.filePath)
|
||||
if (File(filePath).exists()) filePath else null
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
// https://developer.android.com/training/data-storage/shared/documents-files#bitmap
|
||||
fun getLoadedImage(context: Context, file: CIFile?): Bitmap? {
|
||||
val filePath = getLoadedFilePath(context, file)
|
||||
return if (filePath != null) {
|
||||
try {
|
||||
val uri = FileProvider.getUriForFile(context, "${BuildConfig.APPLICATION_ID}.provider", File(filePath))
|
||||
val parcelFileDescriptor = context.contentResolver.openFileDescriptor(uri, "r")
|
||||
val fileDescriptor = parcelFileDescriptor?.fileDescriptor
|
||||
val image = decodeSampledBitmapFromFileDescriptor(fileDescriptor, 1000, 1000)
|
||||
parcelFileDescriptor?.close()
|
||||
image
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
}
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
// https://developer.android.com/topic/performance/graphics/load-bitmap#load-bitmap
|
||||
private fun decodeSampledBitmapFromFileDescriptor(fileDescriptor: FileDescriptor?, reqWidth: Int, reqHeight: Int): Bitmap {
|
||||
// First decode with inJustDecodeBounds=true to check dimensions
|
||||
return BitmapFactory.Options().run {
|
||||
inJustDecodeBounds = true
|
||||
BitmapFactory.decodeFileDescriptor(fileDescriptor, null, this)
|
||||
// Calculate inSampleSize
|
||||
inSampleSize = calculateInSampleSize(this, reqWidth, reqHeight)
|
||||
// Decode bitmap with inSampleSize set
|
||||
inJustDecodeBounds = false
|
||||
|
||||
BitmapFactory.decodeFileDescriptor(fileDescriptor, null, this)
|
||||
}
|
||||
}
|
||||
|
||||
private fun calculateInSampleSize(options: BitmapFactory.Options, reqWidth: Int, reqHeight: Int): Int {
|
||||
// Raw height and width of image
|
||||
val (height: Int, width: Int) = options.run { outHeight to outWidth }
|
||||
var inSampleSize = 1
|
||||
|
||||
if (height > reqHeight || width > reqWidth) {
|
||||
val halfHeight: Int = height / 2
|
||||
val halfWidth: Int = width / 2
|
||||
// Calculate the largest inSampleSize value that is a power of 2 and keeps both
|
||||
// height and width larger than the requested height and width.
|
||||
while (halfHeight / inSampleSize >= reqHeight && halfWidth / inSampleSize >= reqWidth) {
|
||||
inSampleSize *= 2
|
||||
}
|
||||
}
|
||||
|
||||
return inSampleSize
|
||||
}
|
||||
|
||||
fun getFileName(context: Context, uri: Uri): String? {
|
||||
return context.contentResolver.query(uri, null, null, null, null)?.use { cursor ->
|
||||
val nameIndex = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME)
|
||||
cursor.moveToFirst()
|
||||
cursor.getString(nameIndex)
|
||||
}
|
||||
}
|
||||
|
||||
fun getFileSize(context: Context, uri: Uri): Long? {
|
||||
return context.contentResolver.query(uri, null, null, null, null)?.use { cursor ->
|
||||
val sizeIndex = cursor.getColumnIndex(OpenableColumns.SIZE)
|
||||
cursor.moveToFirst()
|
||||
cursor.getLong(sizeIndex)
|
||||
}
|
||||
}
|
||||
|
||||
fun saveImage(context: Context, uri: Uri): String? {
|
||||
val source = ImageDecoder.createSource(SimplexApp.context.contentResolver, uri)
|
||||
val bitmap = ImageDecoder.decodeBitmap(source)
|
||||
return saveImage(context, bitmap)
|
||||
}
|
||||
|
||||
fun saveImage(context: Context, image: Bitmap): String? {
|
||||
return try {
|
||||
val ext = if (image.hasAlpha()) "png" else "jpg"
|
||||
val dataResized = resizeImageToDataSize(image, ext == "png", maxDataSize = MAX_IMAGE_SIZE)
|
||||
val fileToSave = generateNewFileName(context, "IMG", ext)
|
||||
val file = File(getAppFilePath(context, fileToSave))
|
||||
val output = FileOutputStream(file)
|
||||
dataResized.writeTo(output)
|
||||
output.flush()
|
||||
output.close()
|
||||
fileToSave
|
||||
} catch (e: Exception) {
|
||||
Log.e(chat.simplex.app.TAG, "Util.kt saveImage error: ${e.message}")
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
fun saveAnimImage(context: Context, uri: Uri): String? {
|
||||
return try {
|
||||
val filename = getFileName(context, uri)?.lowercase()
|
||||
var ext = when {
|
||||
// remove everything but extension
|
||||
filename?.contains(".") == true -> filename.replaceBeforeLast('.', "").replace(".", "")
|
||||
else -> "gif"
|
||||
}
|
||||
// Just in case the image has a strange extension
|
||||
if (ext.length < 3 || ext.length > 4) ext = "gif"
|
||||
val fileToSave = generateNewFileName(context, "IMG", ext)
|
||||
val file = File(getAppFilePath(context, fileToSave))
|
||||
val output = FileOutputStream(file)
|
||||
context.contentResolver.openInputStream(uri)!!.use { input ->
|
||||
output.use { output ->
|
||||
input.copyTo(output)
|
||||
}
|
||||
}
|
||||
fileToSave
|
||||
} catch (e: Exception) {
|
||||
Log.e(chat.simplex.app.TAG, "Util.kt saveAnimImage error: ${e.message}")
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
fun saveTempImageUncompressed(image: Bitmap, asPng: Boolean): File? {
|
||||
return try {
|
||||
val ext = if (asPng) "png" else "jpg"
|
||||
val tmpDir = SimplexApp.context.getDir("temp", Application.MODE_PRIVATE)
|
||||
return File(tmpDir.absolutePath + File.separator + generateNewFileName(SimplexApp.context, "IMG", ext)).apply {
|
||||
outputStream().use { out ->
|
||||
image.compress(if (asPng) Bitmap.CompressFormat.PNG else Bitmap.CompressFormat.JPEG, 85, out)
|
||||
out.flush()
|
||||
}
|
||||
deleteOnExit()
|
||||
SimplexApp.context.chatModel.filesToDelete.add(this)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Util.kt saveTempImageUncompressed error: ${e.message}")
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
fun saveFileFromUri(context: Context, uri: Uri): String? {
|
||||
return try {
|
||||
val inputStream = context.contentResolver.openInputStream(uri)
|
||||
val fileToSave = getFileName(context, uri)
|
||||
if (inputStream != null && fileToSave != null) {
|
||||
val destFileName = uniqueCombine(context, fileToSave)
|
||||
val destFile = File(getAppFilePath(context, destFileName))
|
||||
FileUtils.copy(inputStream, FileOutputStream(destFile))
|
||||
destFileName
|
||||
} else {
|
||||
Log.e(chat.simplex.app.TAG, "Util.kt saveFileFromUri null inputStream")
|
||||
null
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(chat.simplex.app.TAG, "Util.kt saveFileFromUri error: ${e.message}")
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
fun generateNewFileName(context: Context, prefix: String, ext: String): String {
|
||||
val sdf = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.US)
|
||||
sdf.timeZone = TimeZone.getTimeZone("GMT")
|
||||
val timestamp = sdf.format(Date())
|
||||
return uniqueCombine(context, "${prefix}_$timestamp.$ext")
|
||||
}
|
||||
|
||||
fun uniqueCombine(context: Context, fileName: String): String {
|
||||
val orig = File(fileName)
|
||||
val name = orig.nameWithoutExtension
|
||||
val ext = orig.extension
|
||||
fun tryCombine(n: Int): String {
|
||||
val suffix = if (n == 0) "" else "_$n"
|
||||
val f = "$name$suffix.$ext"
|
||||
return if (File(getAppFilePath(context, f)).exists()) tryCombine(n + 1) else f
|
||||
}
|
||||
return tryCombine(0)
|
||||
}
|
||||
|
||||
fun formatBytes(bytes: Long): String {
|
||||
if (bytes == 0.toLong()) {
|
||||
return "0 bytes"
|
||||
}
|
||||
val bytesDouble = bytes.toDouble()
|
||||
val k = 1000.toDouble()
|
||||
val units = arrayOf("bytes", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB")
|
||||
val i = kotlin.math.floor(log2(bytesDouble) / log2(k))
|
||||
val size = bytesDouble / k.pow(i)
|
||||
val unit = units[i.toInt()]
|
||||
|
||||
return if (i <= 1) {
|
||||
String.format("%.0f %s", size, unit)
|
||||
} else {
|
||||
String.format("%.2f %s", size, unit)
|
||||
}
|
||||
}
|
||||
|
||||
fun removeFile(context: Context, fileName: String): Boolean {
|
||||
val file = File(getAppFilePath(context, fileName))
|
||||
val fileDeleted = file.delete()
|
||||
if (!fileDeleted) {
|
||||
Log.e(chat.simplex.app.TAG, "Util.kt removeFile error")
|
||||
}
|
||||
return fileDeleted
|
||||
}
|
||||
|
||||
fun deleteAppFiles(context: Context) {
|
||||
val dir = File(getAppFilesDirectory(context))
|
||||
try {
|
||||
dir.list()?.forEach {
|
||||
removeFile(context, it)
|
||||
}
|
||||
} catch (e: java.lang.Exception) {
|
||||
Log.e(TAG, "Util deleteAppFiles error: ${e.stackTraceToString()}")
|
||||
}
|
||||
}
|
||||
|
||||
fun directoryFileCountAndSize(dir: String): Pair<Int, Long> { // count, size in bytes
|
||||
var fileCount = 0
|
||||
var bytes = 0L
|
||||
try {
|
||||
File(dir).listFiles()?.forEach {
|
||||
fileCount++
|
||||
bytes += it.length()
|
||||
}
|
||||
} catch (e: java.lang.Exception) {
|
||||
Log.e(TAG, "Util directoryFileCountAndSize error: ${e.stackTraceToString()}")
|
||||
}
|
||||
return fileCount to bytes
|
||||
}
|
||||
|
||||
fun Color.darker(factor: Float = 0.1f): Color =
|
||||
Color(max(red * (1 - factor), 0f), max(green * (1 - factor), 0f), max(blue * (1 - factor), 0f), alpha)
|
||||
|
||||
fun ByteArray.toBase64String() = Base64.encodeToString(this, Base64.DEFAULT)
|
||||
|
||||
fun String.toByteArrayFromBase64() = Base64.decode(this, Base64.DEFAULT)
|
||||
|
||||
val LongRange.Companion.saver
|
||||
get() = Saver<MutableState<LongRange>, Pair<Long, Long>>(
|
||||
save = { it.value.first to it.value.last },
|
||||
restore = { mutableStateOf(it.first..it.second) }
|
||||
)
|
||||
|
||||
/* Make sure that T class has @Serializable annotation */
|
||||
inline fun <reified T> serializableSaver(): Saver<T, *> = Saver(
|
||||
save = { json.encodeToString(it) },
|
||||
restore = { json.decodeFromString(it) }
|
||||
)
|
||||
@@ -1,140 +0,0 @@
|
||||
package chat.simplex.app.views.newchat
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.TheaterComedy
|
||||
import androidx.compose.material.icons.outlined.Info
|
||||
import androidx.compose.material.icons.outlined.Share
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import chat.simplex.app.R
|
||||
import chat.simplex.app.model.ChatModel
|
||||
import chat.simplex.app.ui.theme.*
|
||||
import chat.simplex.app.views.helpers.*
|
||||
|
||||
@Composable
|
||||
fun AddContactView(connReqInvitation: String, connIncognito: Boolean) {
|
||||
val cxt = LocalContext.current
|
||||
AddContactLayout(
|
||||
connReq = connReqInvitation,
|
||||
connIncognito = connIncognito,
|
||||
share = { shareText(cxt, connReqInvitation) }
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun AddContactLayout(connReq: String, connIncognito: Boolean, share: () -> Unit) {
|
||||
BoxWithConstraints {
|
||||
val screenHeight = maxHeight
|
||||
Column(
|
||||
Modifier
|
||||
.verticalScroll(rememberScrollState())
|
||||
.padding(bottom = 16.dp),
|
||||
verticalArrangement = Arrangement.SpaceBetween,
|
||||
) {
|
||||
AppBarTitle(stringResource(R.string.add_contact), false)
|
||||
Text(
|
||||
stringResource(R.string.show_QR_code_for_your_contact_to_scan_from_the_app__multiline),
|
||||
)
|
||||
Row {
|
||||
InfoAboutIncognito(
|
||||
connIncognito,
|
||||
true,
|
||||
generalGetString(R.string.incognito_random_profile_description),
|
||||
generalGetString(R.string.your_profile_will_be_sent)
|
||||
)
|
||||
}
|
||||
if (connReq.isNotEmpty()) {
|
||||
QRCode(
|
||||
connReq, Modifier
|
||||
.aspectRatio(1f)
|
||||
.padding(vertical = 3.dp)
|
||||
)
|
||||
} else {
|
||||
CircularProgressIndicator(
|
||||
Modifier
|
||||
.size(36.dp)
|
||||
.padding(4.dp)
|
||||
.align(Alignment.CenterHorizontally),
|
||||
color = HighOrLowlight,
|
||||
strokeWidth = 3.dp
|
||||
)
|
||||
}
|
||||
Text(
|
||||
annotatedStringResource(R.string.if_you_cannot_meet_in_person_show_QR_in_video_call_or_via_another_channel),
|
||||
lineHeight = 22.sp,
|
||||
modifier = Modifier
|
||||
.padding(top = 16.dp, bottom = if (screenHeight > 600.dp) 16.dp else 0.dp)
|
||||
)
|
||||
Row(
|
||||
Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.Center
|
||||
) {
|
||||
SimpleButton(stringResource(R.string.share_invitation_link), icon = Icons.Outlined.Share, click = share)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun InfoAboutIncognito(chatModelIncognito: Boolean, supportedIncognito: Boolean = true, onText: String, offText: String) {
|
||||
if (chatModelIncognito) {
|
||||
Row(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 4.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(
|
||||
if (supportedIncognito) Icons.Filled.TheaterComedy else Icons.Outlined.Info,
|
||||
stringResource(R.string.incognito),
|
||||
tint = if (supportedIncognito) Indigo else WarningOrange,
|
||||
modifier = Modifier.padding(end = 10.dp).size(20.dp)
|
||||
)
|
||||
Text(onText, textAlign = TextAlign.Left, style = MaterialTheme.typography.body2)
|
||||
}
|
||||
} else {
|
||||
Row(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 4.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(
|
||||
Icons.Outlined.Info,
|
||||
stringResource(R.string.incognito),
|
||||
tint = HighOrLowlight,
|
||||
modifier = Modifier.padding(end = 10.dp).size(20.dp)
|
||||
)
|
||||
Text(offText, textAlign = TextAlign.Left, style = MaterialTheme.typography.body2)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Preview(
|
||||
uiMode = Configuration.UI_MODE_NIGHT_YES,
|
||||
showBackground = true,
|
||||
name = "Dark Mode"
|
||||
)
|
||||
@Composable
|
||||
fun PreviewAddContactView() {
|
||||
SimpleXTheme {
|
||||
AddContactLayout(
|
||||
connReq = "https://simplex.chat/contact#/?v=1&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2FK1rslx-m5bpXVIdMZg9NLUZ_8JBm8xTt%23MCowBQYDK2VuAyEALDeVe-sG8mRY22LsXlPgiwTNs9dbiLrNuA7f3ZMAJ2w%3D",
|
||||
connIncognito = false,
|
||||
share = {}
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,161 +0,0 @@
|
||||
package chat.simplex.app.views.newchat
|
||||
|
||||
import android.Manifest
|
||||
import android.content.res.Configuration
|
||||
import android.net.Uri
|
||||
import android.util.Log
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.core.net.toUri
|
||||
import chat.simplex.app.R
|
||||
import chat.simplex.app.TAG
|
||||
import chat.simplex.app.model.ChatModel
|
||||
import chat.simplex.app.model.json
|
||||
import chat.simplex.app.ui.theme.DEFAULT_PADDING
|
||||
import chat.simplex.app.ui.theme.SimpleXTheme
|
||||
import chat.simplex.app.views.helpers.*
|
||||
import com.google.accompanist.permissions.rememberPermissionState
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Composable
|
||||
fun ScanToConnectView(chatModel: ChatModel, close: () -> Unit) {
|
||||
val cameraPermissionState = rememberPermissionState(permission = Manifest.permission.CAMERA)
|
||||
LaunchedEffect(Unit) {
|
||||
cameraPermissionState.launchPermissionRequest()
|
||||
}
|
||||
ConnectContactLayout(
|
||||
chatModelIncognito = chatModel.incognito.value,
|
||||
qrCodeScanner = {
|
||||
QRCodeScanner { connReqUri ->
|
||||
try {
|
||||
val uri = Uri.parse(connReqUri)
|
||||
withUriAction(uri) { linkType ->
|
||||
val action = suspend {
|
||||
Log.d(TAG, "connectViaUri: connecting")
|
||||
if (connectViaUri(chatModel, linkType, uri)) {
|
||||
close()
|
||||
}
|
||||
}
|
||||
if (linkType == ConnectionLinkType.GROUP) {
|
||||
AlertManager.shared.showAlertMsg(
|
||||
title = generalGetString(R.string.connect_via_group_link),
|
||||
text = generalGetString(R.string.you_will_join_group),
|
||||
confirmText = generalGetString(R.string.connect_via_link_verb),
|
||||
onConfirm = { withApi { action() } }
|
||||
)
|
||||
} else action()
|
||||
}
|
||||
} catch (e: RuntimeException) {
|
||||
AlertManager.shared.showAlertMsg(
|
||||
title = generalGetString(R.string.invalid_QR_code),
|
||||
text = generalGetString(R.string.this_QR_code_is_not_a_link)
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
enum class ConnectionLinkType {
|
||||
CONTACT, INVITATION, GROUP
|
||||
}
|
||||
|
||||
@Serializable
|
||||
sealed class CReqClientData {
|
||||
@Serializable @SerialName("group") data class Group(val groupLinkId: String): CReqClientData()
|
||||
}
|
||||
|
||||
fun withUriAction(uri: Uri, run: suspend (ConnectionLinkType) -> Unit) {
|
||||
val action = uri.path?.drop(1)?.replace("/", "")
|
||||
val data = uri.toString().replaceFirst("#/", "/").toUri().getQueryParameter("data")
|
||||
val type = when {
|
||||
data != null -> {
|
||||
val parsed = runCatching {
|
||||
json.decodeFromString(CReqClientData.serializer(), data)
|
||||
}
|
||||
when {
|
||||
parsed.getOrNull() is CReqClientData.Group -> ConnectionLinkType.GROUP
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
action == "contact" -> ConnectionLinkType.CONTACT
|
||||
action == "invitation" -> ConnectionLinkType.INVITATION
|
||||
else -> null
|
||||
}
|
||||
if (type != null) {
|
||||
withApi { run(type) }
|
||||
} else {
|
||||
AlertManager.shared.showAlertMsg(
|
||||
title = generalGetString(R.string.invalid_contact_link),
|
||||
text = generalGetString(R.string.this_link_is_not_a_valid_connection_link)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun connectViaUri(chatModel: ChatModel, action: ConnectionLinkType, uri: Uri): Boolean {
|
||||
val r = chatModel.controller.apiConnect(uri.toString())
|
||||
if (r) {
|
||||
AlertManager.shared.showAlertMsg(
|
||||
title = generalGetString(R.string.connection_request_sent),
|
||||
text =
|
||||
when (action) {
|
||||
ConnectionLinkType.CONTACT -> generalGetString(R.string.you_will_be_connected_when_your_connection_request_is_accepted)
|
||||
ConnectionLinkType.INVITATION -> generalGetString(R.string.you_will_be_connected_when_your_contacts_device_is_online)
|
||||
ConnectionLinkType.GROUP -> generalGetString(R.string.you_will_be_connected_when_group_host_device_is_online)
|
||||
}
|
||||
)
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ConnectContactLayout(chatModelIncognito: Boolean, qrCodeScanner: @Composable () -> Unit) {
|
||||
Column(
|
||||
Modifier.verticalScroll(rememberScrollState()).padding(horizontal = DEFAULT_PADDING),
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
AppBarTitle(stringResource(R.string.scan_QR_code), false)
|
||||
InfoAboutIncognito(
|
||||
chatModelIncognito,
|
||||
true,
|
||||
generalGetString(R.string.incognito_random_profile_description),
|
||||
generalGetString(R.string.your_profile_will_be_sent)
|
||||
)
|
||||
Box(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.aspectRatio(ratio = 1F)
|
||||
.padding(bottom = 12.dp)
|
||||
) { qrCodeScanner() }
|
||||
Text(
|
||||
annotatedStringResource(R.string.if_you_cannot_meet_in_person_scan_QR_in_video_call_or_ask_for_invitation_link),
|
||||
lineHeight = 22.sp
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Preview(
|
||||
uiMode = Configuration.UI_MODE_NIGHT_YES,
|
||||
showBackground = true,
|
||||
name = "Dark Mode"
|
||||
)
|
||||
@Composable
|
||||
fun PreviewConnectContactLayout() {
|
||||
SimpleXTheme {
|
||||
ConnectContactLayout(
|
||||
chatModelIncognito = false,
|
||||
qrCodeScanner = { Surface {} },
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,73 +0,0 @@
|
||||
package chat.simplex.app.views.onboarding
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalUriHandler
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import chat.simplex.app.R
|
||||
import chat.simplex.app.model.User
|
||||
import chat.simplex.app.ui.theme.DEFAULT_PADDING
|
||||
import chat.simplex.app.ui.theme.SimpleXTheme
|
||||
import chat.simplex.app.views.helpers.*
|
||||
|
||||
@Composable
|
||||
fun HowItWorks(user: User?, onboardingStage: MutableState<OnboardingStage?>? = null) {
|
||||
Column(Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = DEFAULT_PADDING),
|
||||
horizontalAlignment = Alignment.Start
|
||||
) {
|
||||
AppBarTitle(stringResource(R.string.how_simplex_works), false)
|
||||
ReadableText(R.string.many_people_asked_how_can_it_deliver)
|
||||
ReadableText(R.string.to_protect_privacy_simplex_has_ids_for_queues)
|
||||
ReadableText(R.string.you_control_servers_to_receive_your_contacts_to_send)
|
||||
ReadableText(R.string.only_client_devices_store_contacts_groups_e2e_encrypted_messages)
|
||||
if (onboardingStage == null) {
|
||||
val uriHandler = LocalUriHandler.current
|
||||
Text(
|
||||
annotatedStringResource(R.string.read_more_in_github_with_link),
|
||||
modifier = Modifier.padding(bottom = 12.dp).clickable { uriHandler.openUri("https://github.com/simplex-chat/simplex-chat#readme") },
|
||||
lineHeight = 22.sp
|
||||
)
|
||||
} else {
|
||||
ReadableText(R.string.read_more_in_github)
|
||||
}
|
||||
|
||||
Spacer(Modifier.fillMaxHeight().weight(1f))
|
||||
|
||||
if (onboardingStage != null) {
|
||||
Box(Modifier.fillMaxWidth().padding(bottom = 16.dp), contentAlignment = Alignment.Center) {
|
||||
OnboardingActionButton(user, onboardingStage, onclick = { ModalManager.shared.closeModal() })
|
||||
}
|
||||
Spacer(Modifier.fillMaxHeight().weight(1f))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ReadableText(@StringRes stringResId: Int) {
|
||||
Text(annotatedStringResource(stringResId), modifier = Modifier.padding(bottom = 12.dp), lineHeight = 22.sp)
|
||||
}
|
||||
|
||||
@Preview(showBackground = true)
|
||||
@Preview(
|
||||
uiMode = Configuration.UI_MODE_NIGHT_YES,
|
||||
showBackground = true,
|
||||
name = "Dark Mode"
|
||||
)
|
||||
@Composable
|
||||
fun PreviewHowItWorks() {
|
||||
SimpleXTheme {
|
||||
HowItWorks(user = null)
|
||||
}
|
||||
}
|
||||
@@ -1,68 +0,0 @@
|
||||
package chat.simplex.app.views.onboarding
|
||||
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.compose.foundation.*
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import chat.simplex.app.R
|
||||
import chat.simplex.app.model.ChatModel
|
||||
import chat.simplex.app.ui.theme.*
|
||||
import chat.simplex.app.views.helpers.*
|
||||
import chat.simplex.app.views.usersettings.NotificationsMode
|
||||
import chat.simplex.app.views.usersettings.changeNotificationsMode
|
||||
|
||||
@Composable
|
||||
fun SetNotificationsMode(m: ChatModel) {
|
||||
Column(
|
||||
Modifier
|
||||
.fillMaxSize()
|
||||
.verticalScroll(rememberScrollState())
|
||||
.padding(20.dp)
|
||||
) {
|
||||
AppBarTitle(stringResource(R.string.onboarding_notifications_mode_title), false)
|
||||
val currentMode = rememberSaveable { mutableStateOf(NotificationsMode.default) }
|
||||
Text(stringResource(R.string.onboarding_notifications_mode_subtitle))
|
||||
Spacer(Modifier.padding(DEFAULT_PADDING_HALF))
|
||||
NotificationButton(currentMode, NotificationsMode.OFF, R.string.onboarding_notifications_mode_off, R.string.onboarding_notifications_mode_off_desc)
|
||||
NotificationButton(currentMode, NotificationsMode.PERIODIC, R.string.onboarding_notifications_mode_periodic, R.string.onboarding_notifications_mode_periodic_desc)
|
||||
NotificationButton(currentMode, NotificationsMode.SERVICE, R.string.onboarding_notifications_mode_service, R.string.onboarding_notifications_mode_service_desc)
|
||||
Spacer(Modifier.fillMaxHeight().weight(1f))
|
||||
Box(Modifier.fillMaxWidth().padding(bottom = 16.dp), contentAlignment = Alignment.Center) {
|
||||
OnboardingActionButton(R.string.use_chat, OnboardingStage.OnboardingComplete, m.onboardingStage) {
|
||||
changeNotificationsMode(currentMode.value, m)
|
||||
}
|
||||
}
|
||||
Spacer(Modifier.fillMaxHeight().weight(1f))
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun NotificationButton(currentMode: MutableState<NotificationsMode>, mode: NotificationsMode, @StringRes title: Int, @StringRes description: Int) {
|
||||
TextButton(
|
||||
onClick = { currentMode.value = mode },
|
||||
border = BorderStroke(1.dp, color = if (currentMode.value == mode) MaterialTheme.colors.primary else HighOrLowlight.copy(alpha = 0.5f)),
|
||||
shape = RoundedCornerShape(15.dp),
|
||||
) {
|
||||
Column(Modifier.padding(bottom = 6.dp).padding(horizontal = 8.dp)) {
|
||||
Text(
|
||||
stringResource(title),
|
||||
style = MaterialTheme.typography.h2,
|
||||
fontWeight = FontWeight.Medium,
|
||||
color = if (currentMode.value == mode) MaterialTheme.colors.primary else HighOrLowlight,
|
||||
modifier = Modifier.padding(bottom = 4.dp)
|
||||
)
|
||||
Text(annotatedStringResource(description), color = MaterialTheme.colors.onBackground, lineHeight = 24.sp)
|
||||
}
|
||||
}
|
||||
Spacer(Modifier.height(DEFAULT_PADDING))
|
||||
}
|
||||
@@ -1,147 +0,0 @@
|
||||
package chat.simplex.app.views.onboarding
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.compose.foundation.*
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.ArrowForwardIos
|
||||
import androidx.compose.material.icons.outlined.Info
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.painter.Painter
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import chat.simplex.app.R
|
||||
import chat.simplex.app.model.ChatModel
|
||||
import chat.simplex.app.model.User
|
||||
import chat.simplex.app.ui.theme.*
|
||||
import chat.simplex.app.views.helpers.ModalManager
|
||||
|
||||
@Composable
|
||||
fun SimpleXInfo(chatModel: ChatModel, onboarding: Boolean = true) {
|
||||
SimpleXInfoLayout(
|
||||
user = chatModel.currentUser.value,
|
||||
onboardingStage = if (onboarding) chatModel.onboardingStage else null,
|
||||
showModal = { modalView -> { ModalManager.shared.showModal { modalView(chatModel) } } },
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SimpleXInfoLayout(
|
||||
user: User?,
|
||||
onboardingStage: MutableState<OnboardingStage?>?,
|
||||
showModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit),
|
||||
) {
|
||||
Column(
|
||||
Modifier
|
||||
.fillMaxSize()
|
||||
.verticalScroll(rememberScrollState())
|
||||
.padding(horizontal = DEFAULT_PADDING),
|
||||
) {
|
||||
Box(Modifier.fillMaxWidth().padding(top = 24.dp, bottom = 8.dp), contentAlignment = Alignment.Center) {
|
||||
SimpleXLogo()
|
||||
}
|
||||
|
||||
Text(stringResource(R.string.next_generation_of_private_messaging), style = MaterialTheme.typography.h2, modifier = Modifier.padding(bottom = 24.dp), textAlign = TextAlign.Center)
|
||||
|
||||
InfoRow(painterResource(R.drawable.privacy), R.string.privacy_redefined, R.string.first_platform_without_user_ids)
|
||||
InfoRow(painterResource(R.drawable.shield), R.string.immune_to_spam_and_abuse, R.string.people_can_connect_only_via_links_you_share)
|
||||
InfoRow(painterResource(R.drawable.decentralized), R.string.decentralized, R.string.opensource_protocol_and_code_anybody_can_run_servers)
|
||||
|
||||
Spacer(Modifier.fillMaxHeight().weight(1f))
|
||||
|
||||
if (onboardingStage != null) {
|
||||
Box(Modifier.fillMaxWidth(), contentAlignment = Alignment.Center) {
|
||||
OnboardingActionButton(user, onboardingStage)
|
||||
}
|
||||
Spacer(Modifier.fillMaxHeight().weight(1f))
|
||||
}
|
||||
|
||||
Box(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(bottom = 16.dp), contentAlignment = Alignment.Center
|
||||
) {
|
||||
SimpleButton(text = stringResource(R.string.how_it_works), icon = Icons.Outlined.Info,
|
||||
click = showModal { HowItWorks(user, onboardingStage) })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SimpleXLogo() {
|
||||
Image(
|
||||
painter = painterResource(if (isInDarkTheme()) R.drawable.logo_light else R.drawable.logo),
|
||||
contentDescription = stringResource(R.string.image_descr_simplex_logo),
|
||||
modifier = Modifier
|
||||
.padding(vertical = DEFAULT_PADDING)
|
||||
.fillMaxWidth(0.80f)
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun InfoRow(icon: Painter, @StringRes titleId: Int, @StringRes textId: Int) {
|
||||
Row(Modifier.padding(bottom = 20.dp), verticalAlignment = Alignment.Top) {
|
||||
Image(icon, contentDescription = null, modifier = Modifier
|
||||
.width(60.dp)
|
||||
.padding(top = 8.dp, end = 16.dp))
|
||||
Column(horizontalAlignment = Alignment.Start) {
|
||||
Text(stringResource(titleId), fontWeight = FontWeight.Bold, style = MaterialTheme.typography.h3, lineHeight = 24.sp)
|
||||
Text(stringResource(textId), lineHeight = 24.sp, style = MaterialTheme.typography.caption)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun OnboardingActionButton(user: User?, onboardingStage: MutableState<OnboardingStage?>, onclick: (() -> Unit)? = null) {
|
||||
if (user == null) {
|
||||
OnboardingActionButton(R.string.create_your_profile, onboarding = OnboardingStage.Step2_CreateProfile, onboardingStage, onclick)
|
||||
} else {
|
||||
OnboardingActionButton(R.string.make_private_connection, onboarding = OnboardingStage.OnboardingComplete, onboardingStage, onclick)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun OnboardingActionButton(
|
||||
@StringRes labelId: Int,
|
||||
onboarding: OnboardingStage?,
|
||||
onboardingStage: MutableState<OnboardingStage?>,
|
||||
onclick: (() -> Unit)?
|
||||
) {
|
||||
SimpleButtonFrame(click = {
|
||||
onclick?.invoke()
|
||||
onboardingStage.value = onboarding
|
||||
}) {
|
||||
Text(stringResource(labelId), style = MaterialTheme.typography.h2, color = MaterialTheme.colors.primary)
|
||||
Icon(
|
||||
Icons.Outlined.ArrowForwardIos, "next stage", tint = MaterialTheme.colors.primary,
|
||||
modifier = Modifier.padding(end = 8.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(showBackground = true)
|
||||
@Preview(
|
||||
uiMode = Configuration.UI_MODE_NIGHT_YES,
|
||||
showBackground = true,
|
||||
name = "Dark Mode"
|
||||
)
|
||||
@Composable
|
||||
fun PreviewSimpleXInfo() {
|
||||
SimpleXTheme {
|
||||
SimpleXInfoLayout(
|
||||
user = null,
|
||||
onboardingStage = null,
|
||||
showModal = { {} }
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,301 +0,0 @@
|
||||
package chat.simplex.app.views.onboarding
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.platform.LocalUriHandler
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import chat.simplex.app.R
|
||||
import chat.simplex.app.model.ChatModel
|
||||
import chat.simplex.app.ui.theme.*
|
||||
import chat.simplex.app.views.helpers.*
|
||||
|
||||
@Composable
|
||||
fun WhatsNewView(viaSettings: Boolean = false, close: () -> Unit) {
|
||||
val currentVersion = remember { mutableStateOf(versionDescriptions.lastIndex) }
|
||||
|
||||
@Composable
|
||||
fun featureDescription(icon: ImageVector, titleId: Int, descrId: Int, link: String?) {
|
||||
@Composable
|
||||
fun linkButton(link: String) {
|
||||
val uriHandler = LocalUriHandler.current
|
||||
Icon(
|
||||
Icons.Outlined.OpenInNew, stringResource(titleId), tint = MaterialTheme.colors.primary,
|
||||
modifier = Modifier
|
||||
.clickable { uriHandler.openUri(link) }
|
||||
)
|
||||
}
|
||||
|
||||
Column(
|
||||
horizontalAlignment = Alignment.Start
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
Icon(icon, stringResource(titleId), tint = HighOrLowlight)
|
||||
Text(
|
||||
generalGetString(titleId),
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
style = MaterialTheme.typography.h3,
|
||||
fontWeight = FontWeight.Medium
|
||||
)
|
||||
if (link != null) {
|
||||
linkButton(link)
|
||||
}
|
||||
}
|
||||
Text(generalGetString(descrId))
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun pagination() {
|
||||
Row(
|
||||
Modifier
|
||||
.padding(bottom = 16.dp)
|
||||
) {
|
||||
if (currentVersion.value > 0) {
|
||||
val prev = currentVersion.value - 1
|
||||
Surface(shape = RoundedCornerShape(20.dp)) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
modifier = Modifier
|
||||
.clickable { currentVersion.value = prev }
|
||||
.padding(8.dp)
|
||||
) {
|
||||
Icon(Icons.Outlined.ArrowBackIosNew, "previous", tint = MaterialTheme.colors.primary)
|
||||
Text(versionDescriptions[prev].version, color = MaterialTheme.colors.primary)
|
||||
}
|
||||
}
|
||||
}
|
||||
Spacer(Modifier.fillMaxWidth().weight(1f))
|
||||
if (currentVersion.value < versionDescriptions.lastIndex) {
|
||||
val next = currentVersion.value + 1
|
||||
Surface(shape = RoundedCornerShape(20.dp)) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
modifier = Modifier
|
||||
.clickable { currentVersion.value = next }
|
||||
.padding(8.dp)
|
||||
) {
|
||||
Text(versionDescriptions[next].version, color = MaterialTheme.colors.primary)
|
||||
Icon(Icons.Outlined.ArrowForwardIos, "next", tint = MaterialTheme.colors.primary)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val v = versionDescriptions[currentVersion.value]
|
||||
|
||||
ModalView(close = close) {
|
||||
Column(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = DEFAULT_PADDING),
|
||||
horizontalAlignment = Alignment.Start,
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||
) {
|
||||
Text(
|
||||
String.format(generalGetString(R.string.new_in_version), v.version),
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(DEFAULT_PADDING),
|
||||
textAlign = TextAlign.Center,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
style = MaterialTheme.typography.h1,
|
||||
fontWeight = FontWeight.Normal,
|
||||
color = HighOrLowlight
|
||||
)
|
||||
|
||||
v.features.forEach { feature ->
|
||||
featureDescription(feature.icon, feature.titleId, feature.descrId, feature.link)
|
||||
}
|
||||
|
||||
if (!viaSettings) {
|
||||
Spacer(Modifier.fillMaxHeight().weight(1f))
|
||||
Box(
|
||||
Modifier.fillMaxWidth(), contentAlignment = Alignment.Center
|
||||
) {
|
||||
Text(
|
||||
generalGetString(R.string.ok),
|
||||
modifier = Modifier.clickable(onClick = close),
|
||||
style = MaterialTheme.typography.h3,
|
||||
color = MaterialTheme.colors.primary
|
||||
)
|
||||
}
|
||||
Spacer(Modifier.fillMaxHeight().weight(1f))
|
||||
}
|
||||
|
||||
Spacer(Modifier.fillMaxHeight().weight(1f))
|
||||
|
||||
pagination()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private data class FeatureDescription(
|
||||
val icon: ImageVector,
|
||||
val titleId: Int,
|
||||
val descrId: Int,
|
||||
val link: String? = null
|
||||
)
|
||||
|
||||
private data class VersionDescription(
|
||||
val version: String,
|
||||
val features: List<FeatureDescription>
|
||||
)
|
||||
|
||||
private val versionDescriptions: List<VersionDescription> = listOf(
|
||||
VersionDescription(
|
||||
version = "v4.2",
|
||||
features = listOf(
|
||||
FeatureDescription(
|
||||
icon = Icons.Outlined.VerifiedUser,
|
||||
titleId = R.string.v4_2_security_assessment,
|
||||
descrId = R.string.v4_2_security_assessment_desc,
|
||||
link = "https://simplex.chat/blog/20221108-simplex-chat-v4.2-security-audit-new-website.html"
|
||||
),
|
||||
FeatureDescription(
|
||||
icon = Icons.Outlined.Group,
|
||||
titleId = R.string.v4_2_group_links,
|
||||
descrId = R.string.v4_2_group_links_desc
|
||||
),
|
||||
FeatureDescription(
|
||||
icon = Icons.Outlined.Check,
|
||||
titleId = R.string.v4_2_auto_accept_contact_requests,
|
||||
descrId = R.string.v4_2_auto_accept_contact_requests_desc
|
||||
),
|
||||
)
|
||||
),
|
||||
VersionDescription(
|
||||
version = "v4.3",
|
||||
features = listOf(
|
||||
FeatureDescription(
|
||||
icon = Icons.Outlined.Mic,
|
||||
titleId = R.string.v4_3_voice_messages,
|
||||
descrId = R.string.v4_3_voice_messages_desc
|
||||
),
|
||||
FeatureDescription(
|
||||
icon = Icons.Outlined.DeleteForever,
|
||||
titleId = R.string.v4_3_irreversible_message_deletion,
|
||||
descrId = R.string.v4_3_irreversible_message_deletion_desc
|
||||
),
|
||||
FeatureDescription(
|
||||
icon = Icons.Outlined.WifiTethering,
|
||||
titleId = R.string.v4_3_improved_server_configuration,
|
||||
descrId = R.string.v4_3_improved_server_configuration_desc
|
||||
),
|
||||
FeatureDescription(
|
||||
icon = Icons.Outlined.VisibilityOff,
|
||||
titleId = R.string.v4_3_improved_privacy_and_security,
|
||||
descrId = R.string.v4_3_improved_privacy_and_security_desc
|
||||
),
|
||||
)
|
||||
),
|
||||
VersionDescription(
|
||||
version = "v4.4",
|
||||
features = listOf(
|
||||
FeatureDescription(
|
||||
icon = Icons.Outlined.Timer,
|
||||
titleId = R.string.v4_4_disappearing_messages,
|
||||
descrId = R.string.v4_4_disappearing_messages_desc
|
||||
),
|
||||
FeatureDescription(
|
||||
icon = Icons.Outlined.Pending,
|
||||
titleId = R.string.v4_4_live_messages,
|
||||
descrId = R.string.v4_4_live_messages_desc
|
||||
),
|
||||
FeatureDescription(
|
||||
icon = Icons.Outlined.VerifiedUser,
|
||||
titleId = R.string.v4_4_verify_connection_security,
|
||||
descrId = R.string.v4_4_verify_connection_security_desc
|
||||
),
|
||||
FeatureDescription(
|
||||
icon = Icons.Outlined.Translate,
|
||||
titleId = R.string.v4_4_french_interface,
|
||||
descrId = R.string.v4_4_french_interface_descr
|
||||
)
|
||||
)
|
||||
),
|
||||
VersionDescription(
|
||||
version = "v4.5",
|
||||
features = listOf(
|
||||
FeatureDescription(
|
||||
icon = Icons.Outlined.ManageAccounts,
|
||||
titleId = R.string.v4_5_multiple_chat_profiles,
|
||||
descrId = R.string.v4_5_multiple_chat_profiles_descr
|
||||
),
|
||||
FeatureDescription(
|
||||
icon = Icons.Outlined.EditNote,
|
||||
titleId = R.string.v4_5_message_draft,
|
||||
descrId = R.string.v4_5_message_draft_descr
|
||||
),
|
||||
FeatureDescription(
|
||||
icon = Icons.Outlined.SafetyDivider,
|
||||
titleId = R.string.v4_5_transport_isolation,
|
||||
descrId = R.string.v4_5_transport_isolation_descr,
|
||||
link = "https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation"
|
||||
),
|
||||
FeatureDescription(
|
||||
icon = Icons.Outlined.Task,
|
||||
titleId = R.string.v4_5_private_filenames,
|
||||
descrId = R.string.v4_5_private_filenames_descr
|
||||
),
|
||||
FeatureDescription(
|
||||
icon = Icons.Outlined.Battery2Bar,
|
||||
titleId = R.string.v4_5_reduced_battery_usage,
|
||||
descrId = R.string.v4_5_reduced_battery_usage_descr
|
||||
),
|
||||
FeatureDescription(
|
||||
icon = Icons.Outlined.Translate,
|
||||
titleId = R.string.v4_5_italian_interface,
|
||||
descrId = R.string.v4_5_italian_interface_descr,
|
||||
link = "https://github.com/simplex-chat/simplex-chat/tree/stable#translate-the-apps"
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
private val lastVersion = versionDescriptions.last().version
|
||||
|
||||
fun setLastVersionDefault(m: ChatModel) {
|
||||
m.controller.appPrefs.whatsNewVersion.set(lastVersion)
|
||||
}
|
||||
|
||||
fun shouldShowWhatsNew(m: ChatModel): Boolean {
|
||||
val v = m.controller.appPrefs.whatsNewVersion.get()
|
||||
setLastVersionDefault(m)
|
||||
return v != lastVersion
|
||||
}
|
||||
|
||||
@Preview(showBackground = true)
|
||||
@Preview(
|
||||
uiMode = Configuration.UI_MODE_NIGHT_YES,
|
||||
showBackground = true,
|
||||
name = "Dark Mode"
|
||||
)
|
||||
@Composable
|
||||
fun PreviewWhatsNewView() {
|
||||
SimpleXTheme {
|
||||
WhatsNewView(
|
||||
viaSettings = true,
|
||||
close = {}
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,167 +0,0 @@
|
||||
package chat.simplex.app.views.usersettings
|
||||
|
||||
import SectionCustomFooter
|
||||
import SectionDivider
|
||||
import SectionItemView
|
||||
import SectionView
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.TheaterComedy
|
||||
import androidx.compose.material.icons.outlined.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import chat.simplex.app.R
|
||||
import chat.simplex.app.model.*
|
||||
import chat.simplex.app.ui.theme.*
|
||||
import chat.simplex.app.views.helpers.*
|
||||
|
||||
@Composable
|
||||
fun AcceptRequestsView(m: ChatModel, contactLink: UserContactLinkRec) {
|
||||
var contactLink by remember { mutableStateOf(contactLink) }
|
||||
AcceptRequestsLayout(
|
||||
contactLink,
|
||||
saveState = { new: MutableState<AutoAcceptState>, old: MutableState<AutoAcceptState> ->
|
||||
withApi {
|
||||
val link = m.controller.userAddressAutoAccept(new.value.autoAccept)
|
||||
if (link != null) {
|
||||
contactLink = link
|
||||
m.userAddress.value = link
|
||||
old.value = new.value
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun AcceptRequestsLayout(
|
||||
contactLink: UserContactLinkRec,
|
||||
saveState: (new: MutableState<AutoAcceptState>, old: MutableState<AutoAcceptState>) -> Unit,
|
||||
) {
|
||||
Column(
|
||||
Modifier.fillMaxWidth().verticalScroll(rememberScrollState()),
|
||||
) {
|
||||
AppBarTitle(stringResource(R.string.contact_requests))
|
||||
val autoAcceptState = remember { mutableStateOf(AutoAcceptState(contactLink)) }
|
||||
val autoAcceptStateSaved = remember { mutableStateOf(autoAcceptState.value) }
|
||||
SectionView(stringResource(R.string.accept_requests).uppercase()) {
|
||||
SectionItemView {
|
||||
PreferenceToggleWithIcon(stringResource(R.string.accept_automatically), Icons.Outlined.Check, checked = autoAcceptState.value.enable) {
|
||||
autoAcceptState.value = if (!it)
|
||||
AutoAcceptState()
|
||||
else
|
||||
AutoAcceptState(it, autoAcceptState.value.incognito, autoAcceptState.value.welcomeText)
|
||||
}
|
||||
}
|
||||
if (autoAcceptState.value.enable) {
|
||||
SectionDivider()
|
||||
SectionItemView {
|
||||
PreferenceToggleWithIcon(
|
||||
stringResource(R.string.incognito),
|
||||
if (autoAcceptState.value.incognito) Icons.Filled.TheaterComedy else Icons.Outlined.TheaterComedy,
|
||||
if (autoAcceptState.value.incognito) Indigo else HighOrLowlight,
|
||||
autoAcceptState.value.incognito,
|
||||
) {
|
||||
autoAcceptState.value = AutoAcceptState(autoAcceptState.value.enable, it, autoAcceptState.value.welcomeText)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
val welcomeText = remember { mutableStateOf(autoAcceptState.value.welcomeText) }
|
||||
SectionCustomFooter(PaddingValues(horizontal = DEFAULT_PADDING)) {
|
||||
ButtonsFooter(
|
||||
cancel = {
|
||||
autoAcceptState.value = autoAcceptStateSaved.value
|
||||
welcomeText.value = autoAcceptStateSaved.value.welcomeText
|
||||
},
|
||||
save = { saveState(autoAcceptState, autoAcceptStateSaved) },
|
||||
disabled = autoAcceptState.value == autoAcceptStateSaved.value
|
||||
)
|
||||
}
|
||||
Spacer(Modifier.height(DEFAULT_PADDING))
|
||||
if (autoAcceptState.value.enable) {
|
||||
Text(
|
||||
stringResource(R.string.section_title_welcome_message), color = HighOrLowlight, style = MaterialTheme.typography.body2,
|
||||
modifier = Modifier.padding(start = DEFAULT_PADDING, bottom = 5.dp), fontSize = 12.sp
|
||||
)
|
||||
TextEditor(Modifier.padding(horizontal = DEFAULT_PADDING).height(160.dp), text = welcomeText)
|
||||
LaunchedEffect(welcomeText.value) {
|
||||
if (welcomeText.value != autoAcceptState.value.welcomeText) {
|
||||
autoAcceptState.value = AutoAcceptState(autoAcceptState.value.enable, autoAcceptState.value.incognito, welcomeText.value)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ButtonsFooter(cancel: () -> Unit, save: () -> Unit, disabled: Boolean) {
|
||||
Row(
|
||||
Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
FooterButton(Icons.Outlined.Replay, stringResource(R.string.cancel_verb), cancel, disabled)
|
||||
FooterButton(Icons.Outlined.Check, stringResource(R.string.save_verb), save, disabled)
|
||||
}
|
||||
}
|
||||
|
||||
private class AutoAcceptState {
|
||||
var enable: Boolean = false
|
||||
private set
|
||||
var incognito: Boolean = false
|
||||
private set
|
||||
var welcomeText: String = ""
|
||||
private set
|
||||
|
||||
constructor(enable: Boolean = false, incognito: Boolean = false, welcomeText: String = "") {
|
||||
this.enable = enable
|
||||
this.incognito = incognito
|
||||
this.welcomeText = welcomeText
|
||||
}
|
||||
|
||||
constructor(contactLink: UserContactLinkRec) {
|
||||
contactLink.autoAccept?.let { aa ->
|
||||
enable = true
|
||||
incognito = aa.acceptIncognito
|
||||
aa.autoReply?.let { msg ->
|
||||
welcomeText = msg.text
|
||||
} ?: run {
|
||||
welcomeText = ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val autoAccept: AutoAccept?
|
||||
get() {
|
||||
if (enable) {
|
||||
var autoReply: MsgContent? = null
|
||||
val s = welcomeText.trim()
|
||||
if (s != "") {
|
||||
autoReply = MsgContent.MCText(s)
|
||||
}
|
||||
return AutoAccept(incognito, autoReply)
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (other !is AutoAcceptState) return false
|
||||
return this.enable == other.enable && this.incognito == other.incognito && this.welcomeText == other.welcomeText
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = enable.hashCode()
|
||||
result = 31 * result + incognito.hashCode()
|
||||
result = 31 * result + welcomeText.hashCode()
|
||||
return result
|
||||
}
|
||||
}
|
||||
@@ -1,213 +0,0 @@
|
||||
package chat.simplex.app.views.usersettings
|
||||
|
||||
import SectionCustomFooter
|
||||
import SectionDivider
|
||||
import SectionItemViewSpaceBetween
|
||||
import SectionSpacer
|
||||
import SectionView
|
||||
import android.content.ComponentName
|
||||
import android.content.pm.PackageManager
|
||||
import android.content.pm.PackageManager.COMPONENT_ENABLED_STATE_DEFAULT
|
||||
import android.content.pm.PackageManager.COMPONENT_ENABLED_STATE_ENABLED
|
||||
import androidx.compose.foundation.*
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyRow
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.material.MaterialTheme.colors
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Circle
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.shadow
|
||||
import androidx.compose.ui.graphics.*
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.graphics.drawable.toBitmap
|
||||
import chat.simplex.app.*
|
||||
import chat.simplex.app.R
|
||||
import chat.simplex.app.ui.theme.*
|
||||
import chat.simplex.app.views.helpers.*
|
||||
import com.godaddy.android.colorpicker.*
|
||||
|
||||
enum class AppIcon(val resId: Int) {
|
||||
DEFAULT(R.mipmap.icon),
|
||||
DARK_BLUE(R.mipmap.icon_dark_blue),
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun AppearanceView() {
|
||||
val appIcon = remember { mutableStateOf(findEnabledIcon()) }
|
||||
|
||||
fun setAppIcon(newIcon: AppIcon) {
|
||||
if (appIcon.value == newIcon) return
|
||||
val newComponent = ComponentName(BuildConfig.APPLICATION_ID, "chat.simplex.app.MainActivity_${newIcon.name.lowercase()}")
|
||||
val oldComponent = ComponentName(BuildConfig.APPLICATION_ID, "chat.simplex.app.MainActivity_${appIcon.value.name.lowercase()}")
|
||||
SimplexApp.context.packageManager.setComponentEnabledSetting(
|
||||
newComponent,
|
||||
COMPONENT_ENABLED_STATE_ENABLED, PackageManager.DONT_KILL_APP
|
||||
)
|
||||
|
||||
SimplexApp.context.packageManager.setComponentEnabledSetting(
|
||||
oldComponent,
|
||||
PackageManager.COMPONENT_ENABLED_STATE_DISABLED, PackageManager.DONT_KILL_APP
|
||||
)
|
||||
|
||||
appIcon.value = newIcon
|
||||
}
|
||||
|
||||
AppearanceLayout(
|
||||
appIcon,
|
||||
changeIcon = ::setAppIcon,
|
||||
editPrimaryColor = { primary ->
|
||||
ModalManager.shared.showModalCloseable { close ->
|
||||
ColorEditor(primary, close)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@Composable fun AppearanceLayout(
|
||||
icon: MutableState<AppIcon>,
|
||||
changeIcon: (AppIcon) -> Unit,
|
||||
editPrimaryColor: (Color) -> Unit,
|
||||
) {
|
||||
Column(
|
||||
Modifier.fillMaxWidth(),
|
||||
horizontalAlignment = Alignment.Start,
|
||||
) {
|
||||
AppBarTitle(stringResource(R.string.appearance_settings))
|
||||
SectionView(stringResource(R.string.settings_section_title_icon), padding = PaddingValues(horizontal = DEFAULT_PADDING_HALF)) {
|
||||
LazyRow {
|
||||
items(AppIcon.values().size, { index -> AppIcon.values()[index] }) { index ->
|
||||
val item = AppIcon.values()[index]
|
||||
val mipmap = ContextCompat.getDrawable(LocalContext.current, item.resId)!!
|
||||
Image(
|
||||
bitmap = mipmap.toBitmap().asImageBitmap(),
|
||||
contentDescription = "",
|
||||
contentScale = ContentScale.Fit,
|
||||
modifier = Modifier
|
||||
.shadow(if (item == icon.value) 1.dp else 0.dp, ambientColor = colors.secondary)
|
||||
.size(70.dp)
|
||||
.clickable { changeIcon(item) }
|
||||
.padding(10.dp)
|
||||
)
|
||||
|
||||
if (index + 1 != AppIcon.values().size) {
|
||||
Spacer(Modifier.padding(horizontal = 4.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SectionSpacer()
|
||||
val currentTheme by CurrentColors.collectAsState()
|
||||
SectionView(stringResource(R.string.settings_section_title_themes)) {
|
||||
SectionItemViewSpaceBetween {
|
||||
val darkTheme = isSystemInDarkTheme()
|
||||
val state = remember { derivedStateOf { currentTheme.second } }
|
||||
ThemeSelector(state) {
|
||||
ThemeManager.applyTheme(it.name, darkTheme)
|
||||
}
|
||||
}
|
||||
SectionDivider()
|
||||
SectionItemViewSpaceBetween({ editPrimaryColor(currentTheme.first.primary) }) {
|
||||
val title = generalGetString(R.string.color_primary)
|
||||
Text(title)
|
||||
Icon(Icons.Filled.Circle, title, tint = colors.primary)
|
||||
}
|
||||
}
|
||||
if (currentTheme.first.primary != LightColorPalette.primary) {
|
||||
SectionCustomFooter(PaddingValues(start = 7.dp, end = 7.dp, top = 5.dp)) {
|
||||
TextButton(
|
||||
onClick = {
|
||||
ThemeManager.saveAndApplyPrimaryColor(LightColorPalette.primary)
|
||||
},
|
||||
) {
|
||||
Text(generalGetString(R.string.reset_color))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ColorEditor(
|
||||
initialColor: Color,
|
||||
close: () -> Unit,
|
||||
) {
|
||||
Column(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
) {
|
||||
AppBarTitle(stringResource(R.string.color_primary))
|
||||
var currentColor by remember { mutableStateOf(initialColor) }
|
||||
ColorPicker(initialColor) {
|
||||
currentColor = it
|
||||
}
|
||||
|
||||
SectionSpacer()
|
||||
|
||||
TextButton(
|
||||
onClick = {
|
||||
ThemeManager.saveAndApplyPrimaryColor(currentColor)
|
||||
close()
|
||||
},
|
||||
Modifier.align(Alignment.CenterHorizontally),
|
||||
colors = ButtonDefaults.textButtonColors(contentColor = currentColor)
|
||||
) {
|
||||
Text(generalGetString(R.string.save_color))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ColorPicker(initialColor: Color, onColorChanged: (Color) -> Unit) {
|
||||
ClassicColorPicker(
|
||||
color = initialColor,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(300.dp),
|
||||
showAlphaBar = false,
|
||||
onColorChanged = { color: HsvColor ->
|
||||
onColorChanged(color.toColor())
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ThemeSelector(state: State<DefaultTheme>, onSelected: (DefaultTheme) -> Unit) {
|
||||
val darkTheme = isSystemInDarkTheme()
|
||||
val values by remember { mutableStateOf(ThemeManager.allThemes(darkTheme).map { it.second to it.third }) }
|
||||
ExposedDropDownSettingRow(
|
||||
generalGetString(R.string.theme),
|
||||
values,
|
||||
state,
|
||||
icon = null,
|
||||
enabled = remember { mutableStateOf(true) },
|
||||
onSelected = onSelected
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
private fun findEnabledIcon(): AppIcon = AppIcon.values().first { icon ->
|
||||
SimplexApp.context.packageManager.getComponentEnabledSetting(
|
||||
ComponentName(BuildConfig.APPLICATION_ID, "chat.simplex.app.MainActivity_${icon.name.lowercase()}")
|
||||
).let { it == COMPONENT_ENABLED_STATE_DEFAULT || it == COMPONENT_ENABLED_STATE_ENABLED }
|
||||
}
|
||||
|
||||
@Preview(showBackground = true)
|
||||
@Composable
|
||||
fun PreviewAppearanceSettings() {
|
||||
SimpleXTheme {
|
||||
AppearanceLayout(
|
||||
icon = remember { mutableStateOf(AppIcon.DARK_BLUE) },
|
||||
changeIcon = {},
|
||||
editPrimaryColor = {},
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
package chat.simplex.app.views.usersettings
|
||||
|
||||
import SectionView
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.Videocam
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import chat.simplex.app.R
|
||||
import chat.simplex.app.model.ChatModel
|
||||
|
||||
@Composable
|
||||
fun ExperimentalFeaturesView(chatModel: ChatModel, enableCalls: MutableState<Boolean>) {
|
||||
Column(
|
||||
Modifier.fillMaxWidth(),
|
||||
horizontalAlignment = Alignment.Start
|
||||
) {
|
||||
Text(
|
||||
stringResource(R.string.settings_experimental_features),
|
||||
style = MaterialTheme.typography.h1,
|
||||
modifier = Modifier.padding(start = 16.dp, bottom = 24.dp)
|
||||
)
|
||||
SectionView("") {
|
||||
SettingsPreferenceItem(Icons.Outlined.Videocam, stringResource(R.string.settings_audio_video_calls), chatModel.controller.appPrefs.experimentalCalls, enableCalls)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
package chat.simplex.app.views.usersettings
|
||||
|
||||
import androidx.compose.foundation.*
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import chat.simplex.app.R
|
||||
import chat.simplex.app.ui.theme.DEFAULT_PADDING
|
||||
import chat.simplex.app.views.helpers.AppBarTitle
|
||||
import chat.simplex.app.views.helpers.generalGetString
|
||||
|
||||
@Composable
|
||||
fun IncognitoView() {
|
||||
IncognitoLayout()
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun IncognitoLayout() {
|
||||
Column {
|
||||
AppBarTitle(stringResource(R.string.settings_section_title_incognito))
|
||||
Column(
|
||||
Modifier
|
||||
.verticalScroll(rememberScrollState())
|
||||
.padding(horizontal = DEFAULT_PADDING),
|
||||
verticalArrangement = Arrangement.spacedBy(20.dp)
|
||||
) {
|
||||
Text(generalGetString(R.string.incognito_info_protects))
|
||||
Text(generalGetString(R.string.incognito_info_allows))
|
||||
Text(generalGetString(R.string.incognito_info_share))
|
||||
Text(generalGetString(R.string.incognito_info_find))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,307 +0,0 @@
|
||||
package chat.simplex.app.views.usersettings
|
||||
|
||||
import SectionDivider
|
||||
import SectionItemView
|
||||
import SectionItemWithValue
|
||||
import SectionView
|
||||
import SectionViewSelectable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import chat.simplex.app.R
|
||||
import chat.simplex.app.model.*
|
||||
import chat.simplex.app.ui.theme.*
|
||||
import chat.simplex.app.views.helpers.*
|
||||
|
||||
@Composable
|
||||
fun NetworkAndServersView(
|
||||
chatModel: ChatModel,
|
||||
showModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit),
|
||||
showSettingsModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit),
|
||||
) {
|
||||
// It's not a state, just a one-time value. Shouldn't be used in any state-related situations
|
||||
val netCfg = remember { chatModel.controller.getNetCfg() }
|
||||
val networkUseSocksProxy: MutableState<Boolean> = remember { mutableStateOf(netCfg.useSocksProxy) }
|
||||
val developerTools = chatModel.controller.appPrefs.developerTools.get()
|
||||
val onionHosts = remember { mutableStateOf(netCfg.onionHosts) }
|
||||
val sessionMode = remember { mutableStateOf(netCfg.sessionMode) }
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
chatModel.userSMPServersUnsaved.value = null
|
||||
}
|
||||
|
||||
NetworkAndServersLayout(
|
||||
developerTools = developerTools,
|
||||
networkUseSocksProxy = networkUseSocksProxy,
|
||||
onionHosts = onionHosts,
|
||||
sessionMode = sessionMode,
|
||||
showModal = showModal,
|
||||
showSettingsModal = showSettingsModal,
|
||||
toggleSocksProxy = { enable ->
|
||||
if (enable) {
|
||||
AlertManager.shared.showAlertMsg(
|
||||
title = generalGetString(R.string.network_enable_socks),
|
||||
text = generalGetString(R.string.network_enable_socks_info),
|
||||
confirmText = generalGetString(R.string.confirm_verb),
|
||||
onConfirm = {
|
||||
withApi {
|
||||
chatModel.controller.apiSetNetworkConfig(NetCfg.proxyDefaults)
|
||||
chatModel.controller.setNetCfg(NetCfg.proxyDefaults)
|
||||
networkUseSocksProxy.value = true
|
||||
onionHosts.value = NetCfg.proxyDefaults.onionHosts
|
||||
}
|
||||
}
|
||||
)
|
||||
} else {
|
||||
AlertManager.shared.showAlertMsg(
|
||||
title = generalGetString(R.string.network_disable_socks),
|
||||
text = generalGetString(R.string.network_disable_socks_info),
|
||||
confirmText = generalGetString(R.string.confirm_verb),
|
||||
onConfirm = {
|
||||
withApi {
|
||||
chatModel.controller.apiSetNetworkConfig(NetCfg.defaults)
|
||||
chatModel.controller.setNetCfg(NetCfg.defaults)
|
||||
networkUseSocksProxy.value = false
|
||||
onionHosts.value = NetCfg.defaults.onionHosts
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
},
|
||||
useOnion = {
|
||||
if (onionHosts.value == it) return@NetworkAndServersLayout
|
||||
val prevValue = onionHosts.value
|
||||
onionHosts.value = it
|
||||
val startsWith = when (it) {
|
||||
OnionHosts.NEVER -> generalGetString(R.string.network_use_onion_hosts_no_desc_in_alert)
|
||||
OnionHosts.PREFER -> generalGetString(R.string.network_use_onion_hosts_prefer_desc_in_alert)
|
||||
OnionHosts.REQUIRED -> generalGetString(R.string.network_use_onion_hosts_required_desc_in_alert)
|
||||
}
|
||||
updateNetworkSettingsDialog(
|
||||
title = generalGetString(R.string.update_onion_hosts_settings_question),
|
||||
startsWith,
|
||||
onDismiss = {
|
||||
onionHosts.value = prevValue
|
||||
}
|
||||
) {
|
||||
withApi {
|
||||
val newCfg = chatModel.controller.getNetCfg().withOnionHosts(it)
|
||||
val res = chatModel.controller.apiSetNetworkConfig(newCfg)
|
||||
if (res) {
|
||||
chatModel.controller.setNetCfg(newCfg)
|
||||
onionHosts.value = it
|
||||
} else {
|
||||
onionHosts.value = prevValue
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
updateSessionMode = {
|
||||
if (sessionMode.value == it) return@NetworkAndServersLayout
|
||||
val prevValue = sessionMode.value
|
||||
sessionMode.value = it
|
||||
val startsWith = when (it) {
|
||||
TransportSessionMode.User -> generalGetString(R.string.network_session_mode_user_description)
|
||||
TransportSessionMode.Entity -> generalGetString(R.string.network_session_mode_entity_description)
|
||||
}
|
||||
updateNetworkSettingsDialog(
|
||||
title = generalGetString(R.string.update_network_session_mode_question),
|
||||
startsWith,
|
||||
onDismiss = { sessionMode.value = prevValue }
|
||||
) {
|
||||
withApi {
|
||||
val newCfg = chatModel.controller.getNetCfg().copy(sessionMode = it)
|
||||
val res = chatModel.controller.apiSetNetworkConfig(newCfg)
|
||||
if (res) {
|
||||
chatModel.controller.setNetCfg(newCfg)
|
||||
sessionMode.value = it
|
||||
} else {
|
||||
sessionMode.value = prevValue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@Composable fun NetworkAndServersLayout(
|
||||
developerTools: Boolean,
|
||||
networkUseSocksProxy: MutableState<Boolean>,
|
||||
onionHosts: MutableState<OnionHosts>,
|
||||
sessionMode: MutableState<TransportSessionMode>,
|
||||
showModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit),
|
||||
showSettingsModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit),
|
||||
toggleSocksProxy: (Boolean) -> Unit,
|
||||
useOnion: (OnionHosts) -> Unit,
|
||||
updateSessionMode: (TransportSessionMode) -> Unit,
|
||||
) {
|
||||
Column(
|
||||
Modifier.fillMaxWidth(),
|
||||
horizontalAlignment = Alignment.Start,
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
AppBarTitle(stringResource(R.string.network_and_servers))
|
||||
SectionView(generalGetString(R.string.settings_section_title_messages)) {
|
||||
SettingsActionItem(Icons.Outlined.Dns, stringResource(R.string.smp_servers), showSettingsModal { SMPServersView(it) })
|
||||
SectionDivider()
|
||||
SectionItemView {
|
||||
UseSocksProxySwitch(networkUseSocksProxy, toggleSocksProxy)
|
||||
}
|
||||
SectionDivider()
|
||||
UseOnionHosts(onionHosts, networkUseSocksProxy, showSettingsModal, useOnion)
|
||||
SectionDivider()
|
||||
if (developerTools) {
|
||||
SessionModePicker(sessionMode, showSettingsModal, updateSessionMode)
|
||||
SectionDivider()
|
||||
}
|
||||
SettingsActionItem(Icons.Outlined.Cable, stringResource(R.string.network_settings), showSettingsModal { AdvancedNetworkSettingsView(it) })
|
||||
}
|
||||
Spacer(Modifier.height(8.dp))
|
||||
SectionView(generalGetString(R.string.settings_section_title_calls)) {
|
||||
SettingsActionItem(Icons.Outlined.ElectricalServices, stringResource(R.string.webrtc_ice_servers), showModal { RTCServersView(it) })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun UseSocksProxySwitch(
|
||||
networkUseSocksProxy: MutableState<Boolean>,
|
||||
toggleSocksProxy: (Boolean) -> Unit
|
||||
) {
|
||||
Row(
|
||||
Modifier.fillMaxWidth(),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
Row(
|
||||
Modifier.weight(1f),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
Icon(
|
||||
Icons.Outlined.SettingsEthernet,
|
||||
stringResource(R.string.network_socks_toggle),
|
||||
tint = HighOrLowlight
|
||||
)
|
||||
Text(stringResource(R.string.network_socks_toggle))
|
||||
}
|
||||
Switch(
|
||||
checked = networkUseSocksProxy.value,
|
||||
onCheckedChange = toggleSocksProxy,
|
||||
colors = SwitchDefaults.colors(
|
||||
checkedThumbColor = MaterialTheme.colors.primary,
|
||||
uncheckedThumbColor = HighOrLowlight
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun UseOnionHosts(
|
||||
onionHosts: MutableState<OnionHosts>,
|
||||
enabled: State<Boolean>,
|
||||
showModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit),
|
||||
useOnion: (OnionHosts) -> Unit,
|
||||
) {
|
||||
val values = remember {
|
||||
OnionHosts.values().map {
|
||||
when (it) {
|
||||
OnionHosts.NEVER -> ValueTitleDesc(OnionHosts.NEVER, generalGetString(R.string.network_use_onion_hosts_no), generalGetString(R.string.network_use_onion_hosts_no_desc))
|
||||
OnionHosts.PREFER -> ValueTitleDesc(OnionHosts.PREFER, generalGetString(R.string.network_use_onion_hosts_prefer), generalGetString(R.string.network_use_onion_hosts_prefer_desc))
|
||||
OnionHosts.REQUIRED -> ValueTitleDesc(OnionHosts.REQUIRED, generalGetString(R.string.network_use_onion_hosts_required), generalGetString(R.string.network_use_onion_hosts_required_desc))
|
||||
}
|
||||
}
|
||||
}
|
||||
val onSelected = showModal {
|
||||
Column(
|
||||
Modifier.fillMaxWidth(),
|
||||
horizontalAlignment = Alignment.Start,
|
||||
) {
|
||||
AppBarTitle(stringResource(R.string.network_use_onion_hosts))
|
||||
SectionViewSelectable(null, onionHosts, values, useOnion)
|
||||
}
|
||||
}
|
||||
|
||||
SectionItemWithValue(
|
||||
generalGetString(R.string.network_use_onion_hosts),
|
||||
onionHosts,
|
||||
values,
|
||||
icon = Icons.Outlined.Security,
|
||||
enabled = enabled,
|
||||
onSelected = onSelected
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SessionModePicker(
|
||||
sessionMode: MutableState<TransportSessionMode>,
|
||||
showModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit),
|
||||
updateSessionMode: (TransportSessionMode) -> Unit,
|
||||
) {
|
||||
val values = remember {
|
||||
TransportSessionMode.values().map {
|
||||
when (it) {
|
||||
TransportSessionMode.User -> ValueTitleDesc(TransportSessionMode.User, generalGetString(R.string.network_session_mode_user), generalGetString(R.string.network_session_mode_user_description))
|
||||
TransportSessionMode.Entity -> ValueTitleDesc(TransportSessionMode.Entity, generalGetString(R.string.network_session_mode_entity), generalGetString(R.string.network_session_mode_entity_description))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SectionItemWithValue(
|
||||
generalGetString(R.string.network_session_mode_transport_isolation),
|
||||
sessionMode,
|
||||
values,
|
||||
icon = Icons.Outlined.SafetyDivider,
|
||||
onSelected = showModal {
|
||||
Column(
|
||||
Modifier.fillMaxWidth(),
|
||||
horizontalAlignment = Alignment.Start,
|
||||
) {
|
||||
AppBarTitle(stringResource(R.string.network_session_mode_transport_isolation))
|
||||
SectionViewSelectable(null, sessionMode, values, updateSessionMode)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private fun updateNetworkSettingsDialog(
|
||||
title: String,
|
||||
startsWith: String = "",
|
||||
message: String = generalGetString(R.string.updating_settings_will_reconnect_client_to_all_servers),
|
||||
onDismiss: () -> Unit,
|
||||
onConfirm: () -> Unit
|
||||
) {
|
||||
AlertManager.shared.showAlertDialog(
|
||||
title = title,
|
||||
text = startsWith + "\n\n" + message,
|
||||
confirmText = generalGetString(R.string.update_network_settings_confirmation),
|
||||
onDismiss = onDismiss,
|
||||
onConfirm = onConfirm,
|
||||
onDismissRequest = onDismiss
|
||||
)
|
||||
}
|
||||
|
||||
@Preview(showBackground = true)
|
||||
@Composable
|
||||
fun PreviewNetworkAndServersLayout() {
|
||||
SimpleXTheme {
|
||||
NetworkAndServersLayout(
|
||||
developerTools = true,
|
||||
networkUseSocksProxy = remember { mutableStateOf(true) },
|
||||
showModal = { {} },
|
||||
showSettingsModal = { {} },
|
||||
toggleSocksProxy = {},
|
||||
onionHosts = remember { mutableStateOf(OnionHosts.PREFER) },
|
||||
sessionMode = remember { mutableStateOf(TransportSessionMode.User) },
|
||||
useOnion = {},
|
||||
updateSessionMode = {},
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,214 +0,0 @@
|
||||
package chat.simplex.app.views.usersettings
|
||||
|
||||
import SectionDivider
|
||||
import SectionItemViewSpaceBetween
|
||||
import SectionView
|
||||
import SectionViewSelectable
|
||||
import android.os.Build
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.capitalize
|
||||
import androidx.compose.ui.text.intl.Locale
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import chat.simplex.app.*
|
||||
import chat.simplex.app.R
|
||||
import chat.simplex.app.model.ChatModel
|
||||
import chat.simplex.app.ui.theme.*
|
||||
import chat.simplex.app.views.helpers.*
|
||||
import kotlinx.coroutines.*
|
||||
import kotlin.collections.ArrayList
|
||||
|
||||
enum class NotificationsMode(private val requiresIgnoringBatterySinceSdk: Int) {
|
||||
OFF(Int.MAX_VALUE), PERIODIC(Build.VERSION_CODES.M), SERVICE(Build.VERSION_CODES.S), /*INSTANT(Int.MAX_VALUE) - for Firebase notifications */;
|
||||
|
||||
val requiresIgnoringBattery
|
||||
get() = requiresIgnoringBatterySinceSdk <= Build.VERSION.SDK_INT
|
||||
|
||||
companion object {
|
||||
val default: NotificationsMode = SERVICE
|
||||
}
|
||||
}
|
||||
|
||||
enum class NotificationPreviewMode {
|
||||
MESSAGE, CONTACT, HIDDEN;
|
||||
|
||||
companion object {
|
||||
val default: NotificationPreviewMode = MESSAGE
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun NotificationsSettingsView(
|
||||
chatModel: ChatModel,
|
||||
) {
|
||||
val onNotificationPreviewModeSelected = { mode: NotificationPreviewMode ->
|
||||
chatModel.controller.appPrefs.notificationPreviewMode.set(mode.name)
|
||||
chatModel.notificationPreviewMode.value = mode
|
||||
}
|
||||
|
||||
NotificationsSettingsLayout(
|
||||
notificationsMode = chatModel.notificationsMode,
|
||||
notificationPreviewMode = chatModel.notificationPreviewMode,
|
||||
showPage = { page ->
|
||||
ModalManager.shared.showModalCloseable(true) {
|
||||
when (page) {
|
||||
CurrentPage.NOTIFICATIONS_MODE -> NotificationsModeView(chatModel.notificationsMode) { changeNotificationsMode(it, chatModel) }
|
||||
CurrentPage.NOTIFICATION_PREVIEW_MODE -> NotificationPreviewView(chatModel.notificationPreviewMode, onNotificationPreviewModeSelected)
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
enum class CurrentPage {
|
||||
NOTIFICATIONS_MODE, NOTIFICATION_PREVIEW_MODE
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun NotificationsSettingsLayout(
|
||||
notificationsMode: State<NotificationsMode>,
|
||||
notificationPreviewMode: State<NotificationPreviewMode>,
|
||||
showPage: (CurrentPage) -> Unit,
|
||||
) {
|
||||
val modes = remember { notificationModes() }
|
||||
val previewModes = remember { notificationPreviewModes() }
|
||||
|
||||
Column(
|
||||
Modifier.fillMaxWidth(),
|
||||
horizontalAlignment = Alignment.Start,
|
||||
) {
|
||||
AppBarTitle(stringResource(R.string.notifications))
|
||||
SectionView(null) {
|
||||
SectionItemViewSpaceBetween({ showPage(CurrentPage.NOTIFICATIONS_MODE) }) {
|
||||
Text(stringResource(R.string.settings_notifications_mode_title))
|
||||
Spacer(Modifier.padding(horizontal = 10.dp))
|
||||
Text(
|
||||
modes.first { it.value == notificationsMode.value }.title,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
color = HighOrLowlight
|
||||
)
|
||||
}
|
||||
SectionDivider()
|
||||
SectionItemViewSpaceBetween({ showPage(CurrentPage.NOTIFICATION_PREVIEW_MODE) }) {
|
||||
Text(stringResource(R.string.settings_notification_preview_mode_title))
|
||||
Spacer(Modifier.padding(horizontal = 10.dp))
|
||||
Text(
|
||||
previewModes.first { it.value == notificationPreviewMode.value }.title,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
color = HighOrLowlight
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun NotificationsModeView(
|
||||
notificationsMode: State<NotificationsMode>,
|
||||
onNotificationsModeSelected: (NotificationsMode) -> Unit,
|
||||
) {
|
||||
val modes = remember { notificationModes() }
|
||||
Column(
|
||||
Modifier.fillMaxWidth(),
|
||||
horizontalAlignment = Alignment.Start,
|
||||
) {
|
||||
AppBarTitle(stringResource(R.string.settings_notifications_mode_title).lowercase().capitalize(Locale.current))
|
||||
SectionViewSelectable(null, notificationsMode, modes, onNotificationsModeSelected)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun NotificationPreviewView(
|
||||
notificationPreviewMode: State<NotificationPreviewMode>,
|
||||
onNotificationPreviewModeSelected: (NotificationPreviewMode) -> Unit,
|
||||
) {
|
||||
val previewModes = remember { notificationPreviewModes() }
|
||||
Column(
|
||||
Modifier.fillMaxWidth(),
|
||||
horizontalAlignment = Alignment.Start,
|
||||
) {
|
||||
AppBarTitle(stringResource(R.string.settings_notification_preview_title))
|
||||
SectionViewSelectable(null, notificationPreviewMode, previewModes, onNotificationPreviewModeSelected)
|
||||
}
|
||||
}
|
||||
|
||||
// mode, name, description
|
||||
private fun notificationModes(): List<ValueTitleDesc<NotificationsMode>> {
|
||||
val res = ArrayList<ValueTitleDesc<NotificationsMode>>()
|
||||
res.add(
|
||||
ValueTitleDesc(
|
||||
NotificationsMode.OFF,
|
||||
generalGetString(R.string.notifications_mode_off),
|
||||
generalGetString(R.string.notifications_mode_off_desc),
|
||||
)
|
||||
)
|
||||
res.add(
|
||||
ValueTitleDesc(
|
||||
NotificationsMode.PERIODIC,
|
||||
generalGetString(R.string.notifications_mode_periodic),
|
||||
generalGetString(R.string.notifications_mode_periodic_desc),
|
||||
)
|
||||
)
|
||||
res.add(
|
||||
ValueTitleDesc(
|
||||
NotificationsMode.SERVICE,
|
||||
generalGetString(R.string.notifications_mode_service),
|
||||
generalGetString(R.string.notifications_mode_service_desc),
|
||||
)
|
||||
)
|
||||
return res
|
||||
}
|
||||
|
||||
// preview mode, name, description
|
||||
fun notificationPreviewModes(): List<ValueTitleDesc<NotificationPreviewMode>> {
|
||||
val res = ArrayList<ValueTitleDesc<NotificationPreviewMode>>()
|
||||
res.add(
|
||||
ValueTitleDesc(
|
||||
NotificationPreviewMode.MESSAGE,
|
||||
generalGetString(R.string.notification_preview_mode_message),
|
||||
generalGetString(R.string.notification_preview_mode_message_desc),
|
||||
)
|
||||
)
|
||||
res.add(
|
||||
ValueTitleDesc(
|
||||
NotificationPreviewMode.CONTACT,
|
||||
generalGetString(R.string.notification_preview_mode_contact),
|
||||
generalGetString(R.string.notification_preview_mode_contact_desc),
|
||||
)
|
||||
)
|
||||
res.add(
|
||||
ValueTitleDesc(
|
||||
NotificationPreviewMode.HIDDEN,
|
||||
generalGetString(R.string.notification_preview_mode_hidden),
|
||||
generalGetString(R.string.notification_display_mode_hidden_desc),
|
||||
)
|
||||
)
|
||||
return res
|
||||
}
|
||||
|
||||
fun changeNotificationsMode(mode: NotificationsMode, chatModel: ChatModel) {
|
||||
chatModel.controller.appPrefs.notificationsMode.set(mode.name)
|
||||
if (mode.requiresIgnoringBattery && !chatModel.controller.isIgnoringBatteryOptimizations(chatModel.controller.appContext)) {
|
||||
chatModel.controller.appPrefs.backgroundServiceNoticeShown.set(false)
|
||||
}
|
||||
chatModel.notificationsMode.value = mode
|
||||
SimplexService.StartReceiver.toggleReceiver(mode == NotificationsMode.SERVICE)
|
||||
CoroutineScope(Dispatchers.Default).launch {
|
||||
if (mode == NotificationsMode.SERVICE)
|
||||
SimplexService.start(SimplexApp.context)
|
||||
else
|
||||
SimplexService.safeStopService(SimplexApp.context)
|
||||
}
|
||||
|
||||
if (mode != NotificationsMode.PERIODIC) {
|
||||
MessagesFetcherWorker.cancelAll()
|
||||
}
|
||||
chatModel.controller.showBackgroundServiceNoticeIfNeeded()
|
||||
}
|
||||
@@ -1,86 +0,0 @@
|
||||
package chat.simplex.app.views.usersettings
|
||||
|
||||
import SectionDivider
|
||||
import SectionItemView
|
||||
import SectionSpacer
|
||||
import SectionTextFooter
|
||||
import SectionView
|
||||
import android.view.WindowManager
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalView
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import chat.simplex.app.R
|
||||
import chat.simplex.app.model.*
|
||||
import chat.simplex.app.views.helpers.*
|
||||
|
||||
@Composable
|
||||
fun PrivacySettingsView(
|
||||
chatModel: ChatModel,
|
||||
setPerformLA: (Boolean) -> Unit
|
||||
) {
|
||||
Column(
|
||||
Modifier.fillMaxWidth(),
|
||||
horizontalAlignment = Alignment.Start
|
||||
) {
|
||||
val simplexLinkMode = chatModel.controller.appPrefs.simplexLinkMode
|
||||
AppBarTitle(stringResource(R.string.your_privacy))
|
||||
SectionView(stringResource(R.string.settings_section_title_device)) {
|
||||
ChatLockItem(chatModel.performLA, setPerformLA)
|
||||
SectionDivider()
|
||||
val context = LocalContext.current
|
||||
SettingsPreferenceItem(Icons.Outlined.VisibilityOff, stringResource(R.string.protect_app_screen), chatModel.controller.appPrefs.privacyProtectScreen) { on ->
|
||||
if (on) {
|
||||
(context as? FragmentActivity)?.window?.setFlags(
|
||||
WindowManager.LayoutParams.FLAG_SECURE,
|
||||
WindowManager.LayoutParams.FLAG_SECURE
|
||||
)
|
||||
} else {
|
||||
(context as? FragmentActivity)?.window?.clearFlags(WindowManager.LayoutParams.FLAG_SECURE)
|
||||
}
|
||||
}
|
||||
}
|
||||
SectionSpacer()
|
||||
|
||||
SectionView(stringResource(R.string.settings_section_title_chats)) {
|
||||
SettingsPreferenceItem(Icons.Outlined.Image, stringResource(R.string.auto_accept_images), chatModel.controller.appPrefs.privacyAcceptImages)
|
||||
SectionDivider()
|
||||
SettingsPreferenceItem(Icons.Outlined.TravelExplore, stringResource(R.string.send_link_previews), chatModel.controller.appPrefs.privacyLinkPreviews)
|
||||
SectionDivider()
|
||||
SectionItemView { SimpleXLinkOptions(chatModel.simplexLinkMode, onSelected = {
|
||||
simplexLinkMode.set(it)
|
||||
chatModel.simplexLinkMode.value = it
|
||||
}) }
|
||||
}
|
||||
if (chatModel.simplexLinkMode.value == SimplexLinkMode.BROWSER) {
|
||||
SectionTextFooter(stringResource(R.string.simplex_link_mode_browser_warning))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SimpleXLinkOptions(simplexLinkModeState: State<SimplexLinkMode>, onSelected: (SimplexLinkMode) -> Unit) {
|
||||
val values = remember {
|
||||
SimplexLinkMode.values().map {
|
||||
when (it) {
|
||||
SimplexLinkMode.DESCRIPTION -> it to generalGetString(R.string.simplex_link_mode_description)
|
||||
SimplexLinkMode.FULL -> it to generalGetString(R.string.simplex_link_mode_full)
|
||||
SimplexLinkMode.BROWSER -> it to generalGetString(R.string.simplex_link_mode_browser)
|
||||
}
|
||||
}
|
||||
}
|
||||
ExposedDropDownSettingRow(
|
||||
generalGetString(R.string.simplex_link_mode),
|
||||
values,
|
||||
simplexLinkModeState,
|
||||
icon = null,
|
||||
enabled = remember { mutableStateOf(true) },
|
||||
onSelected = onSelected
|
||||
)
|
||||
}
|
||||
@@ -1,335 +0,0 @@
|
||||
package chat.simplex.app.views.usersettings
|
||||
|
||||
import SectionDivider
|
||||
import SectionItemView
|
||||
import SectionSpacer
|
||||
import SectionTextFooter
|
||||
import SectionView
|
||||
import androidx.compose.foundation.*
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalUriHandler
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.*
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import chat.simplex.app.R
|
||||
import chat.simplex.app.model.*
|
||||
import chat.simplex.app.model.ServerAddress.Companion.parseServerAddress
|
||||
import chat.simplex.app.ui.theme.*
|
||||
import chat.simplex.app.views.helpers.*
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@Composable
|
||||
fun SMPServersView(m: ChatModel) {
|
||||
var servers by remember {
|
||||
mutableStateOf(m.userSMPServersUnsaved.value ?: m.userSMPServers.value ?: emptyList())
|
||||
}
|
||||
val testing = rememberSaveable { mutableStateOf(false) }
|
||||
val serversUnchanged = remember { derivedStateOf { servers == m.userSMPServers.value || testing.value } }
|
||||
val allServersDisabled = remember { derivedStateOf { servers.all { !it.enabled } } }
|
||||
val saveDisabled = remember {
|
||||
derivedStateOf {
|
||||
servers.isEmpty() ||
|
||||
servers == m.userSMPServers.value ||
|
||||
testing.value ||
|
||||
!servers.all { srv ->
|
||||
val address = parseServerAddress(srv.server)
|
||||
address != null && uniqueAddress(srv, address, servers)
|
||||
} ||
|
||||
allServersDisabled.value
|
||||
}
|
||||
}
|
||||
|
||||
fun showServer(server: ServerCfg) {
|
||||
ModalManager.shared.showModalCloseable(true) { close ->
|
||||
var old by remember { mutableStateOf(server) }
|
||||
val index = servers.indexOf(old)
|
||||
SMPServerView(
|
||||
m,
|
||||
old,
|
||||
onUpdate = { updated ->
|
||||
val newServers = ArrayList(servers)
|
||||
newServers.removeAt(index)
|
||||
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()
|
||||
})
|
||||
}
|
||||
}
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
SMPServersLayout(
|
||||
testing = testing.value,
|
||||
servers = servers,
|
||||
serversUnchanged = serversUnchanged.value,
|
||||
saveDisabled = saveDisabled.value,
|
||||
allServersDisabled = allServersDisabled.value,
|
||||
m.currentUser.value,
|
||||
addServer = {
|
||||
AlertManager.shared.showAlertDialogButtonsColumn(
|
||||
title = generalGetString(R.string.smp_servers_add),
|
||||
buttons = {
|
||||
Column {
|
||||
SectionItemView({
|
||||
AlertManager.shared.hideAlert()
|
||||
servers = servers + ServerCfg.empty
|
||||
// No saving until something will be changed on the next screen to prevent blank servers on the list
|
||||
showServer(servers.last())
|
||||
}) {
|
||||
Text(stringResource(R.string.smp_servers_enter_manually))
|
||||
}
|
||||
SectionItemView({
|
||||
AlertManager.shared.hideAlert()
|
||||
ModalManager.shared.showModalCloseable { close ->
|
||||
ScanSMPServer {
|
||||
close()
|
||||
servers = servers + it
|
||||
m.userSMPServersUnsaved.value = servers
|
||||
}
|
||||
}
|
||||
}
|
||||
) {
|
||||
Text(stringResource(R.string.smp_servers_scan_qr))
|
||||
}
|
||||
val hasAllPresets = hasAllPresets(servers, m)
|
||||
if (!hasAllPresets) {
|
||||
SectionItemView({
|
||||
AlertManager.shared.hideAlert()
|
||||
servers = (servers + addAllPresets(servers, m)).sortedByDescending { it.preset }
|
||||
}) {
|
||||
Text(stringResource(R.string.smp_servers_preset_add), color = MaterialTheme.colors.onBackground)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
},
|
||||
testServers = {
|
||||
scope.launch {
|
||||
testServers(testing, servers, m) {
|
||||
servers = it
|
||||
m.userSMPServersUnsaved.value = servers
|
||||
}
|
||||
}
|
||||
},
|
||||
resetServers = {
|
||||
servers = m.userSMPServers.value ?: emptyList()
|
||||
m.userSMPServersUnsaved.value = null
|
||||
},
|
||||
saveSMPServers = {
|
||||
saveSMPServers(servers, m)
|
||||
},
|
||||
showServer = ::showServer,
|
||||
)
|
||||
|
||||
if (testing.value) {
|
||||
Box(
|
||||
Modifier.fillMaxSize(),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
CircularProgressIndicator(
|
||||
Modifier
|
||||
.padding(horizontal = 2.dp)
|
||||
.size(30.dp),
|
||||
color = HighOrLowlight,
|
||||
strokeWidth = 2.5.dp
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SMPServersLayout(
|
||||
testing: Boolean,
|
||||
servers: List<ServerCfg>,
|
||||
serversUnchanged: Boolean,
|
||||
saveDisabled: Boolean,
|
||||
allServersDisabled: Boolean,
|
||||
currentUser: User?,
|
||||
addServer: () -> Unit,
|
||||
testServers: () -> Unit,
|
||||
resetServers: () -> Unit,
|
||||
saveSMPServers: () -> Unit,
|
||||
showServer: (ServerCfg) -> Unit,
|
||||
) {
|
||||
Column(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.verticalScroll(rememberScrollState())
|
||||
.padding(bottom = DEFAULT_PADDING),
|
||||
) {
|
||||
AppBarTitle(stringResource(R.string.your_SMP_servers))
|
||||
|
||||
SectionView(stringResource(R.string.smp_servers).uppercase()) {
|
||||
for (srv in servers) {
|
||||
SectionItemView({ showServer(srv) }, disabled = testing) {
|
||||
SmpServerView(srv, servers, testing)
|
||||
}
|
||||
SectionDivider()
|
||||
}
|
||||
SettingsActionItem(
|
||||
Icons.Outlined.Add,
|
||||
stringResource(R.string.smp_servers_add),
|
||||
addServer,
|
||||
disabled = testing,
|
||||
textColor = if (testing) HighOrLowlight else MaterialTheme.colors.primary,
|
||||
iconColor = if (testing) HighOrLowlight else MaterialTheme.colors.primary
|
||||
)
|
||||
}
|
||||
SectionTextFooter(
|
||||
remember(currentUser?.displayName) {
|
||||
buildAnnotatedString {
|
||||
append(generalGetString(R.string.smp_servers_per_user) + " ")
|
||||
withStyle(SpanStyle(fontWeight = FontWeight.Bold)) {
|
||||
append(currentUser?.displayName ?: "")
|
||||
}
|
||||
append(".")
|
||||
}
|
||||
}
|
||||
)
|
||||
SectionSpacer()
|
||||
SectionView {
|
||||
SectionItemView(resetServers, disabled = serversUnchanged) {
|
||||
Text(stringResource(R.string.reset_verb), color = if (!serversUnchanged) MaterialTheme.colors.onBackground else HighOrLowlight)
|
||||
}
|
||||
SectionDivider()
|
||||
val testServersDisabled = testing || allServersDisabled
|
||||
SectionItemView(testServers, disabled = testServersDisabled) {
|
||||
Text(stringResource(R.string.smp_servers_test_servers), color = if (!testServersDisabled) MaterialTheme.colors.onBackground else HighOrLowlight)
|
||||
}
|
||||
SectionDivider()
|
||||
SectionItemView(saveSMPServers, disabled = saveDisabled) {
|
||||
Text(stringResource(R.string.smp_servers_save), color = if (!saveDisabled) MaterialTheme.colors.onBackground else HighOrLowlight)
|
||||
}
|
||||
}
|
||||
SectionSpacer()
|
||||
SectionView {
|
||||
HowToButton()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SmpServerView(srv: ServerCfg, servers: List<ServerCfg>, disabled: Boolean) {
|
||||
val address = parseServerAddress(srv.server)
|
||||
when {
|
||||
address == null || !address.valid || !uniqueAddress(srv, address, servers) -> InvalidServer()
|
||||
!srv.enabled -> Icon(Icons.Outlined.DoNotDisturb, null, tint = HighOrLowlight)
|
||||
else -> ShowTestStatus(srv)
|
||||
}
|
||||
Spacer(Modifier.padding(horizontal = 4.dp))
|
||||
val text = address?.hostnames?.firstOrNull() ?: srv.server
|
||||
if (srv.enabled) {
|
||||
Text(text, color = if (disabled) HighOrLowlight else MaterialTheme.colors.onBackground, maxLines = 1)
|
||||
} else {
|
||||
Text(text, maxLines = 1, color = HighOrLowlight)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun HowToButton() {
|
||||
val uriHandler = LocalUriHandler.current
|
||||
SettingsActionItem(
|
||||
Icons.Outlined.OpenInNew,
|
||||
stringResource(R.string.how_to_use_your_servers),
|
||||
{ uriHandler.openUri("https://github.com/simplex-chat/simplex-chat/blob/stable/docs/SERVER.md") },
|
||||
textColor = MaterialTheme.colors.primary,
|
||||
iconColor = MaterialTheme.colors.primary
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun InvalidServer() {
|
||||
Icon(Icons.Outlined.ErrorOutline, null, tint = MaterialTheme.colors.error)
|
||||
}
|
||||
|
||||
private fun uniqueAddress(s: ServerCfg, address: ServerAddress, servers: List<ServerCfg>): Boolean = servers.all { srv ->
|
||||
address.hostnames.all { host ->
|
||||
srv.id == s.id || !srv.server.contains(host)
|
||||
}
|
||||
}
|
||||
|
||||
private fun hasAllPresets(servers: List<ServerCfg>, m: ChatModel): Boolean =
|
||||
m.presetSMPServers.value?.all { hasPreset(it, servers) } ?: true
|
||||
|
||||
private fun addAllPresets(servers: List<ServerCfg>, m: ChatModel): List<ServerCfg> {
|
||||
val toAdd = ArrayList<ServerCfg>()
|
||||
for (srv in m.presetSMPServers.value ?: emptyList()) {
|
||||
if (!hasPreset(srv, servers)) {
|
||||
toAdd.add(ServerCfg(srv, preset = true, tested = null, enabled = true))
|
||||
}
|
||||
}
|
||||
return toAdd
|
||||
}
|
||||
|
||||
private fun hasPreset(srv: String, servers: List<ServerCfg>): Boolean =
|
||||
servers.any { it.server == srv }
|
||||
|
||||
private suspend fun testServers(testing: MutableState<Boolean>, servers: List<ServerCfg>, m: ChatModel, onUpdated: (List<ServerCfg>) -> Unit) {
|
||||
val resetStatus = resetTestStatus(servers)
|
||||
onUpdated(resetStatus)
|
||||
testing.value = true
|
||||
val fs = runServersTest(resetStatus, m) { onUpdated(it) }
|
||||
testing.value = false
|
||||
if (fs.isNotEmpty()) {
|
||||
val msg = fs.map { it.key + ": " + it.value.localizedDescription }.joinToString("\n")
|
||||
AlertManager.shared.showAlertMsg(
|
||||
title = generalGetString(R.string.smp_servers_test_failed),
|
||||
text = generalGetString(R.string.smp_servers_test_some_failed) + "\n" + msg
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun resetTestStatus(servers: List<ServerCfg>): List<ServerCfg> {
|
||||
val copy = ArrayList(servers)
|
||||
for ((index, server) in servers.withIndex()) {
|
||||
if (server.enabled) {
|
||||
copy.removeAt(index)
|
||||
copy.add(index, server.copy(tested = null))
|
||||
}
|
||||
}
|
||||
return copy
|
||||
}
|
||||
|
||||
private suspend fun runServersTest(servers: List<ServerCfg>, m: ChatModel, onUpdated: (List<ServerCfg>) -> Unit): Map<String, SMPTestFailure> {
|
||||
val fs: MutableMap<String, SMPTestFailure> = mutableMapOf()
|
||||
val updatedServers = ArrayList<ServerCfg>(servers)
|
||||
for ((index, server) in servers.withIndex()) {
|
||||
if (server.enabled) {
|
||||
val (updatedServer, f) = testServerConnection(server, m)
|
||||
updatedServers.removeAt(index)
|
||||
updatedServers.add(index, updatedServer)
|
||||
// toList() is important. Otherwise, Compose will not redraw the screen after first update
|
||||
onUpdated(updatedServers.toList())
|
||||
if (f != null) {
|
||||
fs[serverHostname(updatedServer.server)] = f
|
||||
}
|
||||
}
|
||||
}
|
||||
return fs
|
||||
}
|
||||
|
||||
private fun saveSMPServers(servers: List<ServerCfg>, m: ChatModel) {
|
||||
withApi {
|
||||
if (m.controller.setUserSMPServers(servers)) {
|
||||
m.userSMPServers.value = servers
|
||||
m.userSMPServersUnsaved.value = null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,55 +0,0 @@
|
||||
package chat.simplex.app.views.usersettings
|
||||
|
||||
import android.Manifest
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import chat.simplex.app.R
|
||||
import chat.simplex.app.model.ServerAddress.Companion.parseServerAddress
|
||||
import chat.simplex.app.model.ServerCfg
|
||||
import chat.simplex.app.ui.theme.DEFAULT_PADDING
|
||||
import chat.simplex.app.views.helpers.*
|
||||
import chat.simplex.app.views.newchat.QRCodeScanner
|
||||
import com.google.accompanist.permissions.rememberPermissionState
|
||||
|
||||
@Composable
|
||||
fun ScanSMPServer(onNext: (ServerCfg) -> Unit) {
|
||||
val cameraPermissionState = rememberPermissionState(permission = Manifest.permission.CAMERA)
|
||||
LaunchedEffect(Unit) {
|
||||
cameraPermissionState.launchPermissionRequest()
|
||||
}
|
||||
ScanSMPServerLayout(onNext)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ScanSMPServerLayout(onNext: (ServerCfg) -> Unit) {
|
||||
Column(
|
||||
Modifier
|
||||
.fillMaxSize()
|
||||
.padding(horizontal = DEFAULT_PADDING)
|
||||
) {
|
||||
AppBarTitle(stringResource(R.string.smp_servers_scan_qr), false)
|
||||
Box(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.aspectRatio(ratio = 1F)
|
||||
.padding(bottom = 12.dp)
|
||||
) {
|
||||
QRCodeScanner { text ->
|
||||
val res = parseServerAddress(text)
|
||||
if (res != null) {
|
||||
onNext(ServerCfg(text, false, null, true))
|
||||
} else {
|
||||
AlertManager.shared.showAlertMsg(
|
||||
title = generalGetString(R.string.smp_servers_invalid_address),
|
||||
text = generalGetString(R.string.smp_servers_check_address)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,539 +0,0 @@
|
||||
package chat.simplex.app.views.usersettings
|
||||
|
||||
import SectionDivider
|
||||
import SectionItemView
|
||||
import SectionSpacer
|
||||
import SectionView
|
||||
import android.content.Context
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.foundation.*
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.*
|
||||
import androidx.compose.material.icons.outlined.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.platform.*
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.*
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import chat.simplex.app.*
|
||||
import chat.simplex.app.R
|
||||
import chat.simplex.app.model.*
|
||||
import chat.simplex.app.ui.theme.*
|
||||
import chat.simplex.app.views.TerminalView
|
||||
import chat.simplex.app.views.database.DatabaseView
|
||||
import chat.simplex.app.views.helpers.*
|
||||
import chat.simplex.app.views.newchat.CreateLinkTab
|
||||
import chat.simplex.app.views.newchat.CreateLinkView
|
||||
import chat.simplex.app.views.onboarding.SimpleXInfo
|
||||
import chat.simplex.app.views.onboarding.WhatsNewView
|
||||
|
||||
@Composable
|
||||
fun SettingsView(chatModel: ChatModel, setPerformLA: (Boolean) -> Unit) {
|
||||
val user = chatModel.currentUser.value
|
||||
val stopped = chatModel.chatRunning.value == false
|
||||
|
||||
MaintainIncognitoState(chatModel)
|
||||
|
||||
if (user != null) {
|
||||
val requireAuth = remember { chatModel.controller.appPrefs.performLA.state }
|
||||
val context = LocalContext.current
|
||||
SettingsLayout(
|
||||
profile = user.profile,
|
||||
stopped,
|
||||
chatModel.chatDbEncrypted.value == true,
|
||||
chatModel.incognito,
|
||||
chatModel.controller.appPrefs.incognito,
|
||||
developerTools = chatModel.controller.appPrefs.developerTools,
|
||||
user.displayName,
|
||||
setPerformLA = setPerformLA,
|
||||
showModal = { modalView -> { ModalManager.shared.showModal { modalView(chatModel) } } },
|
||||
showSettingsModal = { modalView -> { ModalManager.shared.showModal(true) { modalView(chatModel) } } },
|
||||
showCustomModal = { modalView -> { ModalManager.shared.showCustomModal { close -> modalView(chatModel, close) } } },
|
||||
showVersion = {
|
||||
withApi {
|
||||
val info = chatModel.controller.apiGetVersion()
|
||||
if (info != null) {
|
||||
ModalManager.shared.showModal { VersionInfoView(info) }
|
||||
}
|
||||
}
|
||||
},
|
||||
withAuth = { block ->
|
||||
if (!requireAuth.value) {
|
||||
block()
|
||||
} else {
|
||||
ModalManager.shared.showModalCloseable { close ->
|
||||
val onFinishAuth = { success: Boolean ->
|
||||
if (success) {
|
||||
close()
|
||||
block()
|
||||
}
|
||||
}
|
||||
LaunchedEffect(Unit) {
|
||||
runAuth(context, onFinishAuth)
|
||||
}
|
||||
Box(
|
||||
Modifier.fillMaxSize(),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
SimpleButton(
|
||||
stringResource(R.string.auth_unlock),
|
||||
icon = Icons.Outlined.Lock,
|
||||
click = {
|
||||
runAuth(context, onFinishAuth)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
val simplexTeamUri =
|
||||
"simplex:/contact#/?v=1&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2FK1rslx-m5bpXVIdMZg9NLUZ_8JBm8xTt%23MCowBQYDK2VuAyEALDeVe-sG8mRY22LsXlPgiwTNs9dbiLrNuA7f3ZMAJ2w%3D"
|
||||
|
||||
@Composable
|
||||
fun SettingsLayout(
|
||||
profile: LocalProfile,
|
||||
stopped: Boolean,
|
||||
encrypted: Boolean,
|
||||
incognito: MutableState<Boolean>,
|
||||
incognitoPref: SharedPreference<Boolean>,
|
||||
developerTools: SharedPreference<Boolean>,
|
||||
userDisplayName: String,
|
||||
setPerformLA: (Boolean) -> Unit,
|
||||
showModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit),
|
||||
showSettingsModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit),
|
||||
showCustomModal: (@Composable (ChatModel, () -> Unit) -> Unit) -> (() -> Unit),
|
||||
showVersion: () -> Unit,
|
||||
withAuth: (block: () -> Unit) -> Unit
|
||||
) {
|
||||
val uriHandler = LocalUriHandler.current
|
||||
Surface(Modifier.fillMaxSize().verticalScroll(rememberScrollState())) {
|
||||
Column(
|
||||
Modifier
|
||||
.fillMaxSize()
|
||||
.background(if (isInDarkTheme()) MaterialTheme.colors.background else SettingsBackgroundLight)
|
||||
.padding(top = DEFAULT_PADDING)
|
||||
) {
|
||||
Text(
|
||||
stringResource(R.string.your_settings),
|
||||
style = MaterialTheme.typography.h1,
|
||||
modifier = Modifier.padding(horizontal = DEFAULT_PADDING),
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
|
||||
Spacer(Modifier.height(30.dp))
|
||||
|
||||
SectionView(stringResource(R.string.settings_section_title_you)) {
|
||||
SectionItemView(showCustomModal { chatModel, close -> UserProfileView(chatModel, close) }, 80.dp, disabled = stopped) {
|
||||
ProfilePreview(profile, stopped = stopped)
|
||||
}
|
||||
SectionDivider()
|
||||
SettingsActionItem(Icons.Outlined.ManageAccounts, stringResource(R.string.your_chat_profiles), { withAuth { showSettingsModal { UserProfilesView(it) }() } }, disabled = stopped)
|
||||
SectionDivider()
|
||||
SettingsIncognitoActionItem(incognitoPref, incognito, stopped) { showModal { IncognitoView() }() }
|
||||
SectionDivider()
|
||||
SettingsActionItem(Icons.Outlined.QrCode, stringResource(R.string.your_simplex_contact_address), showModal { CreateLinkView(it, CreateLinkTab.LONG_TERM) }, disabled = stopped)
|
||||
SectionDivider()
|
||||
ChatPreferencesItem(showCustomModal, stopped = stopped)
|
||||
}
|
||||
SectionSpacer()
|
||||
|
||||
SectionView(stringResource(R.string.settings_section_title_settings)) {
|
||||
SettingsActionItem(Icons.Outlined.Bolt, stringResource(R.string.notifications), showSettingsModal { NotificationsSettingsView(it) }, disabled = stopped)
|
||||
SectionDivider()
|
||||
SettingsActionItem(Icons.Outlined.WifiTethering, stringResource(R.string.network_and_servers), showSettingsModal { NetworkAndServersView(it, showModal, showSettingsModal) }, disabled = stopped)
|
||||
SectionDivider()
|
||||
SettingsActionItem(Icons.Outlined.Videocam, stringResource(R.string.settings_audio_video_calls), showSettingsModal { CallSettingsView(it, showModal) }, disabled = stopped)
|
||||
SectionDivider()
|
||||
SettingsActionItem(Icons.Outlined.Lock, stringResource(R.string.privacy_and_security), showSettingsModal { PrivacySettingsView(it, setPerformLA) }, disabled = stopped)
|
||||
SectionDivider()
|
||||
SettingsActionItem(Icons.Outlined.LightMode, stringResource(R.string.appearance_settings), showSettingsModal { AppearanceView() }, disabled = stopped)
|
||||
SectionDivider()
|
||||
DatabaseItem(encrypted, showSettingsModal { DatabaseView(it, showSettingsModal) }, stopped)
|
||||
}
|
||||
SectionSpacer()
|
||||
|
||||
SectionView(stringResource(R.string.settings_section_title_help)) {
|
||||
SettingsActionItem(Icons.Outlined.HelpOutline, stringResource(R.string.how_to_use_simplex_chat), showModal { HelpView(userDisplayName) }, disabled = stopped)
|
||||
SectionDivider()
|
||||
SettingsActionItem(Icons.Outlined.Add, stringResource(R.string.whats_new), showCustomModal { _, close -> WhatsNewView(viaSettings = true, close) }, disabled = stopped)
|
||||
SectionDivider()
|
||||
SettingsActionItem(Icons.Outlined.Info, stringResource(R.string.about_simplex_chat), showModal { SimpleXInfo(it, onboarding = false) })
|
||||
SectionDivider()
|
||||
SettingsActionItem(Icons.Outlined.Tag, stringResource(R.string.chat_with_the_founder), { uriHandler.openUri(simplexTeamUri) }, textColor = MaterialTheme.colors.primary, disabled = stopped)
|
||||
SectionDivider()
|
||||
SettingsActionItem(Icons.Outlined.Email, stringResource(R.string.send_us_an_email), { uriHandler.openUri("mailto:chat@simplex.chat") }, textColor = MaterialTheme.colors.primary)
|
||||
}
|
||||
SectionSpacer()
|
||||
|
||||
SectionView(stringResource(R.string.settings_section_title_support)) {
|
||||
ContributeItem(uriHandler)
|
||||
SectionDivider()
|
||||
RateAppItem(uriHandler)
|
||||
SectionDivider()
|
||||
StarOnGithubItem(uriHandler)
|
||||
}
|
||||
SectionSpacer()
|
||||
|
||||
SectionView(stringResource(R.string.settings_section_title_develop)) {
|
||||
val devTools = remember { mutableStateOf(developerTools.get()) }
|
||||
SettingsPreferenceItem(Icons.Outlined.Construction, stringResource(R.string.settings_developer_tools), developerTools, devTools)
|
||||
SectionDivider()
|
||||
if (devTools.value) {
|
||||
ChatConsoleItem { withAuth(showCustomModal { it, close -> TerminalView(it, close) }) }
|
||||
SectionDivider()
|
||||
InstallTerminalAppItem(uriHandler)
|
||||
SectionDivider()
|
||||
}
|
||||
// SettingsActionItem(Icons.Outlined.Science, stringResource(R.string.settings_experimental_features), showSettingsModal { ExperimentalFeaturesView(it, enableCalls) })
|
||||
// SectionDivider()
|
||||
AppVersionItem(showVersion)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SettingsIncognitoActionItem(
|
||||
incognitoPref: SharedPreference<Boolean>,
|
||||
incognito: MutableState<Boolean>,
|
||||
stopped: Boolean,
|
||||
onClickInfo: () -> Unit,
|
||||
) {
|
||||
SettingsPreferenceItemWithInfo(
|
||||
if (incognito.value) Icons.Filled.TheaterComedy else Icons.Outlined.TheaterComedy,
|
||||
if (incognito.value) Indigo else HighOrLowlight,
|
||||
stringResource(R.string.incognito),
|
||||
stopped,
|
||||
onClickInfo,
|
||||
incognitoPref,
|
||||
incognito
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun MaintainIncognitoState(chatModel: ChatModel) {
|
||||
// Cache previous value and once it changes in background, update it via API
|
||||
var cachedIncognito by remember { mutableStateOf(chatModel.incognito.value) }
|
||||
LaunchedEffect(chatModel.incognito.value) {
|
||||
// Don't do anything if nothing changed
|
||||
if (cachedIncognito == chatModel.incognito.value) return@LaunchedEffect
|
||||
try {
|
||||
chatModel.controller.apiSetIncognito(chatModel.incognito.value)
|
||||
} catch (e: Exception) {
|
||||
// Rollback the state
|
||||
chatModel.controller.appPrefs.incognito.set(cachedIncognito)
|
||||
// Crash the app
|
||||
throw e
|
||||
}
|
||||
cachedIncognito = chatModel.incognito.value
|
||||
}
|
||||
}
|
||||
|
||||
@Composable private fun DatabaseItem(encrypted: Boolean, openDatabaseView: () -> Unit, stopped: Boolean) {
|
||||
SectionItemView(openDatabaseView) {
|
||||
Row(
|
||||
Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
Row {
|
||||
Icon(
|
||||
Icons.Outlined.FolderOpen,
|
||||
contentDescription = stringResource(R.string.database_passphrase_and_export),
|
||||
tint = if (encrypted) HighOrLowlight else WarningOrange,
|
||||
)
|
||||
Spacer(Modifier.padding(horizontal = 4.dp))
|
||||
Text(stringResource(R.string.database_passphrase_and_export))
|
||||
}
|
||||
if (stopped) {
|
||||
Icon(
|
||||
Icons.Filled.Report,
|
||||
contentDescription = stringResource(R.string.chat_is_stopped),
|
||||
tint = Color.Red,
|
||||
modifier = Modifier.padding(end = 6.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable fun ChatPreferencesItem(showCustomModal: ((@Composable (ChatModel, () -> Unit) -> Unit) -> (() -> Unit)), stopped: Boolean) {
|
||||
SettingsActionItem(
|
||||
Icons.Outlined.ToggleOn,
|
||||
stringResource(R.string.chat_preferences),
|
||||
click = if (stopped) null else ({
|
||||
withApi {
|
||||
showCustomModal { m, close ->
|
||||
PreferencesView(m, m.currentUser.value ?: return@showCustomModal, close)
|
||||
}()
|
||||
}
|
||||
}),
|
||||
disabled = stopped
|
||||
)
|
||||
}
|
||||
|
||||
@Composable fun ChatLockItem(performLA: MutableState<Boolean>, setPerformLA: (Boolean) -> Unit) {
|
||||
SectionItemView() {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Icon(
|
||||
Icons.Outlined.Lock,
|
||||
contentDescription = stringResource(R.string.chat_lock),
|
||||
tint = HighOrLowlight,
|
||||
)
|
||||
Spacer(Modifier.padding(horizontal = 4.dp))
|
||||
Text(
|
||||
stringResource(R.string.chat_lock), Modifier
|
||||
.padding(end = 24.dp)
|
||||
.fillMaxWidth()
|
||||
.weight(1F)
|
||||
)
|
||||
Switch(
|
||||
checked = performLA.value,
|
||||
onCheckedChange = { setPerformLA(it) },
|
||||
colors = SwitchDefaults.colors(
|
||||
checkedThumbColor = MaterialTheme.colors.primary,
|
||||
uncheckedThumbColor = HighOrLowlight
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable private fun ContributeItem(uriHandler: UriHandler) {
|
||||
SectionItemView({ uriHandler.openUri("https://github.com/simplex-chat/simplex-chat#contribute") }) {
|
||||
Icon(
|
||||
Icons.Outlined.Keyboard,
|
||||
contentDescription = "GitHub",
|
||||
tint = HighOrLowlight,
|
||||
)
|
||||
Spacer(Modifier.padding(horizontal = 4.dp))
|
||||
Text(generalGetString(R.string.contribute), color = MaterialTheme.colors.primary)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable private fun RateAppItem(uriHandler: UriHandler) {
|
||||
SectionItemView({
|
||||
runCatching { uriHandler.openUri("market://details?id=chat.simplex.app") }
|
||||
.onFailure { uriHandler.openUri("https://play.google.com/store/apps/details?id=chat.simplex.app") }
|
||||
}
|
||||
) {
|
||||
Icon(
|
||||
Icons.Outlined.StarOutline,
|
||||
contentDescription = "Google Play",
|
||||
tint = HighOrLowlight,
|
||||
)
|
||||
Spacer(Modifier.padding(horizontal = 4.dp))
|
||||
Text(generalGetString(R.string.rate_the_app), color = MaterialTheme.colors.primary)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable private fun StarOnGithubItem(uriHandler: UriHandler) {
|
||||
SectionItemView({ uriHandler.openUri("https://github.com/simplex-chat/simplex-chat") }) {
|
||||
Icon(
|
||||
painter = painterResource(id = R.drawable.ic_github),
|
||||
contentDescription = "GitHub",
|
||||
tint = HighOrLowlight,
|
||||
)
|
||||
Spacer(Modifier.padding(horizontal = 4.dp))
|
||||
Text(generalGetString(R.string.star_on_github), color = MaterialTheme.colors.primary)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable private fun ChatConsoleItem(showTerminal: () -> Unit) {
|
||||
SectionItemView(showTerminal) {
|
||||
Icon(
|
||||
painter = painterResource(id = R.drawable.ic_outline_terminal),
|
||||
contentDescription = stringResource(R.string.chat_console),
|
||||
tint = HighOrLowlight,
|
||||
)
|
||||
Spacer(Modifier.padding(horizontal = 4.dp))
|
||||
Text(stringResource(R.string.chat_console))
|
||||
}
|
||||
}
|
||||
|
||||
@Composable private fun InstallTerminalAppItem(uriHandler: UriHandler) {
|
||||
SectionItemView({ uriHandler.openUri("https://github.com/simplex-chat/simplex-chat") }) {
|
||||
Icon(
|
||||
painter = painterResource(id = R.drawable.ic_github),
|
||||
contentDescription = "GitHub",
|
||||
tint = HighOrLowlight,
|
||||
)
|
||||
Spacer(Modifier.padding(horizontal = 4.dp))
|
||||
Text(generalGetString(R.string.install_simplex_chat_for_terminal), color = MaterialTheme.colors.primary)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable private fun AppVersionItem(showVersion: () -> Unit) {
|
||||
SectionItemView(showVersion) {
|
||||
Text("v${BuildConfig.VERSION_NAME} (${BuildConfig.VERSION_CODE})")
|
||||
}
|
||||
}
|
||||
|
||||
@Composable fun ProfilePreview(profileOf: NamedChat, size: Dp = 60.dp, color: Color = MaterialTheme.colors.secondary, stopped: Boolean = false) {
|
||||
ProfileImage(size = size, image = profileOf.image, color = color)
|
||||
Spacer(Modifier.padding(horizontal = 4.dp))
|
||||
Column {
|
||||
Text(
|
||||
profileOf.displayName,
|
||||
style = MaterialTheme.typography.caption,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = if (stopped) HighOrLowlight else Color.Unspecified,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
Text(
|
||||
profileOf.fullName,
|
||||
color = if (stopped) HighOrLowlight else Color.Unspecified,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SettingsActionItem(icon: ImageVector, text: String, click: (() -> Unit)? = null, textColor: Color = Color.Unspecified, iconColor: Color = HighOrLowlight, disabled: Boolean = false) {
|
||||
SectionItemView(click, disabled = disabled) {
|
||||
Icon(icon, text, tint = if (disabled) HighOrLowlight else iconColor)
|
||||
Spacer(Modifier.padding(horizontal = 4.dp))
|
||||
Text(text, color = if (disabled) HighOrLowlight else textColor)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SettingsPreferenceItem(
|
||||
icon: ImageVector,
|
||||
text: String,
|
||||
pref: SharedPreference<Boolean>,
|
||||
prefState: MutableState<Boolean>? = null,
|
||||
onChange: ((Boolean) -> Unit)? = null,
|
||||
) {
|
||||
SectionItemView {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Icon(icon, text, tint = HighOrLowlight)
|
||||
Spacer(Modifier.padding(horizontal = 4.dp))
|
||||
SharedPreferenceToggle(text, pref, prefState, onChange)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SettingsPreferenceItemWithInfo(
|
||||
icon: ImageVector,
|
||||
iconTint: Color,
|
||||
text: String,
|
||||
stopped: Boolean,
|
||||
onClickInfo: () -> Unit,
|
||||
pref: SharedPreference<Boolean>,
|
||||
prefState: MutableState<Boolean>? = null
|
||||
) {
|
||||
SectionItemView(if (stopped) null else onClickInfo) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Icon(icon, text, tint = if (stopped) HighOrLowlight else iconTint)
|
||||
Spacer(Modifier.padding(horizontal = 4.dp))
|
||||
SharedPreferenceToggleWithIcon(text, Icons.Outlined.Info, stopped, onClickInfo, pref, prefState)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun PreferenceToggle(
|
||||
text: String,
|
||||
checked: Boolean,
|
||||
onChange: (Boolean) -> Unit = {},
|
||||
) {
|
||||
Row(Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) {
|
||||
Text(text)
|
||||
Spacer(Modifier.fillMaxWidth().weight(1f))
|
||||
Switch(
|
||||
checked = checked,
|
||||
onCheckedChange = onChange,
|
||||
colors = SwitchDefaults.colors(
|
||||
checkedThumbColor = MaterialTheme.colors.primary,
|
||||
uncheckedThumbColor = HighOrLowlight
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun PreferenceToggleWithIcon(
|
||||
text: String,
|
||||
icon: ImageVector? = null,
|
||||
iconColor: Color? = HighOrLowlight,
|
||||
checked: Boolean,
|
||||
onChange: (Boolean) -> Unit = {},
|
||||
) {
|
||||
Row(Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) {
|
||||
if (icon != null) {
|
||||
Icon(
|
||||
icon,
|
||||
null,
|
||||
tint = iconColor ?: HighOrLowlight
|
||||
)
|
||||
Spacer(Modifier.padding(horizontal = 4.dp))
|
||||
}
|
||||
Text(text)
|
||||
Spacer(Modifier.fillMaxWidth().weight(1f))
|
||||
Switch(
|
||||
checked = checked,
|
||||
onCheckedChange = {
|
||||
onChange(it)
|
||||
},
|
||||
colors = SwitchDefaults.colors(
|
||||
checkedThumbColor = MaterialTheme.colors.primary,
|
||||
uncheckedThumbColor = HighOrLowlight
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun runAuth(context: Context, onFinish: (success: Boolean) -> Unit) {
|
||||
authenticate(
|
||||
generalGetString(R.string.auth_open_chat_console),
|
||||
generalGetString(R.string.auth_log_in_using_credential),
|
||||
context as FragmentActivity,
|
||||
completed = { laResult ->
|
||||
onFinish(laResult == LAResult.Success || laResult == LAResult.Unavailable)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@Preview(showBackground = true)
|
||||
@Preview(
|
||||
uiMode = Configuration.UI_MODE_NIGHT_YES,
|
||||
showBackground = true,
|
||||
name = "Dark Mode"
|
||||
)
|
||||
@Composable
|
||||
fun PreviewSettingsLayout() {
|
||||
SimpleXTheme {
|
||||
SettingsLayout(
|
||||
profile = LocalProfile.sampleData,
|
||||
stopped = false,
|
||||
encrypted = false,
|
||||
incognito = remember { mutableStateOf(false) },
|
||||
incognitoPref = SharedPreference({ false }, {}),
|
||||
developerTools = SharedPreference({ false }, {}),
|
||||
userDisplayName = "Alice",
|
||||
setPerformLA = {},
|
||||
showModal = { {} },
|
||||
showSettingsModal = { {} },
|
||||
showCustomModal = { {} },
|
||||
showVersion = {},
|
||||
withAuth = {},
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,152 +0,0 @@
|
||||
package chat.simplex.app.views.usersettings
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import chat.simplex.app.R
|
||||
import chat.simplex.app.model.ChatModel
|
||||
import chat.simplex.app.model.UserContactLinkRec
|
||||
import chat.simplex.app.ui.theme.*
|
||||
import chat.simplex.app.views.helpers.*
|
||||
import chat.simplex.app.views.newchat.QRCode
|
||||
|
||||
@Composable
|
||||
fun UserAddressView(chatModel: ChatModel) {
|
||||
val cxt = LocalContext.current
|
||||
UserAddressLayout(
|
||||
userAddress = remember { chatModel.userAddress }.value,
|
||||
createAddress = {
|
||||
withApi {
|
||||
val connReqContact = chatModel.controller.apiCreateUserAddress()
|
||||
if (connReqContact != null) {
|
||||
chatModel.userAddress.value = UserContactLinkRec(connReqContact)
|
||||
}
|
||||
}
|
||||
},
|
||||
share = { userAddress: String -> shareText(cxt, userAddress) },
|
||||
acceptRequests = {
|
||||
chatModel.userAddress.value?.let { address ->
|
||||
ModalManager.shared.showModal(settings = true) { AcceptRequestsView(chatModel, address) }
|
||||
}
|
||||
},
|
||||
deleteAddress = {
|
||||
AlertManager.shared.showAlertMsg(
|
||||
title = generalGetString(R.string.delete_address__question),
|
||||
text = generalGetString(R.string.all_your_contacts_will_remain_connected),
|
||||
confirmText = generalGetString(R.string.delete_verb),
|
||||
onConfirm = {
|
||||
withApi {
|
||||
chatModel.controller.apiDeleteUserAddress()
|
||||
chatModel.userAddress.value = null
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun UserAddressLayout(
|
||||
userAddress: UserContactLinkRec?,
|
||||
createAddress: () -> Unit,
|
||||
share: (String) -> Unit,
|
||||
acceptRequests: () -> Unit,
|
||||
deleteAddress: () -> Unit
|
||||
) {
|
||||
Column(
|
||||
Modifier.verticalScroll(rememberScrollState()),
|
||||
horizontalAlignment = Alignment.Start,
|
||||
verticalArrangement = Arrangement.Top
|
||||
) {
|
||||
AppBarTitle(stringResource(R.string.your_contact_address), false)
|
||||
Text(
|
||||
stringResource(R.string.you_can_share_your_address_anybody_will_be_able_to_connect),
|
||||
Modifier.padding(bottom = 12.dp),
|
||||
lineHeight = 22.sp
|
||||
)
|
||||
Column(
|
||||
Modifier.fillMaxWidth().padding(bottom = DEFAULT_PADDING_HALF),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.SpaceEvenly
|
||||
) {
|
||||
if (userAddress == null) {
|
||||
SimpleButton(stringResource(R.string.create_address), icon = Icons.Outlined.QrCode, click = createAddress)
|
||||
} else {
|
||||
QRCode(userAddress.connReqContact, Modifier.aspectRatio(1f))
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(10.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.padding(vertical = 16.dp)
|
||||
) {
|
||||
SimpleButton(
|
||||
stringResource(R.string.share_link),
|
||||
icon = Icons.Outlined.Share,
|
||||
click = { share(userAddress.connReqContact) })
|
||||
SimpleButtonIconEnded(
|
||||
stringResource(R.string.contact_requests),
|
||||
icon = Icons.Outlined.ChevronRight,
|
||||
click = acceptRequests
|
||||
)
|
||||
}
|
||||
SimpleButton(
|
||||
stringResource(R.string.delete_address),
|
||||
icon = Icons.Outlined.Delete,
|
||||
color = Color.Red,
|
||||
click = deleteAddress
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(showBackground = true)
|
||||
@Preview(
|
||||
uiMode = Configuration.UI_MODE_NIGHT_YES,
|
||||
showBackground = true,
|
||||
name = "Dark Mode"
|
||||
)
|
||||
@Composable
|
||||
fun PreviewUserAddressLayoutNoAddress() {
|
||||
SimpleXTheme {
|
||||
UserAddressLayout(
|
||||
userAddress = null,
|
||||
createAddress = {},
|
||||
share = { _ -> },
|
||||
acceptRequests = {},
|
||||
deleteAddress = {},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(showBackground = true)
|
||||
@Preview(
|
||||
uiMode = Configuration.UI_MODE_NIGHT_YES,
|
||||
showBackground = true,
|
||||
name = "Dark Mode"
|
||||
)
|
||||
@Composable
|
||||
fun PreviewUserAddressLayoutAddressCreated() {
|
||||
SimpleXTheme {
|
||||
UserAddressLayout(
|
||||
userAddress = UserContactLinkRec("https://simplex.chat/contact#/?v=1&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2FK1rslx-m5bpXVIdMZg9NLUZ_8JBm8xTt%23MCowBQYDK2VuAyEALDeVe-sG8mRY22LsXlPgiwTNs9dbiLrNuA7f3ZMAJ2w%3D"),
|
||||
createAddress = {},
|
||||
share = { _ -> },
|
||||
acceptRequests = {},
|
||||
deleteAddress = {},
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,298 +0,0 @@
|
||||
package chat.simplex.app.views.usersettings
|
||||
|
||||
import android.content.res.Configuration
|
||||
import android.graphics.Bitmap
|
||||
import android.net.Uri
|
||||
import androidx.compose.foundation.*
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.text.BasicTextField
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.input.KeyboardCapitalization
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import chat.simplex.app.R
|
||||
import chat.simplex.app.model.*
|
||||
import chat.simplex.app.ui.theme.*
|
||||
import chat.simplex.app.views.helpers.*
|
||||
import chat.simplex.app.views.isValidDisplayName
|
||||
import com.google.accompanist.insets.ProvideWindowInsets
|
||||
import com.google.accompanist.insets.navigationBarsWithImePadding
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@Composable
|
||||
fun UserProfileView(chatModel: ChatModel, close: () -> Unit) {
|
||||
val user = chatModel.currentUser.value
|
||||
if (user != null) {
|
||||
val editProfile = rememberSaveable { mutableStateOf(false) }
|
||||
var profile by remember { mutableStateOf(user.profile.toProfile()) }
|
||||
UserProfileLayout(
|
||||
editProfile = editProfile,
|
||||
profile = profile,
|
||||
close,
|
||||
saveProfile = { displayName, fullName, image ->
|
||||
withApi {
|
||||
val newProfile = chatModel.controller.apiUpdateProfile(profile.copy(displayName = displayName, fullName = fullName, image = image))
|
||||
if (newProfile != null) {
|
||||
chatModel.updateCurrentUser(newProfile)
|
||||
profile = newProfile
|
||||
}
|
||||
editProfile.value = false
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun UserProfileLayout(
|
||||
editProfile: MutableState<Boolean>,
|
||||
profile: Profile,
|
||||
close: () -> Unit,
|
||||
saveProfile: (String, String, String?) -> Unit,
|
||||
) {
|
||||
val bottomSheetModalState = rememberModalBottomSheetState(initialValue = ModalBottomSheetValue.Hidden)
|
||||
val displayName = remember { mutableStateOf(profile.displayName) }
|
||||
val fullName = remember { mutableStateOf(profile.fullName) }
|
||||
val chosenImage = rememberSaveable { mutableStateOf<Uri?>(null) }
|
||||
val profileImage = rememberSaveable { mutableStateOf(profile.image) }
|
||||
val scope = rememberCoroutineScope()
|
||||
val scrollState = rememberScrollState()
|
||||
val keyboardState by getKeyboardState()
|
||||
var savedKeyboardState by remember { mutableStateOf(keyboardState) }
|
||||
ProvideWindowInsets(windowInsetsAnimationsEnabled = true) {
|
||||
ModalBottomSheetLayout(
|
||||
scrimColor = Color.Black.copy(alpha = 0.12F),
|
||||
modifier = Modifier.navigationBarsWithImePadding(),
|
||||
sheetContent = {
|
||||
GetImageBottomSheet(
|
||||
chosenImage,
|
||||
onImageChange = { bitmap -> profileImage.value = resizeImageToStrSize(cropToSquare(bitmap), maxDataSize = 12500) },
|
||||
hideBottomSheet = {
|
||||
scope.launch { bottomSheetModalState.hide() }
|
||||
})
|
||||
},
|
||||
sheetState = bottomSheetModalState,
|
||||
sheetShape = RoundedCornerShape(topStart = 18.dp, topEnd = 18.dp)
|
||||
) {
|
||||
ModalView(close = close) {
|
||||
Column(
|
||||
Modifier
|
||||
.verticalScroll(scrollState)
|
||||
.padding(horizontal = DEFAULT_PADDING),
|
||||
horizontalAlignment = Alignment.Start
|
||||
) {
|
||||
AppBarTitle(stringResource(R.string.your_current_profile), false)
|
||||
Text(
|
||||
stringResource(R.string.your_profile_is_stored_on_device_and_shared_only_with_contacts_simplex_cannot_see_it),
|
||||
Modifier.padding(bottom = 24.dp),
|
||||
color = MaterialTheme.colors.onBackground,
|
||||
lineHeight = 22.sp
|
||||
)
|
||||
if (editProfile.value) {
|
||||
Column(
|
||||
Modifier.fillMaxWidth(),
|
||||
horizontalAlignment = Alignment.Start
|
||||
) {
|
||||
Box(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(bottom = 24.dp),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Box(contentAlignment = Alignment.TopEnd) {
|
||||
Box(contentAlignment = Alignment.Center) {
|
||||
ProfileImage(192.dp, profileImage.value)
|
||||
EditImageButton { scope.launch { bottomSheetModalState.show() } }
|
||||
}
|
||||
if (profileImage.value != null) {
|
||||
DeleteImageButton { profileImage.value = null }
|
||||
}
|
||||
}
|
||||
}
|
||||
Box {
|
||||
if (!isValidDisplayName(displayName.value)) {
|
||||
Icon(Icons.Outlined.Info, tint = Color.Red, contentDescription = stringResource(R.string.display_name_cannot_contain_whitespace))
|
||||
}
|
||||
ProfileNameTextField(displayName)
|
||||
}
|
||||
ProfileNameTextField(fullName)
|
||||
Row {
|
||||
TextButton(stringResource(R.string.cancel_verb)) {
|
||||
displayName.value = profile.displayName
|
||||
fullName.value = profile.fullName
|
||||
profileImage.value = profile.image
|
||||
editProfile.value = false
|
||||
}
|
||||
Spacer(Modifier.padding(horizontal = 8.dp))
|
||||
val enabled = displayName.value.isNotEmpty() && isValidDisplayName(displayName.value)
|
||||
val saveModifier: Modifier
|
||||
val saveColor: Color
|
||||
if (enabled) {
|
||||
saveModifier = Modifier
|
||||
.clickable { saveProfile(displayName.value, fullName.value, profileImage.value) }
|
||||
saveColor = MaterialTheme.colors.primary
|
||||
} else {
|
||||
saveModifier = Modifier
|
||||
saveColor = HighOrLowlight
|
||||
}
|
||||
Text(
|
||||
stringResource(R.string.save_and_notify_contacts),
|
||||
modifier = saveModifier,
|
||||
color = saveColor
|
||||
)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Column(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalAlignment = Alignment.Start
|
||||
) {
|
||||
Box(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(bottom = 24.dp), contentAlignment = Alignment.Center
|
||||
) {
|
||||
ProfileImage(192.dp, profile.image)
|
||||
if (profile.image == null) {
|
||||
EditImageButton {
|
||||
editProfile.value = true
|
||||
scope.launch { bottomSheetModalState.show() }
|
||||
}
|
||||
}
|
||||
}
|
||||
ProfileNameRow(stringResource(R.string.display_name__field), profile.displayName)
|
||||
ProfileNameRow(stringResource(R.string.full_name__field), profile.fullName)
|
||||
TextButton(stringResource(R.string.edit_verb)) { editProfile.value = true }
|
||||
}
|
||||
}
|
||||
if (savedKeyboardState != keyboardState) {
|
||||
LaunchedEffect(keyboardState) {
|
||||
scope.launch {
|
||||
savedKeyboardState = keyboardState
|
||||
scrollState.animateScrollTo(scrollState.maxValue)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ProfileNameTextField(name: MutableState<String>) {
|
||||
BasicTextField(
|
||||
value = name.value,
|
||||
onValueChange = { name.value = it },
|
||||
modifier = Modifier
|
||||
.padding(bottom = 24.dp)
|
||||
.padding(start = 28.dp)
|
||||
.fillMaxWidth(),
|
||||
textStyle = MaterialTheme.typography.body1.copy(color = MaterialTheme.colors.onBackground),
|
||||
keyboardOptions = KeyboardOptions(
|
||||
capitalization = KeyboardCapitalization.None,
|
||||
autoCorrect = false
|
||||
),
|
||||
singleLine = true
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ProfileNameRow(label: String, text: String) {
|
||||
Row(Modifier.padding(bottom = 24.dp)) {
|
||||
Text(
|
||||
label,
|
||||
color = MaterialTheme.colors.onBackground
|
||||
)
|
||||
Spacer(Modifier.padding(horizontal = 4.dp))
|
||||
Text(
|
||||
text,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = MaterialTheme.colors.onBackground
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun TextButton(text: String, click: () -> Unit) {
|
||||
Text(
|
||||
text,
|
||||
color = MaterialTheme.colors.primary,
|
||||
modifier = Modifier.clickable(onClick = click),
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun EditImageButton(click: () -> Unit) {
|
||||
IconButton(
|
||||
onClick = click,
|
||||
modifier = Modifier.background(Color(1f, 1f, 1f, 0.2f), shape = CircleShape)
|
||||
) {
|
||||
Icon(
|
||||
Icons.Outlined.PhotoCamera,
|
||||
contentDescription = stringResource(R.string.edit_image),
|
||||
tint = MaterialTheme.colors.primary,
|
||||
modifier = Modifier.size(36.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun DeleteImageButton(click: () -> Unit) {
|
||||
IconButton(onClick = click) {
|
||||
Icon(
|
||||
Icons.Outlined.Close,
|
||||
contentDescription = stringResource(R.string.delete_image),
|
||||
tint = MaterialTheme.colors.primary,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(showBackground = true)
|
||||
@Preview(
|
||||
uiMode = Configuration.UI_MODE_NIGHT_YES,
|
||||
showBackground = true,
|
||||
name = "Dark Mode"
|
||||
)
|
||||
@Composable
|
||||
fun PreviewUserProfileLayoutEditOff() {
|
||||
SimpleXTheme {
|
||||
UserProfileLayout(
|
||||
profile = Profile.sampleData,
|
||||
close = {},
|
||||
editProfile = remember { mutableStateOf(false) },
|
||||
saveProfile = { _, _, _ -> }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(showBackground = true)
|
||||
@Preview(
|
||||
uiMode = Configuration.UI_MODE_NIGHT_YES,
|
||||
showBackground = true,
|
||||
name = "Dark Mode"
|
||||
)
|
||||
@Composable
|
||||
fun PreviewUserProfileLayoutEditOn() {
|
||||
SimpleXTheme {
|
||||
UserProfileLayout(
|
||||
profile = Profile.sampleData,
|
||||
close = {},
|
||||
editProfile = remember { mutableStateOf(true) },
|
||||
saveProfile = { _, _, _ -> }
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,141 +0,0 @@
|
||||
package chat.simplex.app.views.usersettings
|
||||
|
||||
import SectionDivider
|
||||
import SectionItemView
|
||||
import SectionTextFooter
|
||||
import SectionView
|
||||
import androidx.compose.foundation.*
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.*
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import chat.simplex.app.R
|
||||
import chat.simplex.app.model.*
|
||||
import chat.simplex.app.ui.theme.*
|
||||
import chat.simplex.app.views.chat.item.ItemAction
|
||||
import chat.simplex.app.views.chatlist.UserProfilePickerItem
|
||||
import chat.simplex.app.views.helpers.*
|
||||
import chat.simplex.app.views.onboarding.CreateProfile
|
||||
|
||||
@Composable
|
||||
fun UserProfilesView(m: ChatModel) {
|
||||
val users by remember { derivedStateOf { m.users.map { it.user } } }
|
||||
UserProfilesView(
|
||||
users = users,
|
||||
addUser = {
|
||||
ModalManager.shared.showModalCloseable { close ->
|
||||
CreateProfile(m, close)
|
||||
}
|
||||
},
|
||||
activateUser = { user ->
|
||||
withBGApi {
|
||||
m.controller.changeActiveUser(user.userId)
|
||||
}
|
||||
},
|
||||
removeUser = { user ->
|
||||
val text = buildAnnotatedString {
|
||||
append(generalGetString(R.string.users_delete_all_chats_deleted) + "\n\n" + generalGetString(R.string.users_delete_profile_for) + " ")
|
||||
withStyle(SpanStyle(fontWeight = FontWeight.Bold)) {
|
||||
append(user.displayName)
|
||||
}
|
||||
append(":")
|
||||
}
|
||||
AlertManager.shared.showAlertDialogButtonsColumn(
|
||||
title = generalGetString(R.string.users_delete_question),
|
||||
text = text,
|
||||
buttons = {
|
||||
Column {
|
||||
SectionItemView({
|
||||
AlertManager.shared.hideAlert()
|
||||
removeUser(m, user, users, true)
|
||||
}) {
|
||||
Text(stringResource(R.string.users_delete_with_connections), color = Color.Red)
|
||||
}
|
||||
SectionItemView({
|
||||
AlertManager.shared.hideAlert()
|
||||
removeUser(m, user, users, false)
|
||||
}
|
||||
) {
|
||||
Text(stringResource(R.string.users_delete_data_only), color = Color.Red)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun UserProfilesView(
|
||||
users: List<User>,
|
||||
addUser: () -> Unit,
|
||||
activateUser: (User) -> Unit,
|
||||
removeUser: (User) -> Unit,
|
||||
) {
|
||||
Column(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.verticalScroll(rememberScrollState())
|
||||
.padding(bottom = DEFAULT_PADDING),
|
||||
) {
|
||||
AppBarTitle(stringResource(R.string.your_chat_profiles))
|
||||
|
||||
SectionView {
|
||||
for (user in users) {
|
||||
UserView(user, users, activateUser, removeUser)
|
||||
SectionDivider()
|
||||
}
|
||||
SectionItemView(addUser, minHeight = 68.dp) {
|
||||
Icon(Icons.Outlined.Add, stringResource(R.string.users_add), tint = MaterialTheme.colors.primary)
|
||||
Spacer(Modifier.padding(horizontal = 4.dp))
|
||||
Text(stringResource(R.string.users_add), color = MaterialTheme.colors.primary)
|
||||
}
|
||||
}
|
||||
SectionTextFooter(stringResource(R.string.your_chat_profiles_stored_locally))
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun UserView(user: User, users: List<User>, activateUser: (User) -> Unit, removeUser: (User) -> Unit) {
|
||||
var showDropdownMenu by remember { mutableStateOf(false) }
|
||||
UserProfilePickerItem(user, onLongClick = { if (users.size > 1) showDropdownMenu = true }) {
|
||||
activateUser(user)
|
||||
}
|
||||
Box(Modifier.padding(horizontal = 16.dp)) {
|
||||
DropdownMenu(
|
||||
expanded = showDropdownMenu,
|
||||
onDismissRequest = { showDropdownMenu = false },
|
||||
Modifier.width(220.dp)
|
||||
) {
|
||||
ItemAction(stringResource(R.string.delete_verb), Icons.Outlined.Delete, color = Color.Red, onClick = {
|
||||
removeUser(user)
|
||||
showDropdownMenu = false
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun removeUser(m: ChatModel, user: User, users: List<User>, delSMPQueues: Boolean) {
|
||||
if (users.size < 2) return
|
||||
|
||||
withBGApi {
|
||||
try {
|
||||
if (user.activeUser) {
|
||||
val newActive = users.first { !it.activeUser }
|
||||
m.controller.changeActiveUser_(newActive.userId)
|
||||
}
|
||||
m.controller.apiDeleteUser(user.userId, delSMPQueues)
|
||||
m.users.removeAll { it.user.userId == user.userId }
|
||||
} catch (e: Exception) {
|
||||
AlertManager.shared.showAlertMsg(generalGetString(R.string.error_deleting_user), e.stackTraceToString())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
package chat.simplex.app.views.usersettings
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import chat.simplex.app.BuildConfig
|
||||
import chat.simplex.app.R
|
||||
import chat.simplex.app.model.CoreVersionInfo
|
||||
import chat.simplex.app.ui.theme.DEFAULT_PADDING
|
||||
import chat.simplex.app.views.helpers.AppBarTitle
|
||||
|
||||
@Composable
|
||||
fun VersionInfoView(info: CoreVersionInfo) {
|
||||
Column(
|
||||
Modifier.padding(horizontal = DEFAULT_PADDING),
|
||||
horizontalAlignment = Alignment.Start
|
||||
) {
|
||||
AppBarTitle(stringResource(R.string.app_version_title), false)
|
||||
Text(String.format(stringResource(R.string.app_version_name), BuildConfig.VERSION_NAME))
|
||||
Text(String.format(stringResource(R.string.app_version_code), BuildConfig.VERSION_CODE))
|
||||
Text(String.format(stringResource(R.string.core_version), info.version))
|
||||
Text(String.format(stringResource(R.string.core_build_timestamp), info.buildTimestamp))
|
||||
val simplexmqCommit = if (info.simplexmqCommit.length >= 7) info.simplexmqCommit.substring(startIndex = 0, endIndex = 7) else info.simplexmqCommit
|
||||
Text(String.format(stringResource(R.string.core_simplexmq_version), info.simplexmqVersion, simplexmqCommit))
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 8.2 KiB |
|
Before Width: | Height: | Size: 6.8 KiB |
|
Before Width: | Height: | Size: 5.9 KiB |
|
Before Width: | Height: | Size: 4.5 KiB |
|
Before Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 47 KiB |
|
Before Width: | Height: | Size: 51 KiB |
|
Before Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 77 KiB |
|
Before Width: | Height: | Size: 84 KiB |
|
Before Width: | Height: | Size: 57 KiB |
@@ -1,22 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
Android drawable generated by fa5ad-free project:
|
||||
https://github.com/diwanoczko/fa5ad-free
|
||||
|
||||
Resource generated base on Font Awesome 5 Free icons set:
|
||||
https://fontawesome.com/
|
||||
|
||||
All brand icons are trademarks of their respective owners.
|
||||
Please do not use brand logos for any purpose except to represent the
|
||||
company, product, or service to which they refer.
|
||||
-->
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="23.25dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="496"
|
||||
android:viewportHeight="512">
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M165.9 397.4c0 2-2.3 3.6-5.2 3.6-3.3.3-5.6-1.3-5.6-3.6 0-2 2.3-3.6 5.2-3.6 3-.3 5.6 1.3 5.6 3.6zm-31.1-4.5c-.7 2 1.3 4.3 4.3 4.9 2.6 1 5.6 0 6.2-2s-1.3-4.3-4.3-5.2c-2.6-.7-5.5.3-6.2 2.3zm44.2-1.7c-2.9.7-4.9 2.6-4.6 4.9.3 2 2.9 3.3 5.9 2.6 2.9-.7 4.9-2.6 4.6-4.6-.3-1.9-3-3.2-5.9-2.9zM244.8 8C106.1 8 0 113.3 0 252c0 110.9 69.8 205.8 169.5 239.2 12.8 2.3 17.3-5.6 17.3-12.1 0-6.2-.3-40.4-.3-61.4 0 0-70 15-84.7-29.8 0 0-11.4-29.1-27.8-36.6 0 0-22.9-15.7 1.6-15.4 0 0 24.9 2 38.6 25.8 21.9 38.6 58.6 27.5 72.9 20.9 2.3-16 8.8-27.1 16-33.7-55.9-6.2-112.3-14.3-112.3-110.5 0-27.5 7.6-41.3 23.6-58.9-2.6-6.5-11.1-33.3 2.6-67.9 20.9-6.5 69 27 69 27 20-5.6 41.5-8.5 62.8-8.5s42.8 2.9 62.8 8.5c0 0 48.1-33.6 69-27 13.7 34.7 5.2 61.4 2.6 67.9 16 17.7 25.8 31.5 25.8 58.9 0 96.5-58.9 104.2-114.8 110.5 9.2 7.9 17 22.9 17 46.4 0 33.7-.3 75.4-.3 83.6 0 6.5 4.6 14.4 17.3 12.1C428.2 457.8 496 362.9 496 252 496 113.3 383.5 8 244.8 8zM97.2 352.9c-1.3 1-1 3.3.7 5.2 1.6 1.6 3.9 2.3 5.2 1 1.3-1 1-3.3-.7-5.2-1.6-1.6-3.9-2.3-5.2-1zm-10.8-8.1c-.7 1.3.3 2.9 2.3 3.9 1.6 1 3.6.7 4.3-.7.7-1.3-.3-2.9-2.3-3.9-2-.6-3.6-.3-4.3.7zm32.4 35.6c-1.6 1.3-1 4.3 1.3 6.2 2.3 2.3 5.2 2.6 6.5 1 1.3-1.3.7-4.3-1.3-6.2-2.2-2.3-5.2-2.6-6.5-1zm-11.4-14.7c-1.6 1-1.6 3.6 0 5.9 1.6 2.3 4.3 3.3 5.6 2.3 1.6-1.3 1.6-3.9 0-6.2-1.4-2.3-4-3.3-5.6-2z"
|
||||
/>
|
||||
</vector>
|
||||
@@ -1,170 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="108dp"
|
||||
android:height="108dp"
|
||||
android:viewportWidth="108"
|
||||
android:viewportHeight="108">
|
||||
<path
|
||||
android:fillColor="#3DDC84"
|
||||
android:pathData="M0,0h108v108h-108z" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M9,0L9,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,0L19,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M29,0L29,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M39,0L39,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M49,0L49,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M59,0L59,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M69,0L69,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M79,0L79,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M89,0L89,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M99,0L99,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,9L108,9"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,19L108,19"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,29L108,29"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,39L108,39"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,49L108,49"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,59L108,59"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,69L108,69"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,79L108,79"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,89L108,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,99L108,99"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,29L89,29"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,39L89,39"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,49L89,49"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,59L89,59"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,69L89,69"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,79L89,79"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M29,19L29,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M39,19L39,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M49,19L49,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M59,19L59,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M69,19L69,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M79,19L79,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
</vector>
|
||||
@@ -1,10 +0,0 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24"
|
||||
android:tint="?attr/colorControlNormal">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M20,4H4C2.89,4 2,4.9 2,6v12c0,1.1 0.89,2 2,2h16c1.1,0 2,-0.9 2,-2V6C22,4.9 21.11,4 20,4zM20,18H4V8h16V18zM18,17h-6v-2h6V17zM7.5,17l-1.41,-1.41L8.67,13l-2.59,-2.59L7.5,9l4,4L7.5,17z"/>
|
||||
</vector>
|
||||
@@ -1,953 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="allow_voice_messages_only_if">Povolte hlasové zprávy, pouze pokud je váš kontakt povolí.</string>
|
||||
<string name="allow_to_send_disappearing">Povolit odesílání mizejících zpráv.</string>
|
||||
<string name="allow_to_send_voice">Povolit odesílání hlasových zpráv.</string>
|
||||
<string name="v4_2_group_links_desc">Správci mohou vytvářet odkazy pro připojení ke skupinám.</string>
|
||||
<string name="accept_contact_button">Přijmout</string>
|
||||
<string name="smp_servers_preset_add">Přidejte přednastavené servery</string>
|
||||
<string name="network_settings">Pokročilá nastavení sítě</string>
|
||||
<string name="accept">Přijmout</string>
|
||||
<string name="smp_servers_add">Přidat server…</string>
|
||||
<string name="network_enable_socks_info">Přistupovat k serverům přes SOCKS proxy na portu 9050\? Před povolením této možnosti musí být spuštěna proxy.</string>
|
||||
<string name="accept_feature">Přijmout</string>
|
||||
<string name="allow_your_contacts_to_send_disappearing_messages">Umožněte svým kontaktům odesílat mizející zprávy.</string>
|
||||
<string name="about_simplex_chat">O <xliff:g id="appNameFull">SimpleX Chat</xliff:g></string>
|
||||
<string name="smp_servers_add_to_another_device">Přidat do jiného zařízení</string>
|
||||
<string name="accept_requests">Přijímat žádosti</string>
|
||||
<string name="allow_verb">Povolit</string>
|
||||
<string name="allow_voice_messages_question">Povolit hlasové zprávy\?</string>
|
||||
<string name="about_simplex">O SimpleX</string>
|
||||
<string name="a_plus_b">a + b</string>
|
||||
<string name="accept_call_on_lock_screen">Přijmout</string>
|
||||
<string name="chat_item_ttl_day">1 den</string>
|
||||
<string name="group_member_role_admin">správce</string>
|
||||
<string name="users_add">Přidat profil</string>
|
||||
<string name="users_delete_all_chats_deleted">Všechny chaty a zprávy budou smazány – tuto akci nelze vrátit zpět!</string>
|
||||
<string name="allow_disappearing_messages_only_if">Povolte mizející zprávy, pouze pokud to váš kontakt povolí.</string>
|
||||
<string name="v4_3_improved_server_configuration_desc">Přidejte servery skenováním QR kódů.</string>
|
||||
<string name="chat_item_ttl_month">1 měsíc</string>
|
||||
<string name="chat_item_ttl_week">1 týden</string>
|
||||
<string name="callstatus_accepted">přijatý hovor</string>
|
||||
<string name="accept_contact_incognito_button">Přijmout inkognito</string>
|
||||
<string name="accept_connection_request__question">Přijmout žádost o připojení\?</string>
|
||||
<string name="all_group_members_will_remain_connected">Všichni členové skupiny zůstanou připojeni.</string>
|
||||
<string name="allow_irreversible_message_deletion_only_if">Povolte nevratné smazání zprávy pouze v případě, že vám to váš kontakt dovolí.</string>
|
||||
<string name="allow_direct_messages">Povolit odesílání přímých zpráv členům.</string>
|
||||
<string name="allow_to_delete_messages">Povolit nevratné smazání odeslaných zpráv.</string>
|
||||
<string name="clear_chat_warning">Všechny zprávy budou smazány – tuto akci nelze vrátit zpět! Zprávy budou smazány POUZE pro vás.</string>
|
||||
<string name="allow_your_contacts_irreversibly_delete">Umožněte svým kontaktům nevratně odstranit odeslané zprávy.</string>
|
||||
<string name="allow_your_contacts_to_send_voice_messages">Povolte svým kontaktům odesílání hlasových zpráv.</string>
|
||||
<string name="button_create_group_link">Vytvořit odkaz</string>
|
||||
<string name="delete_link_question">Smazat odkaz\?</string>
|
||||
<string name="button_send_direct_message">Odeslat přímou zprávu</string>
|
||||
<string name="member_info_section_title_member">ČLEN</string>
|
||||
<string name="change_member_role_question">Změnit roli ve skupině\?</string>
|
||||
<string name="info_row_connection">Připojení</string>
|
||||
<string name="conn_level_desc_indirect">nepřímé (<xliff:g id="conn_level">%1$s</xliff:g>)</string>
|
||||
<string name="conn_stats_section_title_servers">SERVERY</string>
|
||||
<string name="receiving_via">Příjem prostřednictvím</string>
|
||||
<string name="create_secret_group_title">Vytvoření tajné skupiny</string>
|
||||
<string name="group_display_name_field">Zobrazení názvu skupiny:</string>
|
||||
<string name="group_full_name_field">Úplný název skupiny:</string>
|
||||
<string name="group_main_profile_sent">Váš profil v chatu bude zaslán členům skupiny</string>
|
||||
<string name="group_profile_is_stored_on_members_devices">Profil skupiny je uložen v zařízeních členů, nikoli na serverech.</string>
|
||||
<string name="network_options_save">Uložit</string>
|
||||
<string name="update_network_settings_question">Aktualizovat nastavení sítě\?</string>
|
||||
<string name="incognito">Inkognito</string>
|
||||
<string name="incognito_random_profile">Váš náhodný profil</string>
|
||||
<string name="incognito_random_profile_description">Vašemu kontaktu bude zaslán náhodný profil</string>
|
||||
<string name="save_color">Uložit barvu</string>
|
||||
<string name="reset_color">Obnovení barev</string>
|
||||
<string name="color_primary">Akcent</string>
|
||||
<string name="chat_preferences_you_allow">Povolíte</string>
|
||||
<string name="chat_preferences_default">výchozí (%s)</string>
|
||||
<string name="chat_preferences_yes">ano</string>
|
||||
<string name="chat_preferences_no">ne</string>
|
||||
<string name="chat_preferences_always">vždy</string>
|
||||
<string name="set_group_preferences">Nastavení skupinových předvoleb</string>
|
||||
<string name="your_preferences">Vaše preference</string>
|
||||
<string name="timed_messages">Zmizení zpráv</string>
|
||||
<string name="feature_enabled_for_contact">povoleno pro kontakt</string>
|
||||
<string name="feature_received_prohibited">přijaté, zakázané</string>
|
||||
<string name="both_you_and_your_contact_can_send_disappearing">Vy i váš kontakt můžete posílat mizející zprávy.</string>
|
||||
<string name="only_your_contact_can_send_disappearing">Zmizelé zprávy může odesílat pouze váš kontakt.</string>
|
||||
<string name="only_you_can_delete_messages">Nevratně mazat zprávy můžete pouze vy (váš kontakt je může označit ke smazání).</string>
|
||||
<string name="message_deletion_prohibited">Nevratné mazání zpráv je v tomto chatu zakázáno.</string>
|
||||
<string name="prohibit_direct_messages">Zakázat odesílání přímých zpráv členům.</string>
|
||||
<string name="ttl_sec">%d sec</string>
|
||||
<string name="ttl_s">%ds</string>
|
||||
<string name="ttl_min">%d min</string>
|
||||
<string name="ttl_hour">\"%d hodina</string>
|
||||
<string name="feature_offered_item_with_param">offered %s: %2s</string>
|
||||
<string name="v4_2_group_links">Odkazy na skupiny</string>
|
||||
<string name="v4_3_voice_messages">Hlasové zprávy</string>
|
||||
<string name="v4_3_irreversible_message_deletion_desc">Vaše kontakty mohou povolit úplné vymazání zpráv.</string>
|
||||
<string name="v4_4_disappearing_messages">Zmizení zpráv</string>
|
||||
<string name="v4_4_verify_connection_security_desc">Porovnejte bezpečnostní kódy se svými kontakty.</string>
|
||||
<string name="app_name"><xliff:g id="appName">SimpleX</xliff:g></string>
|
||||
<string name="thousand_abbreviation">k</string>
|
||||
<string name="connect_via_contact_link">Připojit se přes kontaktní odkaz\?</string>
|
||||
<string name="connect_via_invitation_link">Připojit se přes pozvánku\?</string>
|
||||
<string name="connect_via_group_link">Připojit se přes odkaz skupiny\?</string>
|
||||
<string name="profile_will_be_sent_to_contact_sending_link">Váš profil bude odeslán kontaktu, od kterého jste obdrželi tento odkaz.</string>
|
||||
<string name="server_connected">připojeno</string>
|
||||
<string name="server_error">chyba</string>
|
||||
<string name="server_connecting">připojení</string>
|
||||
<string name="trying_to_connect_to_server_to_receive_messages">Pokus o připojení k serveru používanému pro příjem zpráv od tohoto kontaktu.</string>
|
||||
<string name="deleted_description">Smazáno</string>
|
||||
<string name="invalid_chat">neplatný chat</string>
|
||||
<string name="invalid_data">neplatné údaje</string>
|
||||
<string name="connection_local_display_name">spojení <xliff:g id="connection ID" example="1">%1$d</xliff:g></string>
|
||||
<string name="display_name_connection_established">spojení navázáno</string>
|
||||
<string name="display_name_invited_to_connect">pozvánka k připojení</string>
|
||||
<string name="display_name_connecting">připojení…</string>
|
||||
<string name="description_you_shared_one_time_link">jste sdíleli jednorázové spojení</string>
|
||||
<string name="description_you_shared_one_time_link_incognito">sdíleli jste jednorázový odkaz inkognito</string>
|
||||
<string name="description_via_group_link">prostřednictvím skupinového odkazu</string>
|
||||
<string name="description_via_contact_address_link">prostřednictvím odkazu na kontaktní adresu</string>
|
||||
<string name="description_via_contact_address_link_incognito">inkognito přes odkaz na kontaktní adresu</string>
|
||||
<string name="description_via_one_time_link">prostřednictvím jednorázového odkazu</string>
|
||||
<string name="description_via_one_time_link_incognito">inkognito přes jednorázový odkaz</string>
|
||||
<string name="simplex_link_contact">SimpleX kontaktní adresa</string>
|
||||
<string name="simplex_link_invitation">Jednorázová pozvánka SimpleX</string>
|
||||
<string name="simplex_link_group">Skupinový odkaz SimpleX</string>
|
||||
<string name="simplex_link_connection">prostřednictvím <xliff:g id="serverHost" example="smp.simplex.im">%1$s</xliff:g></string>
|
||||
<string name="simplex_link_mode">Odkazy na SimpleX</string>
|
||||
<string name="simplex_link_mode_description">Popis</string>
|
||||
<string name="simplex_link_mode_full">Úplný odkaz</string>
|
||||
<string name="simplex_link_mode_browser">Prostřednictvím prohlížeče</string>
|
||||
<string name="simplex_link_mode_browser_warning">Otevření odkazu v prohlížeči může snížit soukromí a bezpečnost připojení. Nedůvěryhodné odkazy SimpleX budou červené.</string>
|
||||
<string name="error_saving_smp_servers">Chyba při ukládání serverů SMP</string>
|
||||
<string name="error_setting_network_config">Chyba při aktualizaci konfigurace sítě</string>
|
||||
<string name="failed_to_parse_chat_title">Nepodařilo se načíst chat</string>
|
||||
<string name="failed_to_parse_chats_title">Nepodařilo se načíst chaty</string>
|
||||
<string name="contact_developers">Aktualizujte aplikaci a kontaktujte vývojáře.</string>
|
||||
<string name="connection_timeout">Časový limit připojení</string>
|
||||
<string name="connection_error">Chyba připojení</string>
|
||||
<string name="network_error_desc">Zkontrolujte prosím své síťové připojení pomocí <xliff:g id="serverHost" example="smp.simplex.im">%1$s</xliff:g> a zkuste to znovu.</string>
|
||||
<string name="error_sending_message">Chyba při odesílání zprávy</string>
|
||||
<string name="error_adding_members">Chyba při přidávání prutu(ů)</string>
|
||||
<string name="contact_already_exists">Kontakt již existuje</string>
|
||||
<string name="you_are_already_connected_to_vName_via_this_link">Jste již připojeni k <xliff:g id="contactName" example="Alice">%1$s!</xliff:g>.</string>
|
||||
<string name="invalid_connection_link">Neplatný odkaz na spojení</string>
|
||||
<string name="error_accepting_contact_request">Chyba příjmu požadavku od kontaktu</string>
|
||||
<string name="error_changing_address">Chuba změny adresy</string>
|
||||
<string name="settings_notifications_mode_title">Služba oznamování</string>
|
||||
<string name="notifications_mode_service_desc">Služba na pozadí je spuštěna vždy - oznámení se zobrazí, jakmile jsou zprávy k dispozici.</string>
|
||||
<string name="notification_preview_mode_message">Text zprávy</string>
|
||||
<string name="notification_preview_mode_contact">Jméno kontaktu</string>
|
||||
<string name="notification_preview_mode_hidden">Skryté</string>
|
||||
<string name="notification_preview_mode_message_desc">Zobrazit kontakt a zprávu</string>
|
||||
<string name="notification_contact_connected">Připojeno</string>
|
||||
<string name="la_notice_title_simplex_lock">SimpleX Lock</string>
|
||||
<string name="auth_log_in_using_credential">Přihlaste se pomocí svého pověření</string>
|
||||
<string name="auth_enable_simplex_lock">Zapnutí zámku SimpleX</string>
|
||||
<string name="reply_verb">Odpovězte na</string>
|
||||
<string name="share_verb">Sdílet</string>
|
||||
<string name="copy_verb">Kopírovat</string>
|
||||
<string name="icon_descr_received_msg_status_unread">nepřečteno</string>
|
||||
<string name="personal_welcome">Vítejte <xliff:g>%1$s</xliff:g>!</string>
|
||||
<string name="welcome">Vítejte!</string>
|
||||
<string name="this_text_is_available_in_settings">Tento text je k dispozici v nastavení</string>
|
||||
<string name="icon_descr_sent_msg_status_send_failed">odeslání se nezdařilo</string>
|
||||
<string name="share_file">Sdílet soubor…</string>
|
||||
<string name="attach">Připojit</string>
|
||||
<string name="icon_descr_context">Kontextová ikona</string>
|
||||
<string name="image_decoding_exception_desc">Obrázek nelze dekódovat. Zkuste prosím použít jiný obrázek nebo kontaktujte vývojáře.</string>
|
||||
<string name="image_descr">Obrázek</string>
|
||||
<string name="icon_descr_waiting_for_image">Čekání na obrázek</string>
|
||||
<string name="icon_descr_asked_to_receive">Požádáno o přijetí obrázku</string>
|
||||
<string name="icon_descr_image_snd_complete">Obrázek odeslán</string>
|
||||
<string name="waiting_for_image">Čekáme na obrázek</string>
|
||||
<string name="image_will_be_received_when_contact_is_online">Obrázek bude přijat, až bude váš kontakt online, vyčkejte prosím nebo se podívejte později!</string>
|
||||
<string name="contact_sent_large_file">Váš kontakt odeslal soubor, který je větší než aktuálně podporovaná maximální velikost (<xliff:g id="maxFileSize">%1$s</xliff:g>).</string>
|
||||
<string name="maximum_supported_file_size">V současné době je maximální podporovaná velikost souboru <xliff:g id="maxFileSize">%1$s</xliff:g>.</string>
|
||||
<string name="error_saving_file">Chyba při ukládání souboru</string>
|
||||
<string name="voice_message">Hlasová zpráva</string>
|
||||
<string name="voice_message_send_text">Hlasová zpráva…</string>
|
||||
<string name="icon_descr_server_status_connected">Připojeno</string>
|
||||
<string name="icon_descr_server_status_disconnected">Odpojeno</string>
|
||||
<string name="icon_descr_server_status_error">Chyba</string>
|
||||
<string name="switch_receiving_address_desc">Tato funkce je experimentální! Bude fungovat pouze v případě, že druhý klient má nainstalovanou verzi 4.2. Po dokončení změny adresy by se měla v konverzaci zobrazit zpráva - zkontrolujte, zda můžete od tohoto kontaktu (nebo člena skupiny) stále přijímat zprávy.</string>
|
||||
<string name="switch_receiving_address_question">Přepnout přijímací adresu\?</string>
|
||||
<string name="send_verb">Poslat</string>
|
||||
<string name="you_need_to_allow_to_send_voice">Abyste mohli odesílat hlasové zprávy, musíte je povolit svému kontaktu.</string>
|
||||
<string name="icon_descr_cancel_live_message">Zrušit živou zprávu</string>
|
||||
<string name="back">Zpět</string>
|
||||
<string name="cancel_verb">Zrušit</string>
|
||||
<string name="reset_verb">Obnovit</string>
|
||||
<string name="ok">OK</string>
|
||||
<string name="no_details">bez podrobností</string>
|
||||
<string name="add_contact">Jednorázový zvací odkaz</string>
|
||||
<string name="copied">Zkopírováno do schránky</string>
|
||||
<string name="add_contact_or_create_group">Začít novou konverzaci</string>
|
||||
<string name="create_group">Vytvořit tajnou skupinu</string>
|
||||
<string name="to_share_with_your_contact">(sdílet s kontaktem)</string>
|
||||
<string name="only_stored_on_members_devices">(uloženo pouze členy skupiny)</string>
|
||||
<string name="toast_permission_denied">Oprávnění zamítnuto!</string>
|
||||
<string name="use_camera_button">Použít fotoaparát</string>
|
||||
<string name="from_gallery_button">Z Galerie</string>
|
||||
<string name="choose_file">Vybrat soubor</string>
|
||||
<string name="to_start_a_new_chat_help_header">Pro zahájení nové konverzace</string>
|
||||
<string name="chat_help_tap_button">Klepněte na tlačítko</string>
|
||||
<string name="above_then_preposition_continuation">nad, potom:</string>
|
||||
<string name="add_new_contact_to_create_one_time_QR_code"><b>Přidat nový kontakt</b>: vytvořit jednorázový kód QR.</string>
|
||||
<string name="scan_QR_code_to_connect_to_contact_who_shows_QR_code"><b>Skenovat QR kód</b>: připojení ke kontaktu, který vám ukáže QR kód.</string>
|
||||
<string name="desktop_scan_QR_code_from_app_via_scan_QR_code">💻 desktop: scan displayed QR code from the app, via <b>Scan QR code</b>.</string>
|
||||
<string name="clear_chat_question">Vyčistit konverzaci\?</string>
|
||||
<string name="clear_verb">Čistý</string>
|
||||
<string name="mark_read">Označit přečtení</string>
|
||||
<string name="mark_unread">Označit jako nepřečtené</string>
|
||||
<string name="mute_chat">Ztlumit</string>
|
||||
<string name="unmute_chat">Zrušit ztlumení</string>
|
||||
<string name="you_invited_your_contact">Pozvali jste svůj kontakt</string>
|
||||
<string name="contact_you_shared_link_with_wont_be_able_to_connect">Kontakt, se kterým jste tento odkaz sdíleli, se NEBUDE moci připojit!</string>
|
||||
<string name="connection_you_accepted_will_be_cancelled">Připojení, které jste přijali, bude zrušeno!</string>
|
||||
<string name="icon_descr_help">help</string>
|
||||
<string name="icon_descr_simplex_team"><xliff:g id="appName">SimpleX</xliff:g> Tým</string>
|
||||
<string name="icon_descr_address"><xliff:g id="appName">SimpleX</xliff:g> Adresa</string>
|
||||
<string name="you_will_be_connected_when_group_host_device_is_online">Ke skupině budete připojeni, až bude zařízení hostitele skupiny online, vyčkejte prosím nebo se podívejte později!</string>
|
||||
<string name="you_will_be_connected_when_your_connection_request_is_accepted">Budete připojeni, jakmile bude vaše žádost o připojení přijata, vyčkejte prosím nebo se podívejte později!</string>
|
||||
<string name="connection_request_sent">Požadavek na připojení byl odeslán!</string>
|
||||
<string name="your_profile_will_be_sent">Váš profil v chatu bude odeslán vašemu kontaktu</string>
|
||||
<string name="create_one_time_link">Vytvořit jednorázovou pozvánku</string>
|
||||
<string name="one_time_link">Vytvořit jednorázový zvací odkaz</string>
|
||||
<string name="security_code">Bezpečnostní kód</string>
|
||||
<string name="is_verified">\"%s je ověřeno</string>
|
||||
<string name="chat_console">Konzola pro chat</string>
|
||||
<string name="smp_servers">SMP servery</string>
|
||||
<string name="smp_servers_preset_address">Přednastavená adresa serveru</string>
|
||||
<string name="smp_servers_test_failed">Test serveru se nezdařil!</string>
|
||||
<string name="smp_servers_test_some_failed">Některé servery neprošly testem:</string>
|
||||
<string name="smp_servers_scan_qr">Naskenujte QR kód serveru</string>
|
||||
<string name="smp_servers_enter_manually">Zadejte server ručně</string>
|
||||
<string name="smp_servers_invalid_address">Neplatná adresa serveru!</string>
|
||||
<string name="smp_servers_check_address">Zkontrolujte adresu serveru a zkuste to znovu.</string>
|
||||
<string name="smp_servers_delete_server">Smazat server</string>
|
||||
<string name="contribute">Přispějte na</string>
|
||||
<string name="how_to">Jak na to</string>
|
||||
<string name="how_to_use_your_servers">Jak používat servery</string>
|
||||
<string name="your_ICE_servers">Vaše servery ICE</string>
|
||||
<string name="configure_ICE_servers">Konfigurace serverů ICE</string>
|
||||
<string name="network_settings_title">Nastavení sítě</string>
|
||||
<string name="network_enable_socks">Použít proxy server SOCKS\?</string>
|
||||
<string name="network_disable_socks">Použít přímé připojení k internetu\?</string>
|
||||
<string name="network_use_onion_hosts_no">Ne</string>
|
||||
<string name="network_use_onion_hosts_no_desc_in_alert">Onion hostitelé nebudou použiti.</string>
|
||||
<string name="network_session_mode_user">Profil chatu</string>
|
||||
<string name="network_session_mode_entity">Připojení</string>
|
||||
<string name="core_simplexmq_version">simplexmq: v%s (%2s)</string>
|
||||
<string name="create_address">Vytvořit adresu</string>
|
||||
<string name="accept_automatically">Automaticky</string>
|
||||
<string name="section_title_welcome_message">UVÍTACÍ ZPRÁVA</string>
|
||||
<string name="save_and_notify_group_members">Uložit a upozornit členy skupiny</string>
|
||||
<string name="exit_without_saving">Ukončit bez uložení</string>
|
||||
<string name="the_messaging_and_app_platform_protecting_your_privacy_and_security">Platforma pro zasílání zpráv a aplikace chránící vaše soukromí a bezpečnost.</string>
|
||||
<string name="create_profile">Vytvoření profilu</string>
|
||||
<string name="profile_is_only_shared_with_your_contacts">Profil je sdílen pouze s vašimi kontakty.</string>
|
||||
<string name="display_name_cannot_contain_whitespace">Zobrazované jméno nesmí obsahovat bílé znaky.</string>
|
||||
<string name="bold">tučně</string>
|
||||
<string name="callstatus_in_progress">probíhající hovor</string>
|
||||
<string name="decentralized">Decentralizované</string>
|
||||
<string name="how_it_works">Jak to funguje</string>
|
||||
<string name="how_simplex_works">Jak funguje <xliff:g id="appName">SimpleX</xliff:g></string>
|
||||
<string name="only_client_devices_store_contacts_groups_e2e_encrypted_messages">Pouze klientská zařízení ukládají uživatelské profily, kontakty, skupiny a zprávy odesílané pomocí <b>2vrstvého end-to-end šifrování</b>.</string>
|
||||
<string name="onboarding_notifications_mode_title">Soukromá oznámení</string>
|
||||
<string name="onboarding_notifications_mode_periodic">Pravidelný</string>
|
||||
<string name="ignore">Ignorovat</string>
|
||||
<string name="call_already_ended">Hovor již skončil!</string>
|
||||
<string name="icon_descr_video_call">videohovor</string>
|
||||
<string name="icon_descr_audio_call">audio hovor</string>
|
||||
<string name="settings_audio_video_calls">Audio a video hovory</string>
|
||||
<string name="call_on_lock_screen">Hovory na uzamčené obrazovce:</string>
|
||||
<string name="open_simplex_chat_to_accept_call">Otevřete <xliff:g id="appNameFull">SimpleX Chat</xliff:g> pro přijetí hovoru</string>
|
||||
<string name="allow_accepting_calls_from_lock_screen">Povolte volání ze zamčené obrazovky prostřednictvím Nastavení.</string>
|
||||
<string name="open_verb">Otevřete stránku</string>
|
||||
<string name="icon_descr_audio_on">Zvuk zapnut</string>
|
||||
<string name="icon_descr_speaker_off">Reproduktor vypnut</string>
|
||||
<string name="icon_descr_speaker_on">Zapnutý reproduktor</string>
|
||||
<string name="icon_descr_call_progress">Probíhající hovor</string>
|
||||
<string name="auto_accept_images">Automatické přijímání obrázků</string>
|
||||
<string name="settings_section_title_settings">NASTAVENÍ</string>
|
||||
<string name="settings_section_title_help">NÁPOVĚDA</string>
|
||||
<string name="settings_section_title_device">ZAŘÍZENÍ</string>
|
||||
<string name="settings_section_title_chats">CHATS</string>
|
||||
<string name="settings_experimental_features">Experimentální funkce</string>
|
||||
<string name="settings_section_title_socks">SOCKS PROXY</string>
|
||||
<string name="settings_section_title_icon">IKONA APLIKACE</string>
|
||||
<string name="settings_section_title_themes">TÉMATA</string>
|
||||
<string name="settings_section_title_messages">ZPRÁVY</string>
|
||||
<string name="settings_section_title_calls">VOLÁNÍ</string>
|
||||
<string name="export_database">Export databáze</string>
|
||||
<string name="import_database">Import databáze</string>
|
||||
<string name="delete_database">Odstranění databáze</string>
|
||||
<string name="error_exporting_chat_database">Chyba při exportu databáze chatu</string>
|
||||
<string name="import_database_confirmation">Import</string>
|
||||
<string name="restart_the_app_to_use_imported_chat_database">Restartujte aplikaci, abyste mohli používat importovanou databázi chatu.</string>
|
||||
<string name="delete_chat_profile_question">Smazat profil chatu\?</string>
|
||||
<string name="delete_chat_profile_action_cannot_be_undone_warning">Tuto akci nelze vzít zpět - váš profil, kontakty, zprávy a soubory budou nenávratně ztraceny.</string>
|
||||
<string name="restart_the_app_to_create_a_new_chat_profile">Restartujte aplikaci a vytvořte nový profil chatu.</string>
|
||||
<string name="you_must_use_the_most_recent_version_of_database">Nejnovější verzi databáze chatu musíte používat POUZE v jednom zařízení, jinak se může stát, že přestanete přijímat zprávy od některých kontaktů.</string>
|
||||
<string name="stop_chat_to_enable_database_actions">Zastavte chat a povolte akce s databází.</string>
|
||||
<string name="files_and_media_section">Soubory a média</string>
|
||||
<string name="delete_files_and_media_question">Smazat soubory a média\?</string>
|
||||
<string name="delete_messages">Odstranění zpráv</string>
|
||||
<string name="remove_passphrase_from_keychain">Odstranit přístupovou frázi z úložiště klíčů\?</string>
|
||||
<string name="notifications_will_be_hidden">Oznámení budou doručována pouze do doby, než se aplikace zastaví!</string>
|
||||
<string name="remove_passphrase">Odstranit</string>
|
||||
<string name="update_database">Aktualizovat</string>
|
||||
<string name="current_passphrase">Aktuální přístupová fráze…</string>
|
||||
<string name="update_database_passphrase">Aktualizovat přístupovou frázi databáze</string>
|
||||
<string name="enter_correct_current_passphrase">Zadejte prosím správnou aktuální přístupovou frázi.</string>
|
||||
<string name="database_is_not_encrypted">Váš databáze konverzace není zašifrována - nastavte přístupovou frázi pro její ochranu.</string>
|
||||
<string name="keychain_is_storing_securely">K bezpečnému uložení heslové fráze slouží úložiště klíčů Android - umožňuje fungování služby oznámení.</string>
|
||||
<string name="impossible_to_recover_passphrase"><b>Upozornění</b>: pokud přístupovou frázi ztratíte, NEBUDE možné ji obnovit ani změnit.</string>
|
||||
<string name="database_will_be_encrypted_and_passphrase_stored">Databáze bude zašifrována a přístupová fráze bude uložena v úložišti klíčů.</string>
|
||||
<string name="store_passphrase_securely">Heslo uložte bezpečně, v případě jeho ztráty jej NEBUDE možné změnit.</string>
|
||||
<string name="file_with_path">Soubor: %s</string>
|
||||
<string name="database_passphrase_is_required">Pro otevření chatu je vyžadována přístupová fráze databáze.</string>
|
||||
<string name="unknown_error">Neznámá chyba</string>
|
||||
<string name="open_chat">Otevřete chat</string>
|
||||
<string name="restore_database">Obnovte zálohu databáze</string>
|
||||
<string name="restore_database_alert_desc">Po obnovení zálohy databáze zadejte předchozí heslo. Tuto akci nelze vrátit zpět.</string>
|
||||
<string name="chat_is_stopped_indication">Chat je zastaven</string>
|
||||
<string name="chat_archive_header">Chat se archivuje</string>
|
||||
<string name="delete_chat_archive_question">Smazat archiv chatu\?</string>
|
||||
<string name="join_group_question">Připojit se ke skupině\?</string>
|
||||
<string name="join_group_button">Připojte se na</string>
|
||||
<string name="leave_group_button">Opustit</string>
|
||||
<string name="icon_descr_add_members">Pozvat členy</string>
|
||||
<string name="alert_title_no_group">Skupina nebyla nalezena!</string>
|
||||
<string name="alert_title_cant_invite_contacts">Nelze pozvat kontakty!</string>
|
||||
<string name="snd_group_event_changed_member_role">změnili jste roli %s na %s</string>
|
||||
<string name="snd_group_event_changed_role_for_yourself">změnili jste svou roli na %s</string>
|
||||
<string name="snd_group_event_member_deleted">odstranili jste <xliff:g id="profil člena" example="alice (Alice)">%1$s</xliff:g></string>
|
||||
<string name="snd_group_event_user_left">odešli jste</string>
|
||||
<string name="rcv_conn_event_switch_queue_phase_completed">změnila se vaše adresa</string>
|
||||
<string name="icon_descr_expand_role">Rozšířit výběr rolí</string>
|
||||
<string name="invite_prohibited">Nelze pozvat kontakt!</string>
|
||||
<string name="failed_to_create_user_duplicate_desc">Již máte profil chatu se stejným zobrazovacím názvem. Zvolte prosím jiné jméno.</string>
|
||||
<string name="smp_server_test_create_queue">Vytvořit frontu</string>
|
||||
<string name="smp_server_test_secure_queue">Zabezpečit frontu</string>
|
||||
<string name="service_notifications">Okamžitá oznámení!</string>
|
||||
<string name="it_can_disabled_via_settings_notifications_still_shown"><b>V nastavení ji lze vypnout</b> - oznámení se budou zobrazovat pokud aplikace běží.</string>
|
||||
<string name="turn_off_battery_optimization">Chcete-li ji používat, <b>vypněte optimalizaci baterie</b> pro <xliff:g id="appName">SimpleX</xliff:g> v dalším dialogu. V opačném případě budou oznámení vypnuta.</string>
|
||||
<string name="periodic_notifications_desc">Aplikace pravidelně načítá nové zprávy - denně spotřebuje několik procent baterie. Aplikace nepoužívá push oznámení - data ze zařízení nejsou odesílána na servery.</string>
|
||||
<string name="enter_passphrase_notification_title">Je vyžadována přístupová fráze</string>
|
||||
<string name="enter_passphrase_notification_desc">Chcete-li dostávat oznámení, zadejte přístupovou frázi do databáze.</string>
|
||||
<string name="database_initialization_error_title">Nelze inicializovat databázi</string>
|
||||
<string name="hide_notification">Skrýt</string>
|
||||
<string name="ntf_channel_calls">Volání SimpleX Chat</string>
|
||||
<string name="notification_preview_new_message">nová zpráva</string>
|
||||
<string name="notification_new_contact_request">Žádost o nový kontakt</string>
|
||||
<string name="delete_message_cannot_be_undone_warning">Zpráva bude smazána - nelze to vzít zpět!</string>
|
||||
<string name="confirm_verb">Potvrdit</string>
|
||||
<string name="send_us_an_email">Pošlete nám e-mail</string>
|
||||
<string name="chat_lock">Zámek SimpleX</string>
|
||||
<string name="install_simplex_chat_for_terminal">Instalace <xliff:g id="appNameFull">SimpleX Chat</xliff:g> pro terminál</string>
|
||||
<string name="star_on_github">Hvězda na GitHubu</string>
|
||||
<string name="rate_the_app">Ohodnoťte aplikaci</string>
|
||||
<string name="your_SMP_servers">Vaše servery SMP</string>
|
||||
<string name="network_disable_socks_info">Pokud potvrdíte, budou servery pro zasílání zpráv vidět vaši IP adresu a váš poskytovatel - ke kterým serverům se připojujete.</string>
|
||||
<string name="colored">barevné</string>
|
||||
<string name="secret">secret</string>
|
||||
<string name="callstatus_calling">volání…</string>
|
||||
<string name="callstate_connected">připojeno</string>
|
||||
<string name="callstate_ended">ukončeno</string>
|
||||
<string name="next_generation_of_private_messaging">Nová generace soukromých zpráv</string>
|
||||
<string name="people_can_connect_only_via_links_you_share">Lidé se s vámi mohou spojit pouze prostřednictvím odkazů, které sdílíte.</string>
|
||||
<string name="integrity_msg_bad_hash">špatný hash zprávy</string>
|
||||
<string name="chat_database_imported">Importovaná databáze chatu</string>
|
||||
<string name="new_passphrase">Nová přístupová fráze…</string>
|
||||
<string name="save_passphrase_and_open_chat">Uložte heslo a otevřete chat</string>
|
||||
<string name="chat_archive_section">ARCHIV CHATU</string>
|
||||
<string name="no_contacts_selected">Nebyl vybrán žádný kontakt</string>
|
||||
<string name="invite_prohibited_description">Snažíte se pozvat kontakt, se kterým jste sdíleli inkognito profil, do skupiny, ve které používáte svůj hlavní profil</string>
|
||||
<string name="info_row_group">Skupina</string>
|
||||
<string name="network_options_revert">Vrátit</string>
|
||||
<string name="updating_settings_will_reconnect_client_to_all_servers">Aktualizací nastavení se klient znovu připojí ke všem serverům.</string>
|
||||
<string name="accept_feature_set_1_day">Nastavit 1 den</string>
|
||||
<string name="connection_error_auth">Chyba spojení (AUTH)</string>
|
||||
<string name="sender_may_have_deleted_the_connection_request">Odesílatel možná smazal požadavek připojení</string>
|
||||
<string name="error_smp_test_server_auth">Server vyžaduje autorizaci pro vytváření front, zkontrolujte heslo</string>
|
||||
<string name="smp_server_test_delete_queue">Odstranit frontu</string>
|
||||
<string name="delete_group_menu_action">Smazat</string>
|
||||
<string name="delete_pending_connection__question">Smazat čekající připojení\?</string>
|
||||
<string name="icon_descr_settings">Nastavení</string>
|
||||
<string name="image_descr_qr_code">QR kód</string>
|
||||
<string name="show_QR_code_for_your_contact_to_scan_from_the_app__multiline">Váš kontakt může z aplikace naskenovat QR kód.</string>
|
||||
<string name="if_you_cannot_meet_in_person_show_QR_in_video_call_or_via_another_channel">Pokud se nemůžete setkat osobně, ukažte <b>ve videohovoru QR kód</b> nebo sdílejte odkaz.</string>
|
||||
<string name="scan_code">Skenovat kód</string>
|
||||
<string name="incorrect_code">Nesprávný bezpečnostní kód!</string>
|
||||
<string name="scan_code_from_contacts_app">Naskenujte bezpečnostní kód z aplikace vašeho kontaktu.</string>
|
||||
<string name="mark_code_verified">Označit jako ověřený</string>
|
||||
<string name="clear_verification">Zrušte ověření</string>
|
||||
<string name="to_verify_compare">Chcete-li ověřit koncové šifrování u svého kontaktu, porovnejte (nebo naskenujte) kód na svých zařízeních.</string>
|
||||
<string name="your_settings">Vaše nastavení</string>
|
||||
<string name="your_simplex_contact_address">Vaše <xliff:g id="appName">Adresa kontaktu SimpleX</xliff:g></string>
|
||||
<string name="database_passphrase_and_export">Databázová hesla a export</string>
|
||||
<string name="your_chat_profiles">Vaše profily v chatu</string>
|
||||
<string name="chat_with_the_founder">Zasílání otázek a nápadů</string>
|
||||
<string name="smp_servers_test_server">Testovací server</string>
|
||||
<string name="enter_one_ICE_server_per_line">Servery ICE (jeden na řádek)</string>
|
||||
<string name="network_use_onion_hosts_required_desc">Pro připojení budou vyžadováni Onion hostitelé.</string>
|
||||
<string name="update_network_session_mode_question">Aktualizovat režim izolace\?</string>
|
||||
<string name="app_version_code">Sestavení aplikace: %s</string>
|
||||
<string name="you_can_share_your_address_anybody_will_be_able_to_connect">Můžete sdílet svou adresu jako odkaz nebo jako QR kód - kdokoli se k vám bude moci připojit. O své kontakty nepřijdete, pokud ji později smažete.</string>
|
||||
<string name="share_link">Sdílet odkaz</string>
|
||||
<string name="delete_address">Odstranit adresu</string>
|
||||
<string name="full_name__field">Celé jméno:</string>
|
||||
<string name="your_current_profile">Váš současný profil</string>
|
||||
<string name="to_preserve_privacy_simplex_has_background_service_instead_of_push_notifications_it_uses_a_few_pc_battery">Pro zachování soukromí má aplikace místo push oznámení <b><xliff:g id="appName">SimpleX</xliff:g> službu na pozadí</b> - denně spotřebuje několik procent baterie.</string>
|
||||
<string name="periodic_notifications">Pravidelná oznámení</string>
|
||||
<string name="simplex_service_notification_title"><xliff:g id="appNameFull">SimpleX Chat</xliff:g> služba</string>
|
||||
<string name="simplex_service_notification_text">Příjem zpráv…</string>
|
||||
<string name="ntf_channel_messages">Zprávy SimpleX Chat</string>
|
||||
<string name="settings_notification_preview_mode_title">Zobrazení náhledu</string>
|
||||
<string name="settings_notification_preview_title">Náhled oznámení</string>
|
||||
<string name="notifications_mode_off">Spustí se při otevření aplikace</string>
|
||||
<string name="notifications_mode_periodic">Spouští se pravidelně</string>
|
||||
<string name="notifications_mode_service">Vždy zapnuto</string>
|
||||
<string name="notifications_mode_periodic_desc">Kontroluje nové zprávy každých 10 minut po dobu až 1 minuty</string>
|
||||
<string name="notification_preview_mode_contact_desc">Zobrazit pouze kontakt</string>
|
||||
<string name="notification_preview_somebody">Skrytý kontakt:</string>
|
||||
<string name="la_notice_turn_on">Zapněte funkci</string>
|
||||
<string name="auth_simplex_lock_turned_on">Zapnutý zámek SimpleX Lock</string>
|
||||
<string name="auth_unlock">Odemknutí stránky</string>
|
||||
<string name="auth_device_authentication_is_not_enabled_you_can_turn_on_in_settings_once_enabled">Ověřování zařízení není povoleno. Jakmile povolíte ověřování zařízení, můžete zámek SimpleX Lock zapnout prostřednictvím Nastavení.</string>
|
||||
<string name="auth_device_authentication_is_disabled_turning_off">Ověřování zařízení je zakázáno. Vypnutí zámku SimpleX Lock.</string>
|
||||
<string name="edit_verb">Upravit</string>
|
||||
<string name="delete_verb">Smazat</string>
|
||||
<string name="for_everybody">Pro všechny</string>
|
||||
<string name="icon_descr_sent_msg_status_sent">odesláno</string>
|
||||
<string name="contact_connection_pending">připojení…</string>
|
||||
<string name="images_limit_desc">Současně lze odeslat pouze 10 obrázků</string>
|
||||
<string name="image_decoding_exception_title">Chyba dekódování</string>
|
||||
<string name="image_saved">Obrázek uložen do galerie</string>
|
||||
<string name="icon_descr_file">Soubor</string>
|
||||
<string name="large_file">Velký soubor!</string>
|
||||
<string name="file_will_be_received_when_contact_is_online">Soubor bude přijat, až bude váš kontakt online, vyčkejte prosím nebo se podívejte později!</string>
|
||||
<string name="file_saved">Soubor uložen</string>
|
||||
<string name="file_not_found">Soubor nebyl nalezen</string>
|
||||
<string name="voice_message_with_duration">Hlasová zpráva (<xliff:g id="duration">%1$s</xliff:g>)</string>
|
||||
<string name="delete_contact_all_messages_deleted_cannot_undo_warning">Kontakt a všechny zprávy budou smazány - nelze to vzít zpět!</string>
|
||||
<string name="button_delete_contact">Smazat kontakt</string>
|
||||
<string name="text_field_set_contact_placeholder">Nastavení jména kontaktu…</string>
|
||||
<string name="view_security_code">Zobrazení bezpečnostního kódu</string>
|
||||
<string name="icon_descr_record_voice_message">Nahrát hlasovou zprávu</string>
|
||||
<string name="voice_messages_prohibited">Hlasové zprávy jsou zakázány!</string>
|
||||
<string name="ask_your_contact_to_enable_voice">Prosím, požádejte kontaktní osobu, aby umožnila odesílání hlasových zpráv.</string>
|
||||
<string name="send_live_message_desc">Poslat živou zprávu - zpráva se bude aktualizovat pro příjemce během psaní.</string>
|
||||
<string name="share_one_time_link">Vytvořit jednorázovou pozvánku</string>
|
||||
<string name="scan_QR_code">Skenovat QR kód</string>
|
||||
<string name="connect_via_link_or_qr_from_clipboard_or_in_person">( skenovat nebo vložit ze schránky)</string>
|
||||
<string name="edit_image">Upravit obrázek</string>
|
||||
<string name="delete_image">Smazat obrázek</string>
|
||||
<string name="callstatus_error">chyba volání</string>
|
||||
<string name="opensource_protocol_and_code_anybody_can_run_servers">Protokol a kód s otevřeným zdrojovým kódem - servery může provozovat kdokoli.</string>
|
||||
<string name="create_your_profile">Vytvořte si svůj profil</string>
|
||||
<string name="make_private_connection">Vytvořte si soukromé připojení</string>
|
||||
<string name="encrypted_video_call">Videohovor šifrovaný e2e</string>
|
||||
<string name="encrypted_audio_call">e2e šifrovaný audio hovor</string>
|
||||
<string name="status_contact_has_e2e_encryption">kontakt má šifrování e2e</string>
|
||||
<string name="status_contact_has_no_e2e_encryption">kontakt nemá šifrování e2e</string>
|
||||
<string name="call_connection_peer_to_peer">peer-to-peer</string>
|
||||
<string name="call_connection_via_relay">přes relé</string>
|
||||
<string name="icon_descr_hang_up">Zavěsit</string>
|
||||
<string name="icon_descr_video_off">Video vypnuto</string>
|
||||
<string name="icon_descr_video_on">Video zapnuto</string>
|
||||
<string name="icon_descr_audio_off">Zvuk vypnutý</string>
|
||||
<string name="integrity_msg_bad_id">špatné ID zprávy</string>
|
||||
<string name="integrity_msg_duplicate">duplicitní zpráva</string>
|
||||
<string name="alert_title_skipped_messages">Přeskočené zprávy</string>
|
||||
<string name="privacy_and_security">Ochrana osobních údajů a zabezpečení</string>
|
||||
<string name="your_privacy">Vaše soukromí</string>
|
||||
<string name="protect_app_screen">Ochrana obrazovky aplikace</string>
|
||||
<string name="send_link_previews">Odesílání náhledů odkazů</string>
|
||||
<string name="full_backup">Zálohování dat aplikace</string>
|
||||
<string name="confirm_new_passphrase">Potvrdit novou heslovou frázi…</string>
|
||||
<string name="error_with_info">Chyba: %s</string>
|
||||
<string name="leave_group_question">Opustit skupinu\?</string>
|
||||
<string name="icon_descr_group_inactive">Skupina je neaktivní</string>
|
||||
<string name="rcv_group_event_member_left">left</string>
|
||||
<string name="clear_contacts_selection_button">Vymazat</string>
|
||||
<string name="switch_verb">Přepnout</string>
|
||||
<string name="member_role_will_be_changed_with_notification">Role bude změněna na \"%s\". Všichni ve skupině budou informováni.</string>
|
||||
<string name="error_removing_member">Chyba při odebrání člena</string>
|
||||
<string name="error_saving_group_profile">Chyba při ukládání profilu skupiny</string>
|
||||
<string name="network_option_seconds_label">sec</string>
|
||||
<string name="incognito_info_allows">Umožňuje mít v jednom profilu chatu mnoho anonymních spojení bez jakýchkoli sdílených údajů mezi nimi.</string>
|
||||
<string name="incognito_info_share">Pokud s někým sdílíte inkognito profil, bude tento profil použit pro skupiny, do kterých vás pozve.</string>
|
||||
<string name="theme_system">Systém</string>
|
||||
<string name="voice_messages">Hlasové zprávy</string>
|
||||
<string name="both_you_and_your_contacts_can_delete">Vy i váš kontakt můžete nevratně mazat odeslané zprávy.</string>
|
||||
<string name="ttl_m">\"%dm</string>
|
||||
<string name="ttl_mth">\"%dmth</string>
|
||||
<string name="ttl_hours">\"%d hodin</string>
|
||||
<string name="ttl_h">\"%dh</string>
|
||||
<string name="ttl_d">\"%dd</string>
|
||||
<string name="v4_2_security_assessment">Posouzení bezpečnosti</string>
|
||||
<string name="v4_2_security_assessment_desc">Bezpečnost SimpleX Chat byla prověřena společností Trail of Bits.</string>
|
||||
<string name="v4_3_irreversible_message_deletion">Nevratné mazání zpráv</string>
|
||||
<string name="v4_3_improved_server_configuration">Vylepšená konfigurace serveru</string>
|
||||
<string name="v4_3_improved_privacy_and_security">Vylepšená ochrana soukromí a zabezpečení</string>
|
||||
<string name="v4_3_improved_privacy_and_security_desc">Skrytí obrazovky aplikace v posledních aplikacích.</string>
|
||||
<string name="v4_4_disappearing_messages_desc">Odeslané zprávy se po uplynutí nastavené doby odstraní.</string>
|
||||
<string name="v4_4_live_messages">Živé zprávy</string>
|
||||
<string name="v4_4_live_messages_desc">Příjemci uvidí aktualizace během jejich psaní.</string>
|
||||
<string name="v4_4_verify_connection_security">Ověření zabezpečení připojení</string>
|
||||
<string name="v4_4_french_interface">Francouzské rozhraní</string>
|
||||
<string name="v4_4_french_interface_descr">Díky uživatelům - přispívejte prostřednictvím Weblate!</string>
|
||||
<string name="v4_5_multiple_chat_profiles">Více chatovacích profilů</string>
|
||||
<string name="v4_5_multiple_chat_profiles_descr">Různá jména, avatary a dopravní izolace.</string>
|
||||
<string name="v4_5_message_draft">Návrh zprávy</string>
|
||||
<string name="v4_5_message_draft_descr">Zachování posledního návrhu zprávy s přílohami.</string>
|
||||
<string name="v4_5_transport_isolation">Izolace transportu</string>
|
||||
<string name="v4_5_transport_isolation_descr">Podle profilu chatu (výchozí) nebo podle připojení (BETA).</string>
|
||||
<string name="you_will_join_group">Připojíte se ke skupině, na kterou tento odkaz odkazuje, a spojíte se s jejími členy.</string>
|
||||
<string name="connect_via_link_verb">Připojení</string>
|
||||
<string name="connected_to_server_to_receive_messages_from_contact">Jste připojeni k serveru, který se používá k přijímání zpráv od tohoto kontaktu.</string>
|
||||
<string name="trying_to_connect_to_server_to_receive_messages_with_error">Pokoušíte se připojit k serveru používanému pro příjem zpráv od tohoto kontaktu (chyba: <xliff:g id="errorMsg">%1$s</xliff:g>).</string>
|
||||
<string name="marked_deleted_description">označeno jako smazáno</string>
|
||||
<string name="sending_files_not_yet_supported">Odesílání souborů zatím není podporováno</string>
|
||||
<string name="receiving_files_not_yet_supported">přijímání souborů zatím není podporováno</string>
|
||||
<string name="sender_you_pronoun">ty</string>
|
||||
<string name="unknown_message_format">neznámý formát zprávy</string>
|
||||
<string name="invalid_message_format">neplatný formát zprávy</string>
|
||||
<string name="live">ŽIVĚ</string>
|
||||
<string name="description_via_group_link_incognito">inkognito přes skupinový odkaz</string>
|
||||
<string name="ensure_smp_server_address_are_correct_format_and_unique">Ujistěte se, že adresy serverů SMP jsou ve správném formátu, oddělené na řádcích a nejsou duplicitní.</string>
|
||||
<string name="failed_to_create_user_title">Chyba při vytváření profilu!</string>
|
||||
<string name="failed_to_create_user_duplicate_title">Duplicitní zobrazované jméno!</string>
|
||||
<string name="failed_to_active_user_title">Chyba při přepínání profilu!</string>
|
||||
<string name="error_joining_group">Chyba při připojování ke skupině</string>
|
||||
<string name="cannot_receive_file">Nelze přijmout soubor</string>
|
||||
<string name="sender_cancelled_file_transfer">Odesílatel zrušil přenos souboru.</string>
|
||||
<string name="error_receiving_file">Chyba při příjmu souboru</string>
|
||||
<string name="error_creating_address">Chyba při vytváření adresy</string>
|
||||
<string name="please_check_correct_link_and_maybe_ask_for_a_new_one">Zkontrolujte, zda jste použili správný odkaz, nebo požádejte kontakt, aby vám poslal jiný.</string>
|
||||
<string name="connection_error_auth_desc">Pokud váš kontakt spojení nesmazal nebo tento odkaz již byl použit, může se jednat o chybu - nahlaste ji prosím. Chcete-li se připojit, požádejte prosím svůj kontakt o vytvoření jiného odkazu pro připojení a zkontrolujte, zda máte stabilní připojení k síti.</string>
|
||||
<string name="error_deleting_contact">Chyba mazání kontaktu</string>
|
||||
<string name="error_deleting_group">Chyba mazání skupiny</string>
|
||||
<string name="error_deleting_contact_request">Chyba mazání žádosti kontaktu</string>
|
||||
<string name="error_deleting_pending_contact_connection">Chyba mazání probíhajícího připojení kontaktu</string>
|
||||
<string name="error_smp_test_failed_at_step">Test selhal v kroku %s.</string>
|
||||
<string name="error_smp_test_certificate">Je možné, že otisk certifikátu v adrese serveru je nesprávný.</string>
|
||||
<string name="smp_server_test_connect">Připojit</string>
|
||||
<string name="smp_server_test_disconnect">Odpojit</string>
|
||||
<string name="error_deleting_user">Chyba mazání uživatelského profilu</string>
|
||||
<string name="icon_descr_instant_notifications">Okamžitá oznámení</string>
|
||||
<string name="service_notifications_disabled">Okamžitá oznámení jsou vypnutá!</string>
|
||||
<string name="turning_off_service_and_periodic">Je aktivní optimalizace baterie, která vypíná službu na pozadí a pravidelné požadavky na nové zprávy. Můžete je znovu povolit prostřednictvím nastavení.</string>
|
||||
<string name="periodic_notifications_disabled">Pravidelná oznámení jsou vypnuta!</string>
|
||||
<string name="database_initialization_error_desc">Databáze nefunguje správně. Klepnutím na se dozvíte více</string>
|
||||
<string name="notifications_mode_off_desc">Aplikace může přijímat oznámení pouze při svém spuštění, žádná služba na pozadí se nespustí</string>
|
||||
<string name="notification_display_mode_hidden_desc">Skrýt kontakt a zprávu</string>
|
||||
<string name="la_notice_to_protect_your_information_turn_on_simplex_lock_you_will_be_prompted_to_complete_authentication_before_this_feature_is_enabled">Chcete-li chránit své informace, zapněte zámek SimpleX Lock. Před zapnutím této funkce budete vyzváni k dokončení ověření.</string>
|
||||
<string name="auth_you_will_be_required_to_authenticate_when_you_start_or_resume">Při spuštění nebo obnovení aplikace po 30 sekundách na pozadí budete vyzváni k ověření.</string>
|
||||
<string name="auth_disable_simplex_lock">Vypnutí zámku SimpleX</string>
|
||||
<string name="auth_confirm_credential">Potvrďte své pověření</string>
|
||||
<string name="auth_unavailable">Ověřování není k dispozici</string>
|
||||
<string name="auth_stop_chat">Zastavení chatu</string>
|
||||
<string name="auth_open_chat_console">Otevřete konzolu chatu</string>
|
||||
<string name="message_delivery_error_title">Chyba doručení zprávy</string>
|
||||
<string name="message_delivery_error_desc">Tento kontakt s největší pravděpodobností smazal spojení s vámi.</string>
|
||||
<string name="save_verb">Uložit</string>
|
||||
<string name="reveal_verb">Odhalit</string>
|
||||
<string name="hide_verb">Skrýt</string>
|
||||
<string name="delete_message__question">Smazat zprávu\?</string>
|
||||
<string name="delete_message_mark_deleted_warning">Zpráva bude označena ke smazání. Příjemce (příjemci) bude moci tuto zprávu odhalit.</string>
|
||||
<string name="for_me_only">Smazat pro mě</string>
|
||||
<string name="icon_descr_edited">upraveno</string>
|
||||
<string name="icon_descr_sent_msg_status_unauthorized_send">neautorizované odeslání</string>
|
||||
<string name="group_preview_you_are_invited">jste pozváni do skupiny</string>
|
||||
<string name="group_preview_join_as">připojit jako %s</string>
|
||||
<string name="group_connection_pending">připojuje se…</string>
|
||||
<string name="tap_to_start_new_chat">Klepnutím na zahájíte nový chat</string>
|
||||
<string name="chat_with_developers">Chatujte s vývojáři</string>
|
||||
<string name="you_have_no_chats">Nemáte žádné konverzace</string>
|
||||
<string name="icon_descr_cancel_image_preview">Zrušit náhled obrázku</string>
|
||||
<string name="share_message">Sdílet zprávu…</string>
|
||||
<string name="share_image">Sdílet obrázek…</string>
|
||||
<string name="icon_descr_cancel_file_preview">Zrušit náhled souboru</string>
|
||||
<string name="images_limit_title">Příliš mnoho obrázků!</string>
|
||||
<string name="waiting_for_file">Čekání na soubor</string>
|
||||
<string name="notifications">Oznámení</string>
|
||||
<string name="delete_contact_question">Smazat kontakt\?</string>
|
||||
<string name="icon_descr_server_status_pending">Čeká na vyřízení</string>
|
||||
<string name="verify_security_code">Ověření bezpečnostního kódu</string>
|
||||
<string name="icon_descr_send_message">Odeslat zprávu</string>
|
||||
<string name="only_group_owners_can_enable_voice">Pouze majitelé skupin mohou povolit zasílání hlasových zpráv.</string>
|
||||
<string name="send_live_message">Odeslat živou zprávu</string>
|
||||
<string name="live_message">Živé zprávy!</string>
|
||||
<string name="connect_via_link_or_qr">Připojit se prostřednictvím odkazu / QR kódu</string>
|
||||
<string name="thank_you_for_installing_simplex">Děkujeme za instalaci <xliff:g id="appNameFull">SimpleX Chat</xliff:g>!</string>
|
||||
<string name="you_can_connect_to_simplex_chat_founder">Můžete se <font color="#0088ff">připojit k <xliff:g id="appNameFull">SimpleX Chat</xliff:g> vývojářům a položit jim případné dotazy a získat aktualizace</font>.</string>
|
||||
<string name="to_connect_via_link_title">K připojení prostřednictvím odkazu</string>
|
||||
<string name="if_you_received_simplex_invitation_link_you_can_open_in_browser">Pokud jste dostal <xliff:g id="appName">SimpleX Chat</xliff:g> zvací odkaz, Můžete ho otevřít v prohlížeči:</string>
|
||||
<string name="if_you_choose_to_reject_the_sender_will_not_be_notified">Pokud zvolíte odmítnutí, odesílatel NEBUDE upozorněn.</string>
|
||||
<string name="mobile_tap_open_in_mobile_app_then_tap_connect_in_app">📱 mobilní telefon: tap <b>Otevřete v mobilní aplikaci</b>, potom klikněte <b>Připojit</b>.</string>
|
||||
<string name="reject_contact_button">Odmítnout</string>
|
||||
<string name="clear_chat_button">Čistá konverzace</string>
|
||||
<string name="clear_chat_menu_action">Čistý</string>
|
||||
<string name="delete_contact_menu_action">Smazat</string>
|
||||
<string name="set_contact_name">Nastavit jméno kontaktu</string>
|
||||
<string name="you_accepted_connection">Přijali jste spojení</string>
|
||||
<string name="alert_text_connection_pending_they_need_to_be_online_can_delete_and_retry">Aby se připojení dokončilo, musí být váš kontakt online. Toto připojení můžete zrušit a kontakt odebrat (a zkusit to později s novým odkazem).</string>
|
||||
<string name="contact_wants_to_connect_with_you">Chce se s vámi spojit!</string>
|
||||
<string name="icon_descr_profile_image_placeholder">Zástupce profilového obrázku</string>
|
||||
<string name="image_descr_profile_image">profilový obrázek</string>
|
||||
<string name="icon_descr_close_button">Tlačítko Zavřít</string>
|
||||
<string name="alert_title_contact_connection_pending">Kontakt ještě není připojen!</string>
|
||||
<string name="image_descr_link_preview">náhledový obrázek odkazu</string>
|
||||
<string name="icon_descr_cancel_link_preview">Zrušit náhled odkazu</string>
|
||||
<string name="image_descr_simplex_logo"><xliff:g id="appName">SimpleX</xliff:g> Logo</string>
|
||||
<string name="icon_descr_email">E-mail</string>
|
||||
<string name="icon_descr_more_button">Více na</string>
|
||||
<string name="show_QR_code">Zobrazit QR kód</string>
|
||||
<string name="invalid_QR_code">Neplatný QR kód</string>
|
||||
<string name="this_QR_code_is_not_a_link">Tento QR kód není odkaz!</string>
|
||||
<string name="invalid_contact_link">Neplatný odkaz!</string>
|
||||
<string name="this_link_is_not_a_valid_connection_link">Tento odkaz není platným odkazem pro připojení!</string>
|
||||
<string name="if_you_cannot_meet_in_person_scan_QR_in_video_call_or_ask_for_invitation_link">Pokud se nemůžete setkat osobně, můžete <b>skenovat QR kód ve videohovoru</b> nebo může váš kontakt sdílet pozvánku.</string>
|
||||
<string name="connect_via_link">Připojte se prostřednictvím odkazu</string>
|
||||
<string name="connect_button">Připojit</string>
|
||||
<string name="paste_button">Vložit</string>
|
||||
<string name="this_string_is_not_a_connection_link">Tento řetězec není odkazem na připojení!</string>
|
||||
<string name="you_can_also_connect_by_clicking_the_link">Můžete se také připojit kliknutím na odkaz. Pokud se otevře v prohlížeči, klikněte na tlačítko <b>Otevřít v mobilní aplikaci</b>.</string>
|
||||
<string name="is_not_verified">\"%s není ověřeno</string>
|
||||
<string name="how_to_use_simplex_chat">Jak ji používat</string>
|
||||
<string name="markdown_help">Nápověda k markdown</string>
|
||||
<string name="smp_servers_save">Uložit servery</string>
|
||||
<string name="markdown_in_messages">Markdown ve zprávách</string>
|
||||
<string name="smp_servers_test_servers">Testovací servery</string>
|
||||
<string name="smp_servers_preset_server">Přednastavený server</string>
|
||||
<string name="smp_servers_your_server">Váš server</string>
|
||||
<string name="smp_servers_your_server_address">Adresa vašeho serveru</string>
|
||||
<string name="smp_servers_use_server">Použít server</string>
|
||||
<string name="smp_servers_use_server_for_new_conn">Použít pro nová připojení</string>
|
||||
<string name="smp_servers_per_user">Servery pro nová připojení vašeho aktuálního profilu chatu.</string>
|
||||
<string name="use_simplex_chat_servers__question">Použít <xliff:g id="appNameFull">SimpleX Chat</xliff:g> servery\?</string>
|
||||
<string name="using_simplex_chat_servers">Použití <xliff:g id="appNameFull">SimpleX Chat</xliff:g> serverů.</string>
|
||||
<string name="saved_ICE_servers_will_be_removed">Uložené servery WebRTC ICE budou odstraněny.</string>
|
||||
<string name="error_saving_ICE_servers">Chyba při ukládání serverů ICE</string>
|
||||
<string name="ensure_ICE_server_address_are_correct_format_and_unique">Ujistěte se, že adresy serverů WebRTC ICE jsou ve správném formátu, oddělené na řádcích a nejsou duplicitní.</string>
|
||||
<string name="save_servers_button">Uložit</string>
|
||||
<string name="network_and_servers">Síť a servery</string>
|
||||
<string name="network_socks_toggle">Použít proxy server SOCKS (port 9050)</string>
|
||||
<string name="update_onion_hosts_settings_question">Aktualizovat nastavení hostitelů .onion\?</string>
|
||||
<string name="network_use_onion_hosts">Použít hostitele .onion</string>
|
||||
<string name="network_use_onion_hosts_prefer">Když je k dispozici</string>
|
||||
<string name="network_use_onion_hosts_required">Povinné</string>
|
||||
<string name="network_use_onion_hosts_prefer_desc">Onion hostitelé se použijí, pokud jsou k dispozici.</string>
|
||||
<string name="network_use_onion_hosts_no_desc">Onion hostitelé nebudou použiti.</string>
|
||||
<string name="network_use_onion_hosts_prefer_desc_in_alert">Onion hostitelé budou použiti, pokud budou k dispozici.</string>
|
||||
<string name="network_use_onion_hosts_required_desc_in_alert">Pro připojení budou vyžadováni Onion hostitelé.</string>
|
||||
<string name="network_session_mode_transport_isolation">Izolace přenosu</string>
|
||||
<string name="network_session_mode_user_description">A separate TCP connection (and SOCKS credential) will be used <b>for each chat profile you have in the app</b>.</string>
|
||||
<string name="network_session_mode_entity_description">Oddělit TCP připojení (a SOCKS pověření) bude použito <b>pro všechny kontakty a členy skupin</b>. <b>Upozornění</b>: Pokud máte mnoho připojení, může být spotřeba baterie a provoz podstatně vyšší a některá připojení mohou selhat.</string>
|
||||
<string name="appearance_settings">Vzhled</string>
|
||||
<string name="app_version_title">Verze aplikace</string>
|
||||
<string name="app_version_name">Verze aplikace: v%s</string>
|
||||
<string name="core_version">Verze jádra: v%s</string>
|
||||
<string name="core_build_timestamp">Jádro sestaveno na: %s</string>
|
||||
<string name="delete_address__question">Smazat adresu\?</string>
|
||||
<string name="all_your_contacts_will_remain_connected">Všechny vaše kontakty zůstanou připojeny.</string>
|
||||
<string name="contact_requests">Žádosti o kontakt</string>
|
||||
<string name="display_name__field">Zobrazované jméno:</string>
|
||||
<string name="your_profile_is_stored_on_device_and_shared_only_with_contacts_simplex_cannot_see_it">Váš profil je uložen v zařízení a je sdílen pouze s vašimi kontakty. <xliff:g id="appName">SimpleX</xliff:g> servery váš profil nevidí.</string>
|
||||
<string name="save_preferences_question">Uložit předvolby\?</string>
|
||||
<string name="save_and_notify_contact">Uložit a upozornit kontakt</string>
|
||||
<string name="save_and_notify_contacts">Uložit a upozornit kontakty</string>
|
||||
<string name="you_control_your_chat">Chat máte pod kontrolou!</string>
|
||||
<string name="we_do_not_store_contacts_or_messages_on_servers">Na serverech neukládáme žádné vaše kontakty ani zprávy (po doručení).</string>
|
||||
<string name="your_profile_is_stored_on_your_device">Váš profil, kontakty a doručené zprávy jsou uloženy ve vašem zařízení.</string>
|
||||
<string name="display_name">Zobrazované jméno</string>
|
||||
<string name="full_name_optional__prompt">Celé jméno (volitelné)</string>
|
||||
<string name="create_profile_button">Vytvořit</string>
|
||||
<string name="how_to_use_markdown">Jak používat markdown</string>
|
||||
<string name="you_can_use_markdown_to_format_messages__prompt">K formátování zpráv můžete použít markdown:</string>
|
||||
<string name="italic">kurzíva</string>
|
||||
<string name="strikethrough">přeškrtnout</string>
|
||||
<string name="callstatus_missed">zmeškané volání</string>
|
||||
<string name="callstatus_rejected">odmítnutý hovor</string>
|
||||
<string name="callstatus_connecting">spojovací hovor…</string>
|
||||
<string name="callstatus_ended">hovor ukončen <xliff:g id="duration" example="01:15">%1$s</xliff:g></string>
|
||||
<string name="callstate_starting">začíná…</string>
|
||||
<string name="callstate_waiting_for_answer">čeká na odpověď…</string>
|
||||
<string name="callstate_waiting_for_confirmation">čekání na potvrzení…</string>
|
||||
<string name="callstate_received_answer">obdržel odpověď…</string>
|
||||
<string name="callstate_received_confirmation">obdržel potvrzení…</string>
|
||||
<string name="callstate_connecting">připojení…</string>
|
||||
<string name="privacy_redefined">Nové vymezení soukromí</string>
|
||||
<string name="first_platform_without_user_ids">1. Platforma bez identifikátorů uživatelů - soukromá už od záměru.</string>
|
||||
<string name="immune_to_spam_and_abuse">Odolná vůči spamu a zneužití</string>
|
||||
<string name="to_protect_privacy_simplex_has_ids_for_queues">K ochraně soukromí, místo uživatelských ID používaných všemi ostatními platformami, <xliff:g id="appName">SimpleX</xliff:g> má identifikátory pro fronty zpráv, zvlášť pro každý z vašich kontaktů.</string>
|
||||
<string name="many_people_asked_how_can_it_deliver">Mnoho lidí se ptalo: <i>if <xliff:g id="appName">SimpleX</xliff:g> nemá žádné identifikátory uživatelů, jak může doručovat zprávy\?</i></string>
|
||||
<string name="you_control_servers_to_receive_your_contacts_to_send">Vy kontrolujete přez který server(y) <b>přijímat</b> zprávy, vaše kontakty - servery které používáte pro konverzaci.</string>
|
||||
<string name="read_more_in_github">Další informace najdete v našem repozitáři GitHub.</string>
|
||||
<string name="read_more_in_github_with_link">Další informace najdete v našem <font color="#0088ff">úložišti GitHub</font>.</string>
|
||||
<string name="use_chat">Konverzovat</string>
|
||||
<string name="onboarding_notifications_mode_subtitle">Lze změnit později v nastavení.</string>
|
||||
<string name="onboarding_notifications_mode_off">Když je aplikace spuštěná</string>
|
||||
<string name="onboarding_notifications_mode_service">Okamžitý</string>
|
||||
<string name="onboarding_notifications_mode_off_desc"><b>Nejlepší pro baterii</b>. Budete přijímat oznámení pouze když aplikace běží, služba na pozadí NEBUDE použita.</string>
|
||||
<string name="onboarding_notifications_mode_periodic_desc"><Dobré pro baterii</b>. Služba na pozadí kontroluje nové zprávy každých 10 minut. Můžete zmeškat hovory a naléhavé zprávy.</string>
|
||||
<string name="onboarding_notifications_mode_service_desc"><b>Využívá více baterie</b>! Služba na pozadí je spuštěna vždy - oznámení se zobrazí, jakmile jsou zprávy k dispozici.</string>
|
||||
<string name="paste_the_link_you_received">Vložení přijatého odkazu</string>
|
||||
<string name="incoming_video_call">Příchozí videohovor</string>
|
||||
<string name="incoming_audio_call">Příchozí zvukový hovor</string>
|
||||
<string name="contact_wants_to_connect_via_call"><xliff:g id="contactName" example="Alice">%1$s</xliff:g> se s vámi chce spojit prostřednictvím</string>
|
||||
<string name="video_call_no_encryption">videohovoru (nešifrovaného e2e).</string>
|
||||
<string name="audio_call_no_encryption">zvukový hovor (nešifrovaný e2e)</string>
|
||||
<string name="reject">Odmítnout</string>
|
||||
<string name="your_calls">Vaše hovory</string>
|
||||
<string name="connect_calls_via_relay">Spojení přes relé</string>
|
||||
<string name="show_call_on_lock_screen">Zobrazit</string>
|
||||
<string name="no_call_on_lock_screen">Zakázat</string>
|
||||
<string name="your_ice_servers">Vaše servery ICE</string>
|
||||
<string name="webrtc_ice_servers">WebRTC servery ICE</string>
|
||||
<string name="status_no_e2e_encryption">bez šifrování e2e</string>
|
||||
<string name="icon_descr_flip_camera">Flipová kamera</string>
|
||||
<string name="icon_descr_call_pending_sent">Probíhající hovor</string>
|
||||
<string name="icon_descr_call_missed">Zmeškaný hovor</string>
|
||||
<string name="icon_descr_call_rejected">Odmítnutý hovor</string>
|
||||
<string name="icon_descr_call_connecting">Spojení hovoru</string>
|
||||
<string name="icon_descr_call_ended">Ukončený hovor</string>
|
||||
<string name="answer_call">Přijmout hovor</string>
|
||||
<string name="integrity_msg_skipped"><xliff:g id="connection ID" example="1">%1$d</xliff:g> přeskočená zpráva (zprávy)</string>
|
||||
<string name="alert_text_skipped_messages_it_can_happen_when">Může se to stát, když: 1. Zprávy na serveru vyprší, pokud nebyly přijaty po dobu 30 dnů, 2. Server, který používáte pro příjem zpráv od tohoto kontaktu, byl aktualizován a restartován. 3. Spojení je narušeno. Připojte se k vývojářům prostřednictvím Nastavení, abyste mohli dostávat aktualizace o serverech. Budeme přidávat redundantní servery, abychom zabránili ztrátě zpráv.</string>
|
||||
<string name="settings_section_title_you">VY</string>
|
||||
<string name="settings_section_title_support">PODPORA SIMPLEX CHAT</string>
|
||||
<string name="settings_section_title_develop">DEVELOP</string>
|
||||
<string name="settings_developer_tools">Nástroje pro vývojáře</string>
|
||||
<string name="settings_section_title_incognito">Režim inkognito</string>
|
||||
<string name="your_chat_database">Vaše chatovací databáze</string>
|
||||
<string name="run_chat_section">SPUSTIT CHAT</string>
|
||||
<string name="chat_is_running">Chat je spuštěn</string>
|
||||
<string name="chat_is_stopped">Chat je zastaven</string>
|
||||
<string name="chat_database_section">CHAT DATABÁZE</string>
|
||||
<string name="database_passphrase">Heslo databáze</string>
|
||||
<string name="new_database_archive">Archiv nové databáze</string>
|
||||
<string name="old_database_archive">Archiv staré databáze</string>
|
||||
<string name="error_starting_chat">Chyba při spuštění chatu</string>
|
||||
<string name="stop_chat_question">Zastavit chat\?</string>
|
||||
<string name="stop_chat_to_export_import_or_delete_chat_database">Zastavení chatu pro export, import nebo smazání databáze chatu. Během zastavení chatu nebudete moci přijímat a odesílat zprávy.</string>
|
||||
<string name="stop_chat_confirmation">Zastavit</string>
|
||||
<string name="set_password_to_export">Nastavení přístupové fráze pro export</string>
|
||||
<string name="set_password_to_export_desc">Databáze je šifrována pomocí náhodné přístupové fráze. Před exportem ji změňte.</string>
|
||||
<string name="error_stopping_chat">Chyba při zastavení chatu</string>
|
||||
<string name="import_database_question">Importovat databázi chatu\?</string>
|
||||
<string name="your_current_chat_database_will_be_deleted_and_replaced_with_the_imported_one">Vaše aktuální databáze chatu bude Smazána a Nahrazena importovanou databází. Tuto akci nelze vzít zpět - váš profil, kontakty, zprávy a soubory budou nenávratně ztraceny.</string>
|
||||
<string name="error_deleting_database">Chyba při mazání databáze chatu</string>
|
||||
<string name="error_importing_database">Chyba při importu databáze chatu</string>
|
||||
<string name="chat_database_deleted">Databáze chatu odstraněna</string>
|
||||
<string name="delete_files_and_media_for_all_users">Odstranění souborů pro všechny profily chatu</string>
|
||||
<string name="delete_files_and_media_all">Odstranit všechny soubory</string>
|
||||
<string name="delete_files_and_media_desc">Tuto akci nelze vrátit zpět - všechny přijaté a odeslané soubory a média budou smazány. Obrázky s nízkým rozlišením zůstanou zachovány.</string>
|
||||
<string name="no_received_app_files">Žádné přijaté ani odeslané soubory</string>
|
||||
<string name="total_files_count_and_size">%d soubor(ů) s celkovou velikostí %s</string>
|
||||
<string name="chat_item_ttl_none">nikdy</string>
|
||||
<string name="chat_item_ttl_seconds">\"%s vteřin(y)</string>
|
||||
<string name="messages_section_title">Zprávy</string>
|
||||
<string name="messages_section_description">Toto nastavení se vztahuje na zprávy ve vašem aktuálním profilu chatu.</string>
|
||||
<string name="delete_messages_after">Smazat zprávy po</string>
|
||||
<string name="enable_automatic_deletion_question">Povolit automatické mazání zpráv\?</string>
|
||||
<string name="enable_automatic_deletion_message">Tuto akci nelze vzít zpět - zprávy odeslané a přijaté dříve, než bylo zvoleno, budou smazány. Může to trvat několik minut.</string>
|
||||
<string name="error_changing_message_deletion">Chyba změny nastavení</string>
|
||||
<string name="save_passphrase_in_keychain">Uložit přístupovou frázi do úložiště klíčů</string>
|
||||
<string name="database_encrypted">Databáze zašifrována!</string>
|
||||
<string name="error_encrypting_database">Chyba šifrování databáze</string>
|
||||
<string name="encrypt_database">Šifrovat</string>
|
||||
<string name="encrypted_with_random_passphrase">Databáze je zašifrována pomocí náhodné přístupové fráze, můžete ji změnit.</string>
|
||||
<string name="keychain_allows_to_receive_ntfs">K bezpečnému uložení přístupové fráze se použije úložiště klíčů Android, po restartování aplikace nebo změně přístupové fráze - umožní přijímání oznámení.</string>
|
||||
<string name="you_have_to_enter_passphrase_every_time">Musíte zadat přístupovou frázi při každém spuštění aplikace - není uložena v zařízení.</string>
|
||||
<string name="encrypt_database_question">Šifrovat databázi\?</string>
|
||||
<string name="change_database_passphrase_question">Změnit přístupovou frázi databáze\?</string>
|
||||
<string name="database_will_be_encrypted">Databáze bude zašifrována.</string>
|
||||
<string name="database_encryption_will_be_updated">Šifrovací heslová fráze databáze bude aktualizována a uložena do úložiště klíčů.</string>
|
||||
<string name="database_passphrase_will_be_updated">Šifrovací heslová fráze databáze bude aktualizována.</string>
|
||||
<string name="store_passphrase_securely_without_recover">Uložte prosím bezpečně přístupovou frázi, pokud ji ztratíte, NEBUDE možné přistupovat k chatu.</string>
|
||||
<string name="wrong_passphrase">Špatná přístupová fráze k databázi</string>
|
||||
<string name="encrypted_database">Zašifrovaná databáze</string>
|
||||
<string name="database_error">Chyba databáze</string>
|
||||
<string name="keychain_error">Chyba klíčenky</string>
|
||||
<string name="passphrase_is_different">Databázová heslová fráze se liší od uložené v klíčence.</string>
|
||||
<string name="cannot_access_keychain">Nelze získat přístup k úložišti klíčů pro uložení hesla k databázi.</string>
|
||||
<string name="unknown_database_error_with_info">Neznámá chyba databáze: %s</string>
|
||||
<string name="wrong_passphrase_title">Špatná přístupová fráze!</string>
|
||||
<string name="enter_correct_passphrase">Zadejte správnou přístupovou frázi.</string>
|
||||
<string name="enter_passphrase">Zadejte přístupovou frázi…</string>
|
||||
<string name="database_backup_can_be_restored">Pokus o změnu přístupové fráze databáze nebyl dokončen.</string>
|
||||
<string name="restore_database_alert_title">Obnovit zálohu databáze\?</string>
|
||||
<string name="restore_database_alert_confirm">Obnovit</string>
|
||||
<string name="database_restore_error">Chyba obnovení databáze</string>
|
||||
<string name="restore_passphrase_not_found_desc">Heslo nebylo nalezeno v úložišti klíčů, zadejte jej prosím ručně. K této situaci mohlo dojít, pokud jste obnovili data aplikace pomocí zálohovacího nástroje. Pokud tomu tak není, obraťte se na vývojáře.</string>
|
||||
<string name="you_can_start_chat_via_setting_or_by_restarting_the_app">Chat můžete spustit přes Nastavení aplikace / Databáze nebo restartováním aplikace.</string>
|
||||
<string name="save_archive">Uložit archiv</string>
|
||||
<string name="delete_archive">Smazat archiv</string>
|
||||
<string name="group_invitation_item_description">pozvánka do skupiny <xliff:g id="group_name">%1$s</xliff:g></string>
|
||||
<string name="archive_created_on_ts">Vytvořeno dne <xliff:g id="archive_ts">%1$s</xliff:g></string>
|
||||
<string name="you_are_invited_to_group_join_to_connect_with_group_members">Jste zváni do skupiny. Připojte se a spojte se s členy skupiny.</string>
|
||||
<string name="join_group_incognito_button">Připojte se inkognito</string>
|
||||
<string name="joining_group">Připojení ke skupině</string>
|
||||
<string name="youve_accepted_group_invitation_connecting_to_inviting_group_member">Připojili jste se k této skupině. Připojení k pozvání člena skupiny.</string>
|
||||
<string name="you_will_stop_receiving_messages_from_this_group_chat_history_will_be_preserved">Přestanete dostávat zprávy z této skupiny. Historie chatu bude zachována.</string>
|
||||
<string name="alert_title_group_invitation_expired">Platnost pozvánky vypršela!</string>
|
||||
<string name="alert_message_group_invitation_expired">Pozvánka do skupiny již není platná, byla odstraněna odesílatelem.</string>
|
||||
<string name="alert_message_no_group">Tato skupina již neexistuje.</string>
|
||||
<string name="alert_title_cant_invite_contacts_descr">Pro tuto skupinu používáte inkognito profil - abyste zabránili sdílení svého hlavního profilu, není pozvání kontaktů povoleno.</string>
|
||||
<string name="you_sent_group_invitation">Odeslali jste pozvánku do skupiny</string>
|
||||
<string name="you_are_invited_to_group">Jste pozváni do skupiny</string>
|
||||
<string name="group_invitation_tap_to_join">Klepnutím se připojíte</string>
|
||||
<string name="group_invitation_tap_to_join_incognito">Klepnutím se připojíte inkognito</string>
|
||||
<string name="you_joined_this_group">Připojili jste se k této skupině</string>
|
||||
<string name="you_rejected_group_invitation">Odmítli jste pozvánku do skupiny</string>
|
||||
<string name="group_invitation_expired">Platnost pozvánky do skupiny vypršela</string>
|
||||
<string name="rcv_group_event_member_added">pozvánka <xliff:g id="profil člena" example="alice (Alice)">%1$s</xliff:g></string>
|
||||
<string name="rcv_group_event_member_connected">připojeno</string>
|
||||
<string name="rcv_group_event_changed_member_role">změnil roli %s na %s</string>
|
||||
<string name="rcv_group_event_changed_your_role">změnil svou roli na %s</string>
|
||||
<string name="rcv_group_event_member_deleted">odstraněno <xliff:g id="member profile" example="alice (Alice)">%1$s</xliff:g></string>
|
||||
<string name="rcv_group_event_user_deleted">odstranil vás</string>
|
||||
<string name="rcv_group_event_group_deleted">odstraněna skupina</string>
|
||||
<string name="rcv_group_event_updated_group_profile">aktualizoval profil skupiny</string>
|
||||
<string name="rcv_group_event_invited_via_your_group_link">pozváni prostřednictvím odkazu na vaši skupinu</string>
|
||||
<string name="snd_group_event_group_profile_updated">profil skupiny aktualizován</string>
|
||||
<string name="rcv_conn_event_switch_queue_phase_changing">změna adresy…</string>
|
||||
<string name="snd_conn_event_switch_queue_phase_completed_for_member">jste změnili adresu pro %s</string>
|
||||
<string name="snd_conn_event_switch_queue_phase_changing_for_member">změna adresy pro %s…</string>
|
||||
<string name="snd_conn_event_switch_queue_phase_completed">změnili jste adresu</string>
|
||||
<string name="snd_conn_event_switch_queue_phase_changing">změna adresy…</string>
|
||||
<string name="group_member_role_member">člen</string>
|
||||
<string name="group_member_role_owner">vlastník</string>
|
||||
<string name="group_member_status_removed">odstraněno</string>
|
||||
<string name="group_member_status_left">opustil</string>
|
||||
<string name="group_member_status_group_deleted">skupina smazána</string>
|
||||
<string name="group_member_status_invited">pozvánka</string>
|
||||
<string name="group_member_status_introduced">připojující (zavedený)</string>
|
||||
<string name="group_member_status_intro_invitation">připojení (pozvánka na představení)</string>
|
||||
<string name="group_member_status_accepted">připojení (přijato)</string>
|
||||
<string name="group_member_status_announced">připojení (oznámeno)</string>
|
||||
<string name="group_member_status_connected">připojen</string>
|
||||
<string name="group_member_status_complete">kompletní</string>
|
||||
<string name="group_member_status_creator">tvůrce</string>
|
||||
<string name="group_member_status_connecting">připojování</string>
|
||||
<string name="no_contacts_to_add">Žádné kontakty k přidání</string>
|
||||
<string name="new_member_role">Nová role člena</string>
|
||||
<string name="invite_to_group_button">Pozvat do skupiny</string>
|
||||
<string name="skip_inviting_button">Přeskočit pozvání členů</string>
|
||||
<string name="select_contacts">Vybrat kontakty</string>
|
||||
<string name="icon_descr_contact_checked">Zkontrolované kontakty</string>
|
||||
<string name="num_contacts_selected"><xliff:g id="num_contacts">%1$s</xliff:g> kontakt(y) vybrán(y)</string>
|
||||
<string name="button_add_members">Pozvat členy</string>
|
||||
<string name="group_info_section_title_num_members"><xliff:g id="num_members">%1$s</xliff:g> MEMBERS</string>
|
||||
<string name="group_info_member_you">vy: <xliff:g id="group_info_you">%1$s</xliff:g></string>
|
||||
<string name="button_delete_group">Smazat skupinu</string>
|
||||
<string name="delete_group_question">Smazat skupinu\?</string>
|
||||
<string name="delete_group_for_all_members_cannot_undo_warning">Skupina bude smazána pro všechny členy - nelze to vzít zpět!</string>
|
||||
<string name="delete_group_for_self_cannot_undo_warning">Skupina bude smazána pro vás - toto nelze vzít zpět!</string>
|
||||
<string name="button_leave_group">Opustit skupinu</string>
|
||||
<string name="button_edit_group_profile">Upravit profil skupiny</string>
|
||||
<string name="group_link">Odkaz na skupinu</string>
|
||||
<string name="create_group_link">Vytvořit odkaz na skupinu</string>
|
||||
<string name="delete_link">Smazat odkaz</string>
|
||||
<string name="you_can_share_group_link_anybody_will_be_able_to_connect">Můžete sdílet odkaz nebo QR kód - ke skupině se bude moci připojit kdokoli. O členy skupiny nepřijdete, pokud ji později odstraníte.</string>
|
||||
<string name="error_creating_link_for_group">Chyba při vytváření odkazu skupiny</string>
|
||||
<string name="error_deleting_link_for_group">Chyba při odstraňování odkazu skupiny</string>
|
||||
<string name="only_group_owners_can_change_prefs">Předvolby skupiny mohou měnit pouze vlastníci skupiny.</string>
|
||||
<string name="section_title_for_console">PRO KONSOLE</string>
|
||||
<string name="info_row_local_name">Místní název</string>
|
||||
<string name="info_row_database_id">ID databáze</string>
|
||||
<string name="button_remove_member">Odstranit člena</string>
|
||||
<string name="member_will_be_removed_from_group_cannot_be_undone">Člen bude odstraněn ze skupiny - toto nelze vzít zpět!</string>
|
||||
<string name="remove_member_confirmation">Odstranit</string>
|
||||
<string name="role_in_group">Role</string>
|
||||
<string name="change_role">Změnit roli</string>
|
||||
<string name="change_verb">Změnit</string>
|
||||
<string name="member_role_will_be_changed_with_invitation">Role bude změněna na \"%s\". Člen obdrží novou pozvánku.</string>
|
||||
<string name="error_changing_role">Chyba při změně role</string>
|
||||
<string name="conn_level_desc_direct">přímo</string>
|
||||
<string name="sending_via">Odesílání přes</string>
|
||||
<string name="network_status">Stav sítě</string>
|
||||
<string name="switch_receiving_address">Přepínač přijímací adresy</string>
|
||||
<string name="group_is_decentralized">Skupina je plně decentralizovaná - je viditelná pouze pro členy.</string>
|
||||
<string name="group_unsupported_incognito_main_profile_sent">Zde není podporován režim inkognito - členům skupiny bude zaslán váš hlavní profil.</string>
|
||||
<string name="save_group_profile">Uložení profilu skupiny</string>
|
||||
<string name="network_options_reset_to_defaults">Obnovení výchozího nastavení</string>
|
||||
<string name="network_option_tcp_connection_timeout">Časový limit připojení TCP</string>
|
||||
<string name="network_option_protocol_timeout">Časový limit protokolu</string>
|
||||
<string name="network_option_ping_interval">Interval PING</string>
|
||||
<string name="network_option_ping_count">Počet PING</string>
|
||||
<string name="network_option_enable_tcp_keep_alive">Povolit TCP keep-alive</string>
|
||||
<string name="update_network_settings_confirmation">Aktualizovat</string>
|
||||
<string name="users_delete_question">Smazat profil chatu\?</string>
|
||||
<string name="users_delete_profile_for">Smazat profil chatu pro</string>
|
||||
<string name="users_delete_with_connections">Profil a připojení k serveru</string>
|
||||
<string name="users_delete_data_only">Pouze lokální profilová data</string>
|
||||
<string name="incognito_random_profile_from_contact_description">Náhodný profil bude zaslán kontaktu, od kterého jste obdrželi tento odkaz.</string>
|
||||
<string name="incognito_info_protects">Režim inkognito chrání soukromí vašeho hlavního profilového jména a obrázku - pro každý nový kontakt je vytvořen nový náhodný profil.</string>
|
||||
<string name="incognito_info_find">Chcete-li najít profil použitý pro inkognito připojení, klepněte na název kontaktu nebo skupiny v horní části chatu.</string>
|
||||
<string name="theme_light">Světlo</string>
|
||||
<string name="theme_dark">Tmavý</string>
|
||||
<string name="theme">Téma</string>
|
||||
<string name="chat_preferences_contact_allows">Kontakt povoluje</string>
|
||||
<string name="chat_preferences_on">na</string>
|
||||
<string name="chat_preferences_off">vypnuto</string>
|
||||
<string name="chat_preferences">Předvolby chatu</string>
|
||||
<string name="contact_preferences">Předvolby kontaktů</string>
|
||||
<string name="group_preferences">Předvolby skupiny</string>
|
||||
<string name="direct_messages">Přímé zprávy</string>
|
||||
<string name="full_deletion">Smazat pro všechny</string>
|
||||
<string name="feature_enabled">povoleno</string>
|
||||
<string name="feature_enabled_for_you">povoleno pro vás</string>
|
||||
<string name="feature_off">vypnuto</string>
|
||||
<string name="prohibit_sending_disappearing_messages">Zakázat zasílání mizejících zpráv.</string>
|
||||
<string name="contacts_can_mark_messages_for_deletion">Kontakty mohou označit zprávy ke smazání; vy je budete moci zobrazit.</string>
|
||||
<string name="prohibit_sending_voice_messages">Zakázat odesílání hlasových zpráv.</string>
|
||||
<string name="only_you_can_send_disappearing">Mizící zprávy můžete odesílat pouze vy.</string>
|
||||
<string name="disappearing_prohibited_in_this_chat">Mizící zprávy jsou v tomto chatu zakázány.</string>
|
||||
<string name="only_your_contact_can_delete">Nevratně mazat zprávy může pouze váš kontakt (vy je můžete označit ke smazání).</string>
|
||||
<string name="both_you_and_your_contact_can_send_voice">Hlasové zprávy můžete posílat vy i váš kontakt.</string>
|
||||
<string name="only_you_can_send_voice">Hlasové zprávy můžete posílat pouze vy.</string>
|
||||
<string name="only_your_contact_can_send_voice">Hlasové zprávy může odesílat pouze váš kontakt.</string>
|
||||
<string name="voice_prohibited_in_this_chat">Hlasové zprávy jsou v tomto chatu zakázány.</string>
|
||||
<string name="prohibit_sending_disappearing">Zakázat posílání mizejících zpráv.</string>
|
||||
<string name="prohibit_message_deletion">Zakázat nevratné mazání zpráv.</string>
|
||||
<string name="prohibit_sending_voice">Zakázat odesílání hlasových zpráv.</string>
|
||||
<string name="group_members_can_send_disappearing">Členové skupiny mohou posílat mizející zprávy.</string>
|
||||
<string name="disappearing_messages_are_prohibited">Mizící zprávy jsou v této skupině zakázány.</string>
|
||||
<string name="group_members_can_send_dms">Členové skupiny mohou posílat přímé zprávy.</string>
|
||||
<string name="direct_messages_are_prohibited_in_chat">Přímé zprávy mezi členy jsou v této skupině zakázány.</string>
|
||||
<string name="group_members_can_delete">Členové skupiny mohou nevratně mazat odeslané zprávy.</string>
|
||||
<string name="message_deletion_prohibited_in_chat">Nevratné mazání zpráv je v této skupině zakázáno.</string>
|
||||
<string name="group_members_can_send_voice">Členové skupiny mohou posílat hlasové zprávy.</string>
|
||||
<string name="voice_messages_are_prohibited">Hlasové zprávy jsou v této skupině zakázány.</string>
|
||||
<string name="delete_after">Smazat po</string>
|
||||
<string name="ttl_month">\"%d měsíc</string>
|
||||
<string name="ttl_months">\"%d měsíců</string>
|
||||
<string name="ttl_day">\"%d den</string>
|
||||
<string name="ttl_days">\"%d dnů</string>
|
||||
<string name="ttl_week">\"%d týden</string>
|
||||
<string name="ttl_weeks">\"%d týdnů</string>
|
||||
<string name="ttl_w">\"%dw</string>
|
||||
<string name="feature_offered_item">nabízeno %s</string>
|
||||
<string name="feature_cancelled_item">zrušeno %s</string>
|
||||
<string name="whats_new">Co je nového</string>
|
||||
<string name="new_in_version">Novinky v %s</string>
|
||||
<string name="v4_2_auto_accept_contact_requests">Automatické přijímání žádostí o kontakt</string>
|
||||
<string name="v4_2_auto_accept_contact_requests_desc">S volitelnou uvítací zprávou.</string>
|
||||
<string name="v4_3_voice_messages_desc">Max. 40 sekund, přijímá se okamžitě.</string>
|
||||
<string name="v4_5_private_filenames">Soukromé názvy souborů</string>
|
||||
<string name="v4_5_private_filenames_descr">Pro ochranu časového pásma, obrazové/hlasové soubory používají UTC.</string>
|
||||
<string name="v4_5_reduced_battery_usage">Snížení spotřeby baterie</string>
|
||||
<string name="v4_5_reduced_battery_usage_descr">Další vylepšení se chystají již brzy!</string>
|
||||
<string name="v4_5_italian_interface">Italské rozhraní</string>
|
||||
<string name="v4_5_italian_interface_descr">Díky uživatelům - přispívejte prostřednictvím Weblate!</string>
|
||||
<string name="you_will_be_connected_when_your_contacts_device_is_online">Budete připojeni, až bude zařízení vašeho kontaktu online, vyčkejte prosím nebo se podívejte později!</string>
|
||||
<string name="your_contact_address">Vaše kontaktní adresa</string>
|
||||
<string name="your_chat_profile_will_be_sent_to_your_contact">Váš profil v chatu bude odeslán vašemu kontaktu</string>
|
||||
<string name="your_chat_profiles_stored_locally">Vaše profily v chatu jsou uloženy lokálně, pouze ve vašem zařízení.</string>
|
||||
<string name="your_chats">Vaše konverzace</string>
|
||||
</resources>
|
||||
@@ -1,2 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources></resources>
|
||||
@@ -1,147 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="share_image">चित्र साझा करें…</string>
|
||||
<string name="chat_preferences_off">बंद</string>
|
||||
<string name="accept_feature_set_1_day">1 दिन निर्धारित करें</string>
|
||||
<string name="v4_3_improved_server_configuration_desc">क्यूआर संहिता स्कैन करके सर्वर जोड़ें।</string>
|
||||
<string name="group_preview_you_are_invited">आपको समूह में आमंत्रित किया जाता है</string>
|
||||
<string name="icon_descr_server_status_connected">जुड़े हुए</string>
|
||||
<string name="use_camera_button">कैमरे का प्रयोग करें</string>
|
||||
<string name="above_then_preposition_continuation">ऊपर,तब:</string>
|
||||
<string name="accept_contact_button">स्वीकार करना</string>
|
||||
<string name="connect_button">जुडिये</string>
|
||||
<string name="your_contact_address">आपका संपर्क पता</string>
|
||||
<string name="smp_servers_add_to_another_device">दूसरे उपकरण में जोड़ें</string>
|
||||
<string name="bold">निडर</string>
|
||||
<string name="answer_call">कॉल का उत्तर दें</string>
|
||||
<string name="settings_section_title_you">तुम</string>
|
||||
<string name="settings_section_title_settings">समायोजन</string>
|
||||
<string name="chat_item_ttl_month">1 महीना</string>
|
||||
<string name="rcv_group_event_member_connected">जुड़े हुए</string>
|
||||
<string name="group_member_role_admin">व्यवस्थापक</string>
|
||||
<string name="all_group_members_will_remain_connected">समूह के सभी सदस्य जुड़े रहेंगे।</string>
|
||||
<string name="change_verb">परिवर्तन</string>
|
||||
<string name="sending_via">माध्यम से भेजा जा रहा है</string>
|
||||
<string name="feature_off">बंद</string>
|
||||
<string name="whats_new">नया क्या है</string>
|
||||
<string name="v4_2_group_links_desc">व्यवस्थापक समूहों में शामिल होने के लिए लिंक बना सकते हैं।</string>
|
||||
<string name="chat_item_ttl_day">1 दिन</string>
|
||||
<string name="chat_item_ttl_week">1 सप्ताह</string>
|
||||
<string name="about_simplex">सिंपलएक्स के बारे में</string>
|
||||
<string name="about_simplex_chat">बारे में <xliff:g id="appNameFull">सिंप्लेक्स चैट</xliff:g></string>
|
||||
<string name="accept_call_on_lock_screen">स्वीकार करना</string>
|
||||
<string name="accept">स्वीकार करना</string>
|
||||
<string name="accept_feature">स्वीकार करना</string>
|
||||
<string name="accept_connection_request__question">संबंध अनुरोध स्वीकार करें\?</string>
|
||||
<string name="callstatus_accepted">स्वीकृत कॉल</string>
|
||||
<string name="accept_contact_incognito_button">गुप्त स्वीकार करें</string>
|
||||
<string name="accept_requests">निवेदन स्वीकार करो</string>
|
||||
<string name="smp_servers_preset_add">पूर्वनिर्धारित सर्वर जोड़ें</string>
|
||||
<string name="users_add">प्रोफ़ाइल जोड़ें</string>
|
||||
<string name="smp_servers_add">सर्वर जोड़े…</string>
|
||||
<string name="notifications_mode_service">हमेशा बने रहें</string>
|
||||
<string name="attach">संलग्न करना</string>
|
||||
<string name="network_settings">उन्नत संजाल समायोजन</string>
|
||||
<string name="users_delete_all_chats_deleted">सभी बातचीत और संदेश हटा दिए जाएंगे - इसे पूर्ववत नहीं किया जा सकता!</string>
|
||||
<string name="chat_preferences_always">हमेशा</string>
|
||||
<string name="allow_verb">अनुमति देना</string>
|
||||
<string name="appearance_settings">दिखावट</string>
|
||||
<string name="cancel_verb">रद्द करना</string>
|
||||
<string name="icon_descr_cancel_file_preview">फ़ाइल पूर्वावलोकन रद्द करें</string>
|
||||
<string name="icon_descr_cancel_image_preview">छवि पूर्वावलोकन रद्द करें</string>
|
||||
<string name="clear_verb">साफ़</string>
|
||||
<string name="colored">रंगीन</string>
|
||||
<string name="callstate_connected">जुड़े हुए</string>
|
||||
<string name="smp_server_test_connect">जुडिये</string>
|
||||
<string name="connect_via_link_verb">जुडिये</string>
|
||||
<string name="server_connected">जुड़े हुए</string>
|
||||
<string name="group_member_role_owner">स्वामी</string>
|
||||
<string name="group_member_status_connected">जुड़े हुए</string>
|
||||
<string name="notification_contact_connected">जुड़े हुए</string>
|
||||
<string name="you_joined_this_group">आप इस समूह में शामिल हो गए</string>
|
||||
<string name="group_info_member_you">तुम: <xliff:g id="group_info_you">%1$s</xliff:g></string>
|
||||
<string name="you_are_invited_to_group">आपको समूह में आमंत्रित किया जाता है</string>
|
||||
<string name="snd_conn_event_switch_queue_phase_completed">तुमने पता बदल लिया</string>
|
||||
<string name="snd_group_event_user_left">आप चले गए</string>
|
||||
<string name="unknown_error">अज्ञात त्रुटि</string>
|
||||
<string name="chat_preferences_you_allow">आप आज्ञा दें</string>
|
||||
<string name="welcome">स्वागत!</string>
|
||||
<string name="la_notice_turn_on">चालू करो</string>
|
||||
<string name="section_title_welcome_message">स्वागत संदेश</string>
|
||||
<string name="unknown_message_format">अज्ञात संदेश प्रारूप</string>
|
||||
<string name="personal_welcome">स्वागत <xliff:g>%1$s</xliff:g>!</string>
|
||||
<string name="callstate_starting">शुरुआत</string>
|
||||
<string name="send_verb">भेजना</string>
|
||||
<string name="save_color">रंग बचाओ</string>
|
||||
<string name="share_verb">साझा करना</string>
|
||||
<string name="reject_contact_button">अस्वीकार</string>
|
||||
<string name="network_use_onion_hosts_required">आवश्यक</string>
|
||||
<string name="reject">अस्वीकार</string>
|
||||
<string name="open_verb">खुला</string>
|
||||
<string name="group_member_status_removed">निकाला गया</string>
|
||||
<string name="rcv_group_event_member_deleted">निकाला गया <xliff:g id="member profile" example="alice (Alice)">%1$s</xliff:g></string>
|
||||
<string name="reply_verb">जवाब दे दो</string>
|
||||
<string name="leave_group_button">छोड़ना</string>
|
||||
<string name="mark_read">पढ़ा हुआ चिह्नित करें</string>
|
||||
<string name="icon_descr_more_button">अधिक</string>
|
||||
<string name="network_use_onion_hosts_no">नहीं</string>
|
||||
<string name="chat_item_ttl_none">कभी नहीं</string>
|
||||
<string name="group_member_status_invited">आमंत्रित</string>
|
||||
<string name="delete_after">बाद मिटा दें</string>
|
||||
<string name="display_name_invited_to_connect">जुड़ने के लिए आमंत्रित किया</string>
|
||||
<string name="rcv_group_event_invited_via_your_group_link">आपके समूह लिंक के माध्यम से आमंत्रित किया गया</string>
|
||||
<string name="rcv_group_event_member_added">आमंत्रित <xliff:g id="member profile" example="alice (Alice)">%1$s</xliff:g></string>
|
||||
<string name="icon_descr_add_members">सदस्यों को आमंत्रित करो</string>
|
||||
<string name="v4_3_irreversible_message_deletion">अपरिवर्तनीय संदेश विलोपन</string>
|
||||
<string name="button_add_members">सदस्यों को आमंत्रित करो</string>
|
||||
<string name="invite_to_group_button">समूह में आमंत्रित करें</string>
|
||||
<string name="message_deletion_prohibited_in_chat">इस समूह में अपरिवर्तनीय संदेश हटाना प्रतिबंधित है।</string>
|
||||
<string name="italic">तिरछा</string>
|
||||
<string name="join_group_button">जोड़ना</string>
|
||||
<string name="join_group_incognito_button">गुप्त में शामिल हों</string>
|
||||
<string name="joining_group">समूह में शामिल होना</string>
|
||||
<string name="join_group_question">समूह में शामिल हों\?</string>
|
||||
<string name="thousand_abbreviation">क</string>
|
||||
<string name="keychain_error">चाबी का गुच्छा त्रुटि</string>
|
||||
<string name="button_leave_group">समूह छोड़ दें</string>
|
||||
<string name="leave_group_question">समूह छोड़ दें</string>
|
||||
<string name="rcv_group_event_member_left">बाएं</string>
|
||||
<string name="group_member_status_left">बाएं</string>
|
||||
<string name="theme_light">रोशनी</string>
|
||||
<string name="info_row_local_name">स्थानीय नाम</string>
|
||||
<string name="users_delete_data_only">केवल स्थानीय प्रोफ़ाइल डेटा</string>
|
||||
<string name="auth_log_in_using_credential">अपने क्रेडेंशियल का उपयोग करके लॉग इन करें</string>
|
||||
<string name="make_private_connection">एक निजी संबंध बनाओ</string>
|
||||
<string name="marked_deleted_description">मिटाया हुआ चिह्नित किया गया</string>
|
||||
<string name="mark_unread">अपठित को चिह्नित करें</string>
|
||||
<string name="v4_3_voice_messages_desc">अधिकतम 40 सेकंड, तुरन्त प्राप्त हुआ।</string>
|
||||
<string name="you_sent_group_invitation">आपने समूह आमंत्रण भेजा</string>
|
||||
<string name="message_delivery_error_desc">सबसे अधिक संभावना है कि इस संपर्क ने आपके साथ संबंध हटा दिया है।</string>
|
||||
<string name="mute_chat">मूक</string>
|
||||
<string name="network_status">नेटवर्क की स्थिति</string>
|
||||
<string name="notification_new_contact_request">नया संपर्क अनुरोध</string>
|
||||
<string name="delete_files_and_media_all">सभी फाइलों को मिटा दें</string>
|
||||
<string name="delete_archive">संग्रह हटाएं</string>
|
||||
<string name="new_database_archive">नया डेटाबेस संग्रह</string>
|
||||
<string name="new_member_role">नए सदस्य की भूमिका</string>
|
||||
<string name="settings_notifications_mode_title">अधिसूचना सेवा</string>
|
||||
<string name="notification_preview_new_message">नया सन्देश</string>
|
||||
<string name="no_contacts_to_add">जोड़ने के लिए कोई संपर्क नहीं है</string>
|
||||
<string name="chat_preferences_no">नहीं</string>
|
||||
<string name="no_contacts_selected">कोई संपर्क नहीं चुना गया</string>
|
||||
<string name="no_details">कोई विवरण नहीं</string>
|
||||
<string name="settings_notification_preview_title">अधिसूचना पूर्वावलोकन</string>
|
||||
<string name="notifications">सूचनाएं</string>
|
||||
<string name="full_deletion">सभी के लिए हटाएं</string>
|
||||
<string name="delete_chat_archive_question">चैट संग्रह मिटाएं\?</string>
|
||||
<string name="delete_chat_profile_question">चैट प्रोफ़ाइल हटाएं\?</string>
|
||||
<string name="users_delete_question">चैट प्रोफ़ाइल हटाएं\?</string>
|
||||
<string name="users_delete_profile_for">के लिए चैट प्रोफ़ाइल हटाएं</string>
|
||||
<string name="button_delete_contact">संपर्क मिटा दें</string>
|
||||
<string name="deleted_description">हटाए गए</string>
|
||||
<string name="delete_contact_question">संपर्क मिटा दें\?</string>
|
||||
<string name="rcv_group_event_group_deleted">हटाए गए समूह</string>
|
||||
<string name="delete_image">छवि हटाएं</string>
|
||||
<string name="button_delete_group">समूह हटाएं</string>
|
||||
<string name="for_me_only">मेरे लिए हटाएं</string>
|
||||
</resources>
|
||||
@@ -1,2 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources></resources>
|
||||
@@ -1,86 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="chat_item_ttl_day">1日</string>
|
||||
<string name="chat_item_ttl_week">1週間</string>
|
||||
<string name="callstatus_accepted">受けた通話</string>
|
||||
<string name="smp_servers_preset_add">既存サーバを追加</string>
|
||||
<string name="group_member_role_admin">管理者</string>
|
||||
<string name="v4_2_group_links_desc">管理者はグループの参加リンクを発行できます。</string>
|
||||
<string name="network_settings">ネットワーク詳細設定</string>
|
||||
<string name="chat_item_ttl_month">1ヶ月</string>
|
||||
<string name="about_simplex">SimpleXについて</string>
|
||||
<string name="a_plus_b">a + b</string>
|
||||
<string name="about_simplex_chat"><xliff:g id="appNameFull">SimpleX Chat</xliff:g>について</string>
|
||||
<string name="color_primary">アクセント色</string>
|
||||
<string name="accept_contact_button">承諾</string>
|
||||
<string name="accept_connection_request__question">繋がりを承諾しますか?</string>
|
||||
<string name="accept">承諾</string>
|
||||
<string name="accept_feature">承諾</string>
|
||||
<string name="accept_call_on_lock_screen">承諾</string>
|
||||
<string name="accept_contact_incognito_button">シークレットモードで承諾</string>
|
||||
<string name="v4_3_improved_server_configuration_desc">QRコードでサーバを追加</string>
|
||||
<string name="smp_servers_add_to_another_device">別の端末に追加</string>
|
||||
<string name="users_add">プロフィールを追加</string>
|
||||
<string name="smp_servers_add">サーバを追加…</string>
|
||||
<string name="network_enable_socks_info">SOCKSプロキシ(ポート9050)経由で接続しますか?(※設定する前にプロキシ起動が必要※)</string>
|
||||
<string name="users_delete_all_chats_deleted">全チャットとメッセージが削除されます(※元に戻せません※)!</string>
|
||||
<string name="allow_your_contacts_to_send_voice_messages">送信相手からの音声メッセージを許可する。</string>
|
||||
<string name="both_you_and_your_contact_can_send_voice">あなたと連絡相手が音声メッセージを送信できます。</string>
|
||||
<string name="onboarding_notifications_mode_periodic_desc"><b>電池省エネに良い</b>:バックグラウンド機能で10分毎に新着メッセージを確認します。通話と緊急メッセージを見逃す可能性があります。</string>
|
||||
<string name="icon_descr_audio_off">音声オフ</string>
|
||||
<string name="attach">添付する</string>
|
||||
<string name="app_version_code">アプリ・ビルド番号: %s</string>
|
||||
<string name="all_your_contacts_will_remain_connected">あなたの連絡先が繋がったまま継続します。</string>
|
||||
<string name="accept_requests">リクエストを承諾</string>
|
||||
<string name="accept_automatically">自動的に</string>
|
||||
<string name="icon_descr_audio_on">音声オン</string>
|
||||
<string name="integrity_msg_bad_hash">メッセージのハッシュ値問題</string>
|
||||
<string name="integrity_msg_bad_id">メッセージIDの問題</string>
|
||||
<string name="allow_verb">許可</string>
|
||||
<string name="allow_voice_messages_question">音声メッセージを許可しますか?</string>
|
||||
<string name="back">戻る</string>
|
||||
<string name="appearance_settings">見た目</string>
|
||||
<string name="app_version_title">アプリのバージョン</string>
|
||||
<string name="app_version_name">アプリのバージョン: v%s</string>
|
||||
<string name="network_session_mode_user_description"><b>アプリ内の各チャットプロフィールに、</b>.連絡先毎にそれぞれのTCP接続(とSOCKS資格情報)が使われます。</string>
|
||||
<string name="network_session_mode_entity_description"><b>各連絡先とグループに、</b>それぞれのTCP接続(とSOCKS資格情報)が使われます。
|
||||
\n<b>※注意※</b> 接続が多かったら、電池とデータの使用量が増えて、切断する可能性もあります。</string>
|
||||
<string name="bold">太文字</string>
|
||||
<string name="icon_descr_audio_call">音声通話</string>
|
||||
<string name="settings_audio_video_calls">音声とビデオ通話</string>
|
||||
<string name="impossible_to_recover_passphrase"><b>※注意※</b>:喪失したら、パスフレーズの回復・変更ができません。</string>
|
||||
<string name="all_group_members_will_remain_connected">グループ全員の接続が継続します。</string>
|
||||
<string name="allow_your_contacts_to_send_disappearing_messages">送信相手が消えるメッセージを送るのを許可する。</string>
|
||||
<string name="allow_your_contacts_irreversibly_delete">送信相手が永久メッセージ削除するのを許可する。</string>
|
||||
<string name="allow_voice_messages_only_if">送信相手も音声メッセージを許可する時のみに許可する。</string>
|
||||
<string name="both_you_and_your_contact_can_send_disappearing">あなたと連絡相手が消えるメッセージを送信できます。</string>
|
||||
<string name="v4_2_auto_accept_contact_requests">連絡先を自動的に承諾</string>
|
||||
<string name="chat_preferences_always">常に</string>
|
||||
<string name="notifications_mode_service">常にオン</string>
|
||||
<string name="clear_chat_warning">全てのメッセージが削除されます(※注意:元に戻せません!※)。削除されるのは片方あなたのメッセージのみ</string>
|
||||
<string name="allow_disappearing_messages_only_if">送信相手も消えるメッセージ機能を許可する時のみに許可する。</string>
|
||||
<string name="allow_irreversible_message_deletion_only_if">送信相手も永久メッセージ削除を許可する時のみに許可する。</string>
|
||||
<string name="allow_to_delete_messages">送信済みメッセージの永久削除を許可</string>
|
||||
<string name="allow_to_send_disappearing">消えるメッセージの送信を許可</string>
|
||||
<string name="allow_direct_messages">メンバーへのダイレクトメッセージを許可</string>
|
||||
<string name="allow_to_send_voice">音声メッセージの送信を許可</string>
|
||||
<string name="notifications_mode_off_desc">アクティブの時のみに通知が出ます。バックグラウンド通知サービスは起動されません。</string>
|
||||
<string name="keychain_is_storing_securely">Androidキーストアはパスフレーズの保管に使われます。通知機能に必要です。</string>
|
||||
<string name="keychain_allows_to_receive_ntfs">再起動時とパスフレーズ変更時にAndroidキーストアがパスフレーズの保管に使われます。通知機能に必要です。</string>
|
||||
<string name="answer_call">通話に応答</string>
|
||||
<string name="settings_section_title_icon">アプリのアイコン</string>
|
||||
<string name="full_backup">アプリデータのバックアップ</string>
|
||||
<string name="incognito_random_profile_from_contact_description">このリンクの送信元にランダムなプロフィール(ダミー)が送られます。</string>
|
||||
<string name="incognito_random_profile_description">連絡先にランダムなプロフィール(ダミー)が送られます。</string>
|
||||
<string name="audio_call_no_encryption">音声通話 (エンドツーエンド暗号化なし)</string>
|
||||
<string name="icon_descr_asked_to_receive">画像受信を依頼しました。</string>
|
||||
<string name="auth_unavailable">認証不可能</string>
|
||||
<string name="auto_accept_images">画像を自動的に受信</string>
|
||||
<string name="notifications_mode_service_desc">バックグラウンド機能が常にオンで、メッセージが到着次第に通知が出ます。</string>
|
||||
<string name="add_new_contact_to_create_one_time_QR_code"><b>新しい連絡先を追加</b>:使い捨てのQRコードを発行</string>
|
||||
<string name="turning_off_service_and_periodic">電池省エネをオンに、バックグラウンド機能と定期的な受信依頼をオフにします。設定メニューにて変更できます。</string>
|
||||
<string name="onboarding_notifications_mode_off_desc"><b>電池消費が最少</b>:アプリがアクティブ時のみに通知が出ます。</string>
|
||||
<string name="it_can_disabled_via_settings_notifications_still_shown"><b>設定メニューにてオフにできます。</b> アプリがアクティブ時に通知が出ます。</string>
|
||||
<string name="both_you_and_your_contacts_can_delete">あなたと連絡相手が送信済みメッセージを永久削除できます。</string>
|
||||
<string name="scan_QR_code_to_connect_to_contact_who_shows_QR_code"><b>QRコードを読み込み</b>:連絡相手のQRコードをスキャンすると繋がります。</string>
|
||||
</resources>
|
||||
@@ -1,617 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="callstatus_error">oproepfout</string>
|
||||
<string name="callstatus_calling">bellen…</string>
|
||||
<string name="call_on_lock_screen">Oproepen op het vergrendelingsscherm:</string>
|
||||
<string name="callstatus_in_progress">gesprek gaande</string>
|
||||
<string name="icon_descr_call_progress">Gesprek gaande</string>
|
||||
<string name="settings_section_title_calls">OPROEPEN</string>
|
||||
<string name="cancel_verb">Annuleren</string>
|
||||
<string name="icon_descr_cancel_file_preview">Bestandsvoorbeeld annuleren</string>
|
||||
<string name="icon_descr_cancel_image_preview">Afbeeldingsvoorbeeld annuleren</string>
|
||||
<string name="feature_cancelled_item">geannuleerd %s</string>
|
||||
<string name="icon_descr_cancel_live_message">Live bericht annuleren</string>
|
||||
<string name="snd_conn_event_switch_queue_phase_changing">veranderen van adres…</string>
|
||||
<string name="notifications_mode_service">altijd aan</string>
|
||||
<string name="icon_descr_asked_to_receive">Gevraagd om de afbeelding te ontvangen</string>
|
||||
<string name="change_verb">Wijzig</string>
|
||||
<string name="network_settings">Geavanceerde netwerkinstellingen</string>
|
||||
<string name="network_enable_socks_info">Toegang tot de servers via SOCKS proxy op poort 9050\? De proxy moet worden gestart voordat u deze optie inschakelt.</string>
|
||||
<string name="alert_title_cant_invite_contacts">Kan contacten niet uitnodigen</string>
|
||||
<string name="allow_direct_messages">Directe berichten sturen naar leden toestaan.</string>
|
||||
<string name="allow_to_delete_messages">Onherroepelijk wissen van verzonden berichten toestaan.</string>
|
||||
<string name="allow_to_send_voice">Sta toe om spraakberichten te versturen.</string>
|
||||
<string name="chat_is_running">Chat is aktief</string>
|
||||
<string name="clear_chat_menu_action">Clear</string>
|
||||
<string name="chat_database_section">CHAT DATABASE</string>
|
||||
<string name="chat_archive_section">CHAT ARCHIEF</string>
|
||||
<string name="chat_console">Chat console</string>
|
||||
<string name="chat_database_imported">Chat database geïmporteerd</string>
|
||||
<string name="chat_database_deleted">Chat database verwijderd</string>
|
||||
<string name="chat_item_ttl_week">1 week</string>
|
||||
<string name="a_plus_b">a + b</string>
|
||||
<string name="accept_contact_button">Accepteer</string>
|
||||
<string name="accept_call_on_lock_screen">Accepteer</string>
|
||||
<string name="color_primary">Accent</string>
|
||||
<string name="accept">Accepteer</string>
|
||||
<string name="accept_connection_request__question">Verbindingsverzoek accepteren\?</string>
|
||||
<string name="callstatus_accepted">aanvaarde oproep</string>
|
||||
<string name="accept_contact_incognito_button">Accepteer incognito</string>
|
||||
<string name="smp_servers_preset_add">Vooraf ingestelde servers toevoegen</string>
|
||||
<string name="users_add">Profiel toevoegen</string>
|
||||
<string name="smp_servers_add">Server toevoegen…</string>
|
||||
<string name="smp_servers_add_to_another_device">Toevoegen aan een ander apparaat</string>
|
||||
<string name="v4_2_group_links_desc">Admins kunnen de links naar groepen aanmaken.</string>
|
||||
<string name="v4_3_improved_server_configuration_desc">Servers toevoegen door QR-codes te scannen.</string>
|
||||
<string name="group_member_role_admin">admin</string>
|
||||
<string name="all_group_members_will_remain_connected">Alle groepsleden blijven verbonden.</string>
|
||||
<string name="allow_verb">"Sta toe."</string>
|
||||
<string name="chat_item_ttl_day">1 dag</string>
|
||||
<string name="accept_feature">Accepteer</string>
|
||||
<string name="incognito_random_profile_from_contact_description">Een willekeurig profiel wordt gestuurd naar het contact waarvan u deze link heeft ontvangen.</string>
|
||||
<string name="network_session_mode_entity_description">Er wordt een aparte TCP-verbinding (en SOCKS-credential) gebruikt <b>voor elk contact en groepslid</b>.
|
||||
\n<b>Let op</b>: als u veel verbindingen hebt, kan uw batterij- en verkeersverbruik aanzienlijk hoger zijn en kunnen sommige verbindingen mislukken.</string>
|
||||
<string name="icon_descr_audio_call">audio-oproep</string>
|
||||
<string name="icon_descr_audio_on">Geluid aan</string>
|
||||
<string name="settings_audio_video_calls">Audio- en videogesprekken</string>
|
||||
<string name="auto_accept_images">Afbeeldingen automatisch accepteren</string>
|
||||
<string name="auth_unavailable">Verificatie niet beschikbaar</string>
|
||||
<string name="back">Terug</string>
|
||||
<string name="v4_2_auto_accept_contact_requests">Automatisch contactverzoeken accepteren</string>
|
||||
<string name="bold">vet</string>
|
||||
<string name="incognito_random_profile_description">Een willekeurig profiel wordt naar uw contactpersoon gestuurd</string>
|
||||
<string name="attach">Voeg toe</string>
|
||||
<string name="allow_irreversible_message_deletion_only_if">Laat onomkeerbare verwijdering van berichten alleen toe als uw contactpersoon u dat toestaat.</string>
|
||||
<string name="allow_to_send_disappearing">Laat verdwijnende berichten toe.</string>
|
||||
<string name="allow_your_contacts_to_send_voice_messages">Laat uw contacten spraakberichten versturen.</string>
|
||||
<string name="all_your_contacts_will_remain_connected">Al uw contacten blijven verbonden.</string>
|
||||
<string name="allow_voice_messages_question">Spraakberichten toestaan\?</string>
|
||||
<string name="onboarding_notifications_mode_periodic_desc"><b>Goed voor de batterij</b>. De achtergronddienst controleert elke 10 minuten op nieuwe berichten. U kunt oproepen en dringende berichten missen.</string>
|
||||
<string name="integrity_msg_bad_hash">Onjuiste bericht-hash</string>
|
||||
<string name="scan_QR_code_to_connect_to_contact_who_shows_QR_code"><b>Scan QR code</b>: om verbinding te maken met uw contactpersoon die de QR code aan u toont.</string>
|
||||
<string name="integrity_msg_bad_id">Onjuiste bericht-ID</string>
|
||||
<string name="call_already_ended">De oproep is al beëindigd!</string>
|
||||
<string name="chat_item_ttl_month">1 maand</string>
|
||||
<string name="about_simplex">Over SimpleX</string>
|
||||
<string name="about_simplex_chat">About <xliff:g id="appNameFull">SimpleX Chat</xliff:g></string>
|
||||
<string name="above_then_preposition_continuation">hierboven, dan:</string>
|
||||
<string name="accept_requests">Verzoeken accepteren</string>
|
||||
<string name="users_delete_all_chats_deleted">Alle chats en berichten worden verwijderd - dit kan niet ongedaan worden gemaakt!</string>
|
||||
<string name="clear_chat_warning">Alle berichten worden verwijderd - dit kan niet ongedaan worden gemaakt! De berichten worden ALLEEN voor jou verwijderd.</string>
|
||||
<string name="allow_disappearing_messages_only_if">Laat verdwijnende berichten alleen toe als uw contact dat toestaat.</string>
|
||||
<string name="allow_voice_messages_only_if">Sta spraakberichten alleen toe als uw contactpersoon ze toestaat.</string>
|
||||
<string name="allow_your_contacts_irreversibly_delete">Laat uw contacten onherroepelijk verzonden berichten verwijderen.</string>
|
||||
<string name="allow_your_contacts_to_send_disappearing_messages">Laat uw contacten verdwijnende berichten sturen.</string>
|
||||
<string name="chat_preferences_always">altijd</string>
|
||||
<string name="icon_descr_audio_off">Geluid uit</string>
|
||||
<string name="full_backup">App gegevens back-up</string>
|
||||
<string name="answer_call">Beantwoord de oproep</string>
|
||||
<string name="keychain_is_storing_securely">Android Keystore wordt gebruikt om passphrase veilig op te slaan - het laat notificatiedienst werken.</string>
|
||||
<string name="keychain_allows_to_receive_ntfs">Android Keystore wordt gebruikt om de passphrase veilig op te slaan nadat u de app opnieuw hebt opgestart of de passphrase hebt gewijzigd - hiermee kunt u meldingen ontvangen.</string>
|
||||
<string name="app_version_code">App build: %s</string>
|
||||
<string name="notifications_mode_off_desc">App kan alleen meldingen ontvangen als hij draait, er wordt geen achtergronddienst gestart.</string>
|
||||
<string name="appearance_settings">Uiterlijk</string>
|
||||
<string name="settings_section_title_icon">APP ICON</string>
|
||||
<string name="app_version_title">App versie</string>
|
||||
<string name="app_version_name">App-versie: v%s</string>
|
||||
<string name="network_session_mode_user_description">Er wordt een aparte TCP-verbinding (en SOCKS-credential) gebruikt <b>voor elk chatprofiel dat u in de app hebt</b>.</string>
|
||||
<string name="audio_call_no_encryption">audio oproep (niet e2e versleuteld)</string>
|
||||
<string name="accept_automatically">Automatisch</string>
|
||||
<string name="notifications_mode_service_desc">De achtergronddienst draait altijd - meldingen worden getoond zodra de berichten beschikbaar zijn.</string>
|
||||
<string name="add_new_contact_to_create_one_time_QR_code"><b>Nieuw contact toevoegen</b>: om uw eenmalige QR-code voor uw contactpersoon aan te maken.</string>
|
||||
<string name="icon_descr_call_ended">Oproep beëindigd</string>
|
||||
<string name="turning_off_service_and_periodic">Batterijoptimalisatie is actief en schakelt de achtergronddienst en periodieke verzoeken om nieuwe berichten uit. U kunt ze opnieuw inschakelen via de instellingen.</string>
|
||||
<string name="onboarding_notifications_mode_off_desc"><b>Beste voor de batterij</b>. U ontvangt alleen meldingen als de app draait, de achtergronddienst wordt NIET gebruikt.</string>
|
||||
<string name="it_can_disabled_via_settings_notifications_still_shown"><b>Het kan worden uitgeschakeld via instellingen</b> - meldingen worden nog steeds getoond terwijl de app draait.</string>
|
||||
<string name="both_you_and_your_contacts_can_delete">Zowel u als uw contactpersoon kunnen verzonden berichten onherroepelijk verwijderen.</string>
|
||||
<string name="both_you_and_your_contact_can_send_disappearing">Zowel jij als je contact kunnen verdwijnende berichten sturen.</string>
|
||||
<string name="both_you_and_your_contact_can_send_voice">Zowel u als uw contactpersoon kunnen spraakberichten versturen.</string>
|
||||
<string name="impossible_to_recover_passphrase"><b>Let op</b>: u kunt de wachtwoordzin NIET herstellen of wijzigen als u deze verliest.</string>
|
||||
<string name="onboarding_notifications_mode_service_desc"><b>Verbruikt meer batterij</b>! Achtergronddienst draait altijd - meldingen worden getoond zodra de berichten beschikbaar zijn.</string>
|
||||
<string name="icon_descr_cancel_link_preview">link preview annuleren</string>
|
||||
<string name="callstatus_ended">Oproep beëindigd <xliff:g id="duration" example="01:15">%1$s</xliff:g></string>
|
||||
<string name="database_initialization_error_title">Kan de database niet initialiseren</string>
|
||||
<string name="snd_conn_event_switch_queue_phase_changing_for_member">adres wijzigen voor %s…</string>
|
||||
<string name="invite_prohibited">Kan contact niet uitnodigen!</string>
|
||||
<string name="cannot_access_keychain">Kan geen toegang krijgen tot Keystore om database wachtwoord op te slaan</string>
|
||||
<string name="cannot_receive_file">Kan het bestand niet ontvangen</string>
|
||||
<string name="change_role">Rol wijzigen</string>
|
||||
<string name="rcv_conn_event_switch_queue_phase_changing">veranderen van adres…</string>
|
||||
<string name="rcv_conn_event_switch_queue_phase_completed">veranderd adres voor jou</string>
|
||||
<string name="rcv_group_event_changed_member_role">rol van %s veranderd in %s</string>
|
||||
<string name="change_member_role_question">Groepsrol wijzigen\?</string>
|
||||
<string name="chat_is_stopped">Chat is gestopt</string>
|
||||
<string name="notifications_mode_periodic_desc">Controleert nieuwe berichten elke 10 minuten gedurende maximaal 1 minuut</string>
|
||||
<string name="rcv_group_event_changed_your_role">uw rol veranderd in %s</string>
|
||||
<string name="chat_archive_header">Chat archief</string>
|
||||
<string name="change_database_passphrase_question">Database wachtwoord wijzigen\?</string>
|
||||
<string name="chat_is_stopped_indication">Chat is gestopt</string>
|
||||
<string name="chat_preferences">Chat voorkeuren</string>
|
||||
<string name="network_session_mode_user">Chat profiel</string>
|
||||
<string name="settings_section_title_chats">CHATS</string>
|
||||
<string name="chat_with_developers">Chat met de ontwikkelaars</string>
|
||||
<string name="smp_servers_check_address">Controleer het serveradres en probeer het opnieuw.</string>
|
||||
<string name="choose_file">Kies bestand</string>
|
||||
<string name="clear_verb">Clear</string>
|
||||
<string name="v4_4_verify_connection_security_desc">Vergelijk beveiligingscodes met je contacten.</string>
|
||||
<string name="icon_descr_contact_checked">Contact gecontroleerd</string>
|
||||
<string name="notification_contact_connected">Verbonden</string>
|
||||
<string name="display_name_connecting">Verbinden…</string>
|
||||
<string name="connection_local_display_name">verbinding <xliff:g id="connection ID" example="1">%1$d</xliff:g></string>
|
||||
<string name="connection_error">Verbindingsfout</string>
|
||||
<string name="group_member_status_introduced">verbinden (geïntroduceerd)</string>
|
||||
<string name="group_member_status_intro_invitation">verbinden (introductie uitnodiging)</string>
|
||||
<string name="display_name_connection_established">verbinding gemaakt</string>
|
||||
<string name="connection_request_sent">Verbindingsverzoek verzonden!</string>
|
||||
<string name="connection_timeout">Time-out verbinding</string>
|
||||
<string name="share_one_time_link">"Maak een eenmalige uitnodigings link"</string>
|
||||
<string name="create_address">Adres aanmaken</string>
|
||||
<string name="create_group_link">Groeps link maken</string>
|
||||
<string name="create_group">Maak een geheime groep aan</string>
|
||||
<string name="database_will_be_encrypted">Database wordt versleuteld.</string>
|
||||
<string name="group_member_status_creator">Starter</string>
|
||||
<string name="delete_address__question">Adres verwijderen\?</string>
|
||||
<string name="database_passphrase_and_export">Database wachtwoord zin & exporteren</string>
|
||||
<string name="passphrase_is_different">De wachtwoord zin van de database verschilt van de wachtwoord zin die is opgeslagen in de keystore.</string>
|
||||
<string name="ttl_d">%dd</string>
|
||||
<string name="delete_verb">Verwijderen</string>
|
||||
<string name="delete_after">Verwijderen na</string>
|
||||
<string name="connect_via_link_verb">Verbind</string>
|
||||
<string name="server_connected">verbonden</string>
|
||||
<string name="server_connecting">Verbinden</string>
|
||||
<string name="connect_via_contact_link">Verbinden via contact link\?</string>
|
||||
<string name="connect_via_group_link">Verbinden via groeps link\?</string>
|
||||
<string name="connect_via_invitation_link">Verbinden via uitnodigings link\?</string>
|
||||
<string name="notification_preview_mode_contact">Contact naam</string>
|
||||
<string name="notification_preview_somebody">Contact verborgen:</string>
|
||||
<string name="image_decoding_exception_title">Decodeerfout</string>
|
||||
<string name="maximum_supported_file_size">De momenteel maximaal ondersteunde bestandsgrootte is <xliff:g id="maxFileSize">%1$s</xliff:g>.</string>
|
||||
<string name="delete_contact_all_messages_deleted_cannot_undo_warning">Contact en alle berichten worden verwijderd - dit kan niet ongedaan worden gemaakt!</string>
|
||||
<string name="icon_descr_server_status_connected">Verbonden</string>
|
||||
<string name="confirm_verb">Bevestigen</string>
|
||||
<string name="connect_via_link_or_qr">Maak verbinding via link / QR-code</string>
|
||||
<string name="copied">Gekopieerd naar het klembord</string>
|
||||
<string name="contribute">Bijdragen</string>
|
||||
<string name="configure_ICE_servers">ICE-servers configureren</string>
|
||||
<string name="network_session_mode_entity">Verbinding</string>
|
||||
<string name="core_build_timestamp">Core gebouwd op: %s</string>
|
||||
<string name="core_version">Core versie: v%s</string>
|
||||
<string name="callstate_connected">verbonden</string>
|
||||
<string name="callstate_connecting">Verbinden…</string>
|
||||
<string name="decentralized">Gedecentraliseerd</string>
|
||||
<string name="create_your_profile">Maak je profiel aan</string>
|
||||
<string name="ttl_day">%d dag</string>
|
||||
<string name="ttl_days">%d dagen</string>
|
||||
<string name="encrypted_with_random_passphrase">De database is versleuteld met een willekeurige wachtwoord zin, u kunt deze wijzigen.</string>
|
||||
<string name="database_encryption_will_be_updated">De wachtwoord zin voor database codering wordt bijgewerkt en opgeslagen in de sleutel kluis.</string>
|
||||
<string name="database_will_be_encrypted_and_passphrase_stored">De database wordt gecodeerd en de wachtwoord zin wordt opgeslagen in de Keystore.</string>
|
||||
<string name="database_passphrase_will_be_updated">De wachtwoordzin voor databasecodering wordt bijgewerkt.</string>
|
||||
<string name="database_error">Database fout</string>
|
||||
<string name="database_passphrase_is_required">Databases wachtwoord zin is vereist om de chat te openen.</string>
|
||||
<string name="contact_already_exists">Contact bestaat al</string>
|
||||
<string name="icon_descr_call_connecting">Oproep verbinden</string>
|
||||
<string name="button_create_group_link">Maak link</string>
|
||||
<string name="smp_server_test_connect">Verbind</string>
|
||||
<string name="connection_error_auth">Verbindingsfout (AUTH)</string>
|
||||
<string name="smp_server_test_create_queue">Maak een wachtrij</string>
|
||||
<string name="auth_confirm_credential">Bevestig uw inloggegevens</string>
|
||||
<string name="contact_connection_pending">Verbinden…</string>
|
||||
<string name="group_connection_pending">Verbinden…</string>
|
||||
<string name="icon_descr_context">Context icon</string>
|
||||
<string name="copy_verb">Kopiëren</string>
|
||||
<string name="clear_chat_question">Wis gesprek</string>
|
||||
<string name="icon_descr_close_button">Sluiten</string>
|
||||
<string name="clear_chat_button">Chat wissen</string>
|
||||
<string name="alert_title_contact_connection_pending">Contact is nog niet verbonden!</string>
|
||||
<string name="delete_contact_menu_action">Verwijderen</string>
|
||||
<string name="delete_group_menu_action">Verwijderen</string>
|
||||
<string name="clear_verification">Verwijderd verificatie</string>
|
||||
<string name="connect_button">Verbind</string>
|
||||
<string name="connect_via_link">Maak verbinding via link</string>
|
||||
<string name="create_one_time_link">Maak een eenmalige uitnodigings link</string>
|
||||
<string name="colored">gekleurd</string>
|
||||
<string name="callstatus_connecting">Oproep verbinden…</string>
|
||||
<string name="contact_requests">Contact verzoeken</string>
|
||||
<string name="create_profile_button">Maak</string>
|
||||
<string name="create_profile">Maak een profiel aan</string>
|
||||
<string name="delete_address">Adres verwijderen</string>
|
||||
<string name="connect_calls_via_relay">Verbinden via relais</string>
|
||||
<string name="status_contact_has_e2e_encryption">contact heeft e2e encryptie</string>
|
||||
<string name="status_contact_has_no_e2e_encryption">contact heeft geen e2e-encryptie</string>
|
||||
<string name="set_password_to_export_desc">De database is versleuteld met een willekeurige wachtwoordzin. Wijzig dit voordat u exporteert.</string>
|
||||
<string name="database_passphrase">Database-wachtwoordzin</string>
|
||||
<string name="confirm_new_passphrase">Bevestig nieuwe wachtwoordzin…</string>
|
||||
<string name="current_passphrase">Huidige wachtwoordzin…</string>
|
||||
<string name="database_encrypted">Database versleuteld!</string>
|
||||
<string name="rcv_group_event_member_connected">verbonden</string>
|
||||
<string name="archive_created_on_ts">Gemaakt op <xliff:g id="archive_ts">%1$s</xliff:g></string>
|
||||
<string name="group_member_status_complete">compleet</string>
|
||||
<string name="clear_contacts_selection_button">Duidelijk</string>
|
||||
<string name="group_member_status_connected">verbonden</string>
|
||||
<string name="group_member_status_connecting">Verbinden</string>
|
||||
<string name="group_member_status_accepted">verbinden (geaccepteerd)</string>
|
||||
<string name="group_member_status_announced">verbinden (aangekondigd)</string>
|
||||
<string name="info_row_connection">Verbinding</string>
|
||||
<string name="create_secret_group_title">Maak een geheime groep aan</string>
|
||||
<string name="info_row_database_id">Database ID</string>
|
||||
<string name="chat_preferences_contact_allows">Contact staat toe</string>
|
||||
<string name="contact_preferences">Contact voorkeuren</string>
|
||||
<string name="contacts_can_mark_messages_for_deletion">Contact personen kunnen berichten markeren voor verwijdering; u kunt ze wel bekijken.</string>
|
||||
<string name="theme_dark">Donker</string>
|
||||
<string name="chat_preferences_default">standaard (%s)</string>
|
||||
<string name="delete_chat_archive_question">Chat archief verwijderen\?</string>
|
||||
<string name="delete_archive">Archief verwijderen</string>
|
||||
<string name="delete_contact_question">Verwijder contact\?</string>
|
||||
<string name="delete_chat_profile_question">Chat profiel verwijderen\?</string>
|
||||
<string name="full_deletion">Verwijderen voor iedereen</string>
|
||||
<string name="delete_link">Link verwijderen</string>
|
||||
<string name="conn_level_desc_direct">direct</string>
|
||||
<string name="settings_section_title_develop">ONTWIKKELEN</string>
|
||||
<string name="settings_section_title_device">APPARAAT</string>
|
||||
<string name="delete_files_and_media_all">Verwijder alle bestanden</string>
|
||||
<string name="delete_messages_after">Berichten verwijderen na</string>
|
||||
<string name="direct_messages">Privéberichten</string>
|
||||
<string name="ttl_month">%d maand</string>
|
||||
<string name="delete_image">Verwijder afbeelding</string>
|
||||
<string name="delete_database">Database verwijderen</string>
|
||||
<string name="rcv_group_event_group_deleted">verwijderde groep</string>
|
||||
<string name="delete_files_and_media_question">Bestanden en media verwijderen\?</string>
|
||||
<string name="delete_group_question">Groep verwijderen\?</string>
|
||||
<string name="delete_message__question">Verwijder bericht\?</string>
|
||||
<string name="delete_messages">Verwijder berichten</string>
|
||||
<string name="smp_server_test_delete_queue">Wachtrij verwijderen</string>
|
||||
<string name="delete_files_and_media_for_all_users">Verwijder bestanden voor alle chatprofielen</string>
|
||||
<string name="for_me_only">Verwijder voor mij</string>
|
||||
<string name="button_delete_group">Groep verwijderen</string>
|
||||
<string name="delete_link_question">Link verwijderen\?</string>
|
||||
<string name="delete_pending_connection__question">Wachtende verbinding verwijderen\?</string>
|
||||
<string name="desktop_scan_QR_code_from_app_via_scan_QR_code">💻 desktop: scan weergegeven QR-code vanuit de app, via <b>Scan QR-code</b>.</string>
|
||||
<string name="settings_developer_tools">Ontwikkel gereedschap</string>
|
||||
<string name="auth_device_authentication_is_disabled_turning_off">Apparaatverificatie is uitgeschakeld. SimpleX Lock uitschakelen.</string>
|
||||
<string name="display_name">Weergavenaam</string>
|
||||
<string name="auth_device_authentication_is_not_enabled_you_can_turn_on_in_settings_once_enabled">Apparaatverificatie is niet ingeschakeld. Je kunt SimpleX Lock inschakelen via Instellingen zodra je apparaatverificatie hebt ingeschakeld.</string>
|
||||
<string name="direct_messages_are_prohibited_in_chat">Privéberichten tussen leden zijn in deze groep verboden.</string>
|
||||
<string name="total_files_count_and_size">%d bestand(en) met een totale grootte van %s</string>
|
||||
<string name="ttl_hour">%d uur</string>
|
||||
<string name="no_call_on_lock_screen">Uitzetten</string>
|
||||
<string name="v4_4_disappearing_messages">Verdwijnende berichten</string>
|
||||
<string name="disappearing_prohibited_in_this_chat">Verdwijnende berichten zijn verboden in deze chat.</string>
|
||||
<string name="auth_disable_simplex_lock">SimpleX Lock uitschakelen</string>
|
||||
<string name="timed_messages">Verdwijnende berichten</string>
|
||||
<string name="smp_server_test_disconnect">verbinding verbreken</string>
|
||||
<string name="icon_descr_server_status_disconnected">verbinding verbroken</string>
|
||||
<string name="display_name__field">Weergavenaam:</string>
|
||||
<string name="display_name_cannot_contain_whitespace">Weergavenaam mag geen spatie bevatten.</string>
|
||||
<string name="ttl_min">%d min</string>
|
||||
<string name="ttl_months">%d maanden</string>
|
||||
<string name="failed_to_create_user_title">Fout bij aanmaken van profiel!</string>
|
||||
<string name="ttl_s">%ds</string>
|
||||
<string name="button_delete_contact">Verwijder contact</string>
|
||||
<string name="smp_servers_delete_server">Server verwijderen</string>
|
||||
<string name="disappearing_messages_are_prohibited">Verdwijnende berichten zijn verboden in deze groep.</string>
|
||||
<string name="ttl_sec">%d sec</string>
|
||||
<string name="ttl_m">%dm</string>
|
||||
<string name="ttl_mth">%dmth</string>
|
||||
<string name="ttl_hours">%d uren</string>
|
||||
<string name="ttl_h">%dh</string>
|
||||
<string name="users_delete_question">Chat profiel verwijderen\?</string>
|
||||
<string name="users_delete_profile_for">Chat profiel verwijderen voor</string>
|
||||
<string name="deleted_description">verwijderd</string>
|
||||
<string name="simplex_link_mode_description">Beschrijving</string>
|
||||
<string name="error_receiving_file">Fout bij ontvangen van bestand</string>
|
||||
<string name="error_joining_group">Fout bij lid worden van groep</string>
|
||||
<string name="error_deleting_group">Fout bij verwijderen van groep</string>
|
||||
<string name="error_deleting_contact">Fout bij het verwijderen van contact</string>
|
||||
<string name="error_deleting_contact_request">Fout bij verwijderen van contact verzoek</string>
|
||||
<string name="full_name__field">Volledige naam:</string>
|
||||
<string name="error_importing_database">Fout bij het importeren van de chat database</string>
|
||||
<string name="encrypt_database_question">Database versleutelen\?</string>
|
||||
<string name="alert_title_no_group">Groep niet gevonden!</string>
|
||||
<string name="group_display_name_field">Weergave naam groep:</string>
|
||||
<string name="failed_to_create_user_duplicate_title">Dubbele weergavenaam!</string>
|
||||
<string name="error_sending_message">Fout bij verzenden van bericht</string>
|
||||
<string name="failed_to_active_user_title">Fout bij wisselen van profiel!</string>
|
||||
<string name="error_changing_address">Fout bij wijzigen van adres</string>
|
||||
<string name="error_deleting_pending_contact_connection">Fout bij het verwijderen van in behandeling zijnde contact verbinding</string>
|
||||
<string name="error_deleting_user">Fout bij het verwijderen van gebruikers profiel</string>
|
||||
<string name="auth_enable_simplex_lock">SimpleX Lock inschakelen</string>
|
||||
<string name="hide_verb">Verbergen</string>
|
||||
<string name="icon_descr_edited">bewerkt</string>
|
||||
<string name="for_everybody">Voor iedereen</string>
|
||||
<string name="icon_descr_server_status_error">Fout</string>
|
||||
<string name="icon_descr_email">Email</string>
|
||||
<string name="edit_image">Bewerk afbeelding</string>
|
||||
<string name="exit_without_saving">Afsluiten zonder op te slaan</string>
|
||||
<string name="full_name_optional__prompt">Volledige naam (optioneel)</string>
|
||||
<string name="encrypted_video_call">e2e versleuteld videogesprek</string>
|
||||
<string name="allow_accepting_calls_from_lock_screen">Schakel oproepen vanaf het vergrendelscherm in via Instellingen.</string>
|
||||
<string name="icon_descr_hang_up">Ophangen</string>
|
||||
<string name="settings_section_title_help">HELP</string>
|
||||
<string name="settings_experimental_features">Experimentele functies</string>
|
||||
<string name="error_starting_chat">Fout bij het starten van de chat</string>
|
||||
<string name="export_database">Database exporteren</string>
|
||||
<string name="error_deleting_database">Fout bij het verwijderen van de chat database</string>
|
||||
<string name="error_exporting_chat_database">Fout bij het exporteren van de chat database</string>
|
||||
<string name="error_stopping_chat">Fout bij het stoppen van de chat</string>
|
||||
<string name="files_and_media_section">Bestanden en media</string>
|
||||
<string name="error_changing_message_deletion">Fout bij wijzigen van instelling</string>
|
||||
<string name="error_encrypting_database">Fout bij het versleutelen van de database</string>
|
||||
<string name="file_with_path">Bestand: %s</string>
|
||||
<string name="enter_passphrase">Voer wachtwoordzin in…</string>
|
||||
<string name="icon_descr_group_inactive">Groep inactief</string>
|
||||
<string name="alert_message_group_invitation_expired">Groeps uitnodiging is niet meer geldig, deze is verwijderd door de afzender.</string>
|
||||
<string name="snd_group_event_group_profile_updated">groeps profiel bijgewerkt</string>
|
||||
<string name="group_member_status_group_deleted">groep verwijderd</string>
|
||||
<string name="icon_descr_expand_role">Vouw de rolselectie uit</string>
|
||||
<string name="delete_group_for_all_members_cannot_undo_warning">Groep wordt verwijderd voor alle leden - dit kan niet ongedaan worden gemaakt!</string>
|
||||
<string name="error_creating_link_for_group">Fout bij maken van groeps link</string>
|
||||
<string name="error_deleting_link_for_group">Fout bij verwijderen groeps link</string>
|
||||
<string name="group_link">Groeps link</string>
|
||||
<string name="error_changing_role">Fout bij wisselen van rol</string>
|
||||
<string name="error_removing_member">Fout bij verwijderen van lid</string>
|
||||
<string name="info_row_group">Groep</string>
|
||||
<string name="group_full_name_field">Volledige naam groep:</string>
|
||||
<string name="group_preferences">Groeps voorkeuren</string>
|
||||
<string name="feature_enabled">ingeschakeld</string>
|
||||
<string name="feature_enabled_for_contact">ingeschakeld voor contact</string>
|
||||
<string name="feature_enabled_for_you">voor u ingeschakeld</string>
|
||||
<string name="group_members_can_delete">Groeps leden kunnen verzonden berichten onherroepelijk verwijderen.</string>
|
||||
<string name="group_members_can_send_dms">Groeps leden kunnen directe berichten sturen.</string>
|
||||
<string name="group_members_can_send_voice">Groeps leden kunnen spraak berichten verzenden.</string>
|
||||
<string name="v4_5_transport_isolation_descr">Per chatprofiel (standaard) of per verbinding (BETA).</string>
|
||||
<string name="v4_5_multiple_chat_profiles_descr">Verschillende namen, avatars en transportisolatie.</string>
|
||||
<string name="v4_4_french_interface">Franse interface</string>
|
||||
<string name="error_saving_group_profile">Fout bij opslaan van groeps profiel</string>
|
||||
<string name="encrypted_audio_call">e2e versleutelde audio-oproep</string>
|
||||
<string name="status_e2e_encrypted">e2e versleuteld</string>
|
||||
<string name="edit_verb">Bewerk</string>
|
||||
<string name="enable_automatic_deletion_question">Automatisch verwijderen van berichten aanzetten\?</string>
|
||||
<string name="enter_correct_passphrase">Voer de juiste wachtwoordzin in.</string>
|
||||
<string name="button_edit_group_profile">Groepsprofiel bewerken</string>
|
||||
<string name="network_option_enable_tcp_keep_alive">Schakel TCP-keep-alive in</string>
|
||||
<string name="encrypt_database">Versleutelen</string>
|
||||
<string name="error_adding_members">Fout bij het toevoegen van lid (leden)</string>
|
||||
<string name="smp_servers_enter_manually">Voer de server handmatig in</string>
|
||||
<string name="error_accepting_contact_request">Fout bij het accepteren van een contactverzoek</string>
|
||||
<string name="group_invitation_expired">Groeps uitnodiging verlopen</string>
|
||||
<string name="icon_descr_file">Bestand</string>
|
||||
<string name="section_title_for_console">VOOR CONSOLE</string>
|
||||
<string name="group_profile_is_stored_on_members_devices">Groeps proces wordt opgeslagen op de apparaten van de leden, niet op de servers.</string>
|
||||
<string name="notification_preview_mode_hidden">Verborgen</string>
|
||||
<string name="delete_group_for_self_cannot_undo_warning">De groep wordt voor u verwijderd - dit kan niet ongedaan worden gemaakt!</string>
|
||||
<string name="hide_notification">Verbergen</string>
|
||||
<string name="server_error">fout</string>
|
||||
<string name="file_will_be_received_when_contact_is_online">Het bestand wordt ontvangen wanneer uw contact persoon online is, even geduld a.u.b. of controleer later!</string>
|
||||
<string name="error_saving_file">Fout bij opslaan van bestand</string>
|
||||
<string name="file_not_found">Bestand niet gevonden</string>
|
||||
<string name="file_saved">Bestand opgeslagen</string>
|
||||
<string name="from_gallery_button">Van Galerij</string>
|
||||
<string name="error_saving_ICE_servers">Fout bij opslaan van ICE-servers</string>
|
||||
<string name="callstate_ended">geëindigd</string>
|
||||
<string name="group_members_can_send_disappearing">Groeps leden kunnen verdwijnende berichten sturen.</string>
|
||||
<string name="ttl_week">%d week</string>
|
||||
<string name="ttl_w">%dw</string>
|
||||
<string name="ttl_weeks">%d weken</string>
|
||||
<string name="v4_2_group_links">Groeps links</string>
|
||||
<string name="encrypted_database">Versleutelde database</string>
|
||||
<string name="error_with_info">Fout: %s</string>
|
||||
<string name="error_creating_address">Fout bij aanmaken van adres</string>
|
||||
<string name="icon_descr_help">help</string>
|
||||
<string name="icon_descr_flip_camera">Flip-camera</string>
|
||||
<string name="error_saving_smp_servers">Fout bij opslaan van SMP-servers</string>
|
||||
<string name="error_setting_network_config">Fout bij updaten van netwerk configuratie</string>
|
||||
<string name="failed_to_parse_chat_title">Kan chat niet laden</string>
|
||||
<string name="failed_to_parse_chats_title">Kan chats niet laden</string>
|
||||
<string name="simplex_link_mode_full">Volledige link</string>
|
||||
<string name="integrity_msg_duplicate">dubbel bericht</string>
|
||||
<string name="invalid_connection_link">Ongeldige verbindingslink</string>
|
||||
<string name="service_notifications_disabled">Onmiddelijke meldingen zijn uitgeschakeld!</string>
|
||||
<string name="service_notifications">Onmiddellijke berichten!</string>
|
||||
<string name="notification_preview_new_message">nieuw bericht</string>
|
||||
<string name="icon_descr_image_snd_complete">Afbeelding verzonden</string>
|
||||
<string name="live_message">Live bericht!</string>
|
||||
<string name="if_you_received_simplex_invitation_link_you_can_open_in_browser">Als je een uitnodigingslink voor <xliff:g id="appName">SimpleX Chat</xliff:g> hebt ontvangen, kun je deze in je browser openen:</string>
|
||||
<string name="onboarding_notifications_mode_subtitle">Dit kan later worden gewijzigd via instellingen.</string>
|
||||
<string name="join_group_question">Deelnemen aan groep\?</string>
|
||||
<string name="icon_descr_add_members">Nodig leden uit</string>
|
||||
<string name="no_contacts_selected">Geen contacten geselecteerd</string>
|
||||
<string name="v4_4_live_messages">Live berichten</string>
|
||||
<string name="icon_descr_instant_notifications">Onmiddellijke meldingen</string>
|
||||
<string name="notification_new_contact_request">Nieuw contactverzoek</string>
|
||||
<string name="auth_log_in_using_credential">Log in met uw inloggegevens</string>
|
||||
<string name="message_delivery_error_desc">Hoogstwaarschijnlijk heeft dit contact de verbinding met jou verwijderd.</string>
|
||||
<string name="delete_message_cannot_be_undone_warning">Bericht wordt verwijderd - dit kan niet ongedaan worden gemaakt!</string>
|
||||
<string name="large_file">Groot bestand!</string>
|
||||
<string name="mark_read">Markeer gelezen</string>
|
||||
<string name="mark_unread">Markeer als ongelezen</string>
|
||||
<string name="mute_chat">Stom</string>
|
||||
<string name="if_you_cannot_meet_in_person_scan_QR_in_video_call_or_ask_for_invitation_link">Als u elkaar niet persoonlijk kunt ontmoeten, kunt u <b> de QR-code scannen in het videogesprek </b>, of uw contactpersoon kan een uitnodigingslink delen.</string>
|
||||
<string name="if_you_cannot_meet_in_person_show_QR_in_video_call_or_via_another_channel">Als je elkaar niet persoonlijk kunt ontmoeten, <b>toon je de QR-code in het videogesprek</b> of deel je de link.</string>
|
||||
<string name="invalid_QR_code">Ongeldige QR-code</string>
|
||||
<string name="icon_descr_more_button">Meer</string>
|
||||
<string name="incorrect_code">Onjuiste beveiligingscode!</string>
|
||||
<string name="mark_code_verified">Markeer geverifieerd</string>
|
||||
<string name="how_to_use_simplex_chat">Hoe te gebruiken</string>
|
||||
<string name="markdown_help">Markdown-hulp</string>
|
||||
<string name="markdown_in_messages">Markdown in berichten</string>
|
||||
<string name="network_settings_title">Netwerkinstellingen</string>
|
||||
<string name="how_to_use_markdown">Markdown gebruiken</string>
|
||||
<string name="italic">cursief</string>
|
||||
<string name="how_it_works">Hoe het werkt</string>
|
||||
<string name="callstatus_missed">gemiste oproep</string>
|
||||
<string name="how_simplex_works">Hoe <xliff:g id="appName">SimpleX</xliff:g> werkt</string>
|
||||
<string name="many_people_asked_how_can_it_deliver">Veel mensen vroegen: <i>als <xliff:g id="appName">SimpleX</xliff:g> geen gebruikers-ID\'s heeft, hoe kan het dan berichten bezorgen\?</i></string>
|
||||
<string name="incoming_audio_call">Inkomende audio-oproep</string>
|
||||
<string name="incoming_video_call">Inkomend videogesprek</string>
|
||||
<string name="ignore">Negeren</string>
|
||||
<string name="status_no_e2e_encryption">geen e2e-encryptie</string>
|
||||
<string name="import_database_question">Chatdatabase importeren\?</string>
|
||||
<string name="chat_item_ttl_none">nooit</string>
|
||||
<string name="no_received_app_files">Geen ontvangen of verzonden bestanden</string>
|
||||
<string name="alert_title_group_invitation_expired">Uitnodiging verlopen!</string>
|
||||
<string name="rcv_group_event_member_left">Verlaten</string>
|
||||
<string name="group_member_status_left">Verlaten</string>
|
||||
<string name="group_member_status_invited">uitgenodigd</string>
|
||||
<string name="button_leave_group">Groep verlaten</string>
|
||||
<string name="info_row_local_name">Lokale naam</string>
|
||||
<string name="group_unsupported_incognito_main_profile_sent">Incognitomodus wordt hier niet ondersteund - uw hoofdprofiel wordt naar groepsleden verzonden</string>
|
||||
<string name="users_delete_data_only">Alleen lokale profielgegevens</string>
|
||||
<string name="message_deletion_prohibited_in_chat">Het onomkeerbaar verwijderen van berichten is verboden in deze groep.</string>
|
||||
<string name="v4_3_improved_privacy_and_security_desc">App-scherm verbergen in de recente apps.</string>
|
||||
<string name="settings_section_title_incognito">Incognito modus</string>
|
||||
<string name="messages_section_title">Berichten</string>
|
||||
<string name="new_passphrase">Nieuwe wachtwoordzin…</string>
|
||||
<string name="keychain_error">Keychain fout</string>
|
||||
<string name="join_group_button">Word lid van</string>
|
||||
<string name="leave_group_question">Groep verlaten\?</string>
|
||||
<string name="new_member_role">Nieuwe ledenrol</string>
|
||||
<string name="no_contacts_to_add">Geen contacten om toe te voegen</string>
|
||||
<string name="incognito_info_allows">Het maakt het mogelijk om veel anonieme verbindingen te hebben zonder enige gedeelde gegevens tussen hen in een enkel chatprofiel.</string>
|
||||
<string name="theme_light">Licht</string>
|
||||
<string name="chat_preferences_no">Nee</string>
|
||||
<string name="v4_5_multiple_chat_profiles">Meerdere chatprofielen</string>
|
||||
<string name="v4_5_italian_interface">Italiaanse interface</string>
|
||||
<string name="v4_5_message_draft">Concept bericht</string>
|
||||
<string name="v4_5_reduced_battery_usage_descr">Meer verbeteringen volgen snel!</string>
|
||||
<string name="button_add_members">Nodig leden uit</string>
|
||||
<string name="notification_display_mode_hidden_desc">Verberg contact en bericht</string>
|
||||
<string name="turn_off_battery_optimization">Om het te gebruiken <b xmlns:xliff=\"urn:oasis:names:tc:xliff:document:1.2\">batterijoptimalisatie uitschakelen</b> voor <xliff:g xmlns:xliff=\"urn:oasis: names:tc:xliff:document:1.2\" id=\"appName\">SimpleX</xliff:g> in het volgende dialoogvenster. Anders worden de meldingen uitgeschakeld.</string>
|
||||
<string name="if_you_choose_to_reject_the_sender_will_not_be_notified">Als u ervoor kiest om te weigeren, wordt de afzender NIET op de hoogte gesteld.</string>
|
||||
<string name="onboarding_notifications_mode_service">Onmiddellijk</string>
|
||||
<string name="rcv_group_event_member_added">uitgenodigd <xliff:g id="member profile" example="alice (Alice)">%1$s</xliff:g></string>
|
||||
<string name="group_invitation_item_description">uitnodiging voor groep <xliff:g id="group_name">%1$s</xliff:g></string>
|
||||
<string name="invite_to_group_button">Uitnodigen voor groep</string>
|
||||
<string name="join_group_incognito_button">Doe incognito mee</string>
|
||||
<string name="group_preview_join_as">lid worden als %s</string>
|
||||
<string name="alert_text_skipped_messages_it_can_happen_when">Het kan gebeuren wanneer:
|
||||
\n1. De berichten op de server verlopen als ze 30 dagen niet zijn ontvangen,
|
||||
\n2. De server die u gebruikt om de berichten van deze contactpersoon te ontvangen, is bijgewerkt en opnieuw opgestart.
|
||||
\n3. De verbinding is verbroken.
|
||||
\nMaak verbinding met de ontwikkelaars via Instellingen om de updates over de servers te ontvangen.
|
||||
\nWe zullen serverredundantie toevoegen om verloren berichten te voorkomen.</string>
|
||||
<string name="joining_group">Deel nemen aan groep</string>
|
||||
<string name="leave_group_button">Verlaten</string>
|
||||
<string name="group_member_role_member">lid</string>
|
||||
<string name="image_descr_link_preview">link voorbeeldafbeelding</string>
|
||||
<string name="member_info_section_title_member">LID</string>
|
||||
<string name="settings_section_title_messages">BERICHTEN</string>
|
||||
<string name="mobile_tap_open_in_mobile_app_then_tap_connect_in_app">📱 mobiel: tik op <b>Openen in mobiele app</b> en tik vervolgens op <b>Verbinden</b> in de app.</string>
|
||||
<string name="member_will_be_removed_from_group_cannot_be_undone">Lid wordt uit de groep verwijderd - dit kan niet ongedaan worden gemaakt!</string>
|
||||
<string name="message_delivery_error_title">Fout bij bezorging van bericht</string>
|
||||
<string name="delete_message_mark_deleted_warning">Bericht wordt gemarkeerd voor verwijdering. De ontvanger(s) kunnen dit bericht onthullen.</string>
|
||||
<string name="network_status">Netwerk status</string>
|
||||
<string name="image_descr">Afbeelding</string>
|
||||
<string name="image_saved">Afbeelding opgeslagen in Galerij</string>
|
||||
<string name="import_database_confirmation">Importeren</string>
|
||||
<string name="import_database">Database importeren</string>
|
||||
<string name="v4_3_improved_privacy_and_security">Verbeterde privacy en veiligheid</string>
|
||||
<string name="image_will_be_received_when_contact_is_online">De afbeelding wordt ontvangen wanneer uw contact online is, even geduld a.u.b. of kijk later!</string>
|
||||
<string name="incognito_info_protects">De incognitomodus beschermt de privacy van uw hoofdprofielnaam en -afbeelding - voor elk nieuw contact wordt een nieuw willekeurig profiel gemaakt.</string>
|
||||
<string name="new_database_archive">Nieuw database-archief</string>
|
||||
<string name="no_details">geen details</string>
|
||||
<string name="conn_level_desc_indirect">indirect (<xliff:g id="conn_level">%1$s</xliff:g>)</string>
|
||||
<string name="thousand_abbreviation">k</string>
|
||||
<string name="notification_preview_mode_message">Bericht tekst</string>
|
||||
<string name="settings_notification_preview_title">Meldingsvoorbeeld</string>
|
||||
<string name="settings_notifications_mode_title">Meldingsservice</string>
|
||||
<string name="notifications">Meldingen</string>
|
||||
<string name="invalid_contact_link">Ongeldige link!</string>
|
||||
<string name="smp_servers_invalid_address">Ongeldig serveradres!</string>
|
||||
<string name="install_simplex_chat_for_terminal">Installeer <xliff:g id="appNameFull">SimpleX Chat</xliff:g> voor terminal</string>
|
||||
<string name="how_to">Hoe</string>
|
||||
<string name="how_to_use_your_servers">Hoe u uw servers gebruikt</string>
|
||||
<string name="network_and_servers">Netwerk & servers</string>
|
||||
<string name="enter_one_ICE_server_per_line">ICE-servers (één per lijn)</string>
|
||||
<string name="ensure_ICE_server_address_are_correct_format_and_unique">Zorg ervoor dat WebRTC ICE-serveradressen de juiste indeling hebben, regelgescheiden zijn en niet gedupliceerd zijn.</string>
|
||||
<string name="network_disable_socks_info">Als u bevestigt, kunnen de berichtenservers uw IP-adres zien en uw provider - met welke servers u verbinding maakt.</string>
|
||||
<string name="network_use_onion_hosts_no">Nee</string>
|
||||
<string name="immune_to_spam_and_abuse">Immuun voor spam en misbruik</string>
|
||||
<string name="make_private_connection">Maak een privéverbinding</string>
|
||||
<string name="message_deletion_prohibited">Het onomkeerbaar verwijderen van berichten is verboden in deze chat.</string>
|
||||
<string name="new_in_version">Nieuw in %s</string>
|
||||
<string name="v4_3_voice_messages_desc">Max 40 seconden, direct ontvangen.</string>
|
||||
<string name="v4_3_improved_server_configuration">Verbeterde serverconfiguratie</string>
|
||||
<string name="v4_3_irreversible_message_deletion">Onomkeerbare berichtverwijdering</string>
|
||||
<string name="rcv_group_event_invited_via_your_group_link">uitgenodigd via je groepslink</string>
|
||||
<string name="incognito">Incognito</string>
|
||||
<string name="icon_descr_call_missed">Gemiste oproep</string>
|
||||
<string name="description_via_contact_address_link_incognito">incognito via link naar contactadres</string>
|
||||
<string name="description_via_group_link_incognito">incognito via groepslink</string>
|
||||
<string name="description_via_one_time_link_incognito">incognito via eenmalige link</string>
|
||||
<string name="invalid_chat">ongeldige chat</string>
|
||||
<string name="invalid_data">onjuiste data</string>
|
||||
<string name="invalid_message_format">ongeldig berichtformaat</string>
|
||||
<string name="display_name_invited_to_connect">uitgenodigd om te verbinden</string>
|
||||
<string name="live">LIVE</string>
|
||||
<string name="ensure_smp_server_address_are_correct_format_and_unique">Zorg ervoor dat SMP-serveradressen de juiste indeling hebben, regelgescheiden zijn en niet gedupliceerd zijn.</string>
|
||||
<string name="marked_deleted_description">gemarkeerd als verwijderd</string>
|
||||
<string name="please_check_correct_link_and_maybe_ask_for_a_new_one">Controleer of u de juiste link heeft gebruikt of vraag uw contactpersoon om u een andere te sturen.</string>
|
||||
<string name="image_descr_profile_image">profielfoto</string>
|
||||
<string name="privacy_redefined">Privacy opnieuw gedefinieerd</string>
|
||||
<string name="privacy_and_security">Privacy en beveiliging</string>
|
||||
<string name="network_error_desc">Controleer uw netwerkverbinding met <xliff:g id="serverHost" example="smp.simplex.im">%1$s</xliff:g> en probeer het opnieuw.</string>
|
||||
<string name="error_smp_test_certificate">Mogelijk is de certificaatvingerafdruk in het serveradres onjuist</string>
|
||||
<string name="periodic_notifications">Periodieke meldingen</string>
|
||||
<string name="auth_open_chat_console">Chatconsole openen</string>
|
||||
<string name="toast_permission_denied">Geen toestemming!</string>
|
||||
<string name="icon_descr_profile_image_placeholder">profielafbeelding tijdelijke aanduiding</string>
|
||||
<string name="one_time_link">Eenmalige uitnodigingslink</string>
|
||||
<string name="paste_button">Plakken</string>
|
||||
<string name="smp_servers_preset_address">Vooraf ingesteld serveradres</string>
|
||||
<string name="network_use_onion_hosts_no_desc_in_alert">Onion hosts worden niet gebruikt.</string>
|
||||
<string name="onboarding_notifications_mode_title">Privé meldingen</string>
|
||||
<string name="paste_the_link_you_received">Plak de ontvangen link</string>
|
||||
<string name="onboarding_notifications_mode_periodic">Periodiek</string>
|
||||
<string name="open_verb">Open</string>
|
||||
<string name="open_simplex_chat_to_accept_call">Open <xliff:g id="appNameFull">SimpleX Chat</xliff:g> om de oproep te accepteren</string>
|
||||
<string name="call_connection_peer_to_peer">peer-to-peer</string>
|
||||
<string name="notifications_will_be_hidden">Meldingen worden alleen afgeleverd totdat de app stopt!</string>
|
||||
<string name="restore_passphrase_not_found_desc">Wachtwoordzin niet gevonden in Keystore, voer deze handmatig in. Dit kan zijn gebeurd als u de gegevens van de app hebt hersteld met een back-uptool. Als dit niet het geval is, neem dan contact op met de ontwikkelaars.</string>
|
||||
<string name="users_delete_with_connections">Profiel- en serververbindingen</string>
|
||||
<string name="chat_preferences_off">uit</string>
|
||||
<string name="chat_preferences_on">aan</string>
|
||||
<string name="only_you_can_send_disappearing">Alleen jij kunt verdwijnende berichten verzenden.</string>
|
||||
<string name="only_your_contact_can_send_disappearing">Alleen uw contactpersoon kan verdwijnende berichten verzenden.</string>
|
||||
<string name="only_you_can_delete_messages">Alleen jij kunt berichten onomkeerbaar verwijderen (je contactpersoon kan ze markeren voor verwijdering).</string>
|
||||
<string name="feature_offered_item_with_param">aangeboden %s: %2s</string>
|
||||
<string name="old_database_archive">Oud database-archief</string>
|
||||
<string name="enter_correct_current_passphrase">Voer de juiste huidige wachtwoordzin in.</string>
|
||||
<string name="group_member_role_owner">eigenaar</string>
|
||||
<string name="network_option_ping_count">PING-telling</string>
|
||||
<string name="network_option_ping_interval">PING-interval</string>
|
||||
<string name="v4_5_message_draft_descr">Bewaar het laatste berichtconcept, met bijlagen.</string>
|
||||
<string name="v4_5_private_filenames">Privé bestandsnamen</string>
|
||||
<string name="images_limit_desc">Er kunnen slechts 10 afbeeldingen tegelijk worden verzonden</string>
|
||||
<string name="enter_passphrase_notification_title">Wachtwoordzin is nodig</string>
|
||||
<string name="feature_off">uit</string>
|
||||
<string name="add_contact">Eenmalige uitnodigingslink</string>
|
||||
<string name="network_use_onion_hosts_required_desc_in_alert">Onion hosts zijn vereist voor verbinding.</string>
|
||||
<string name="only_group_owners_can_change_prefs">Alleen groepseigenaren kunnen groepsvoorkeuren wijzigen.</string>
|
||||
<string name="only_stored_on_members_devices">(alleen opgeslagen door groepsleden)</string>
|
||||
<string name="paste_connection_link_below_to_connect">Plak de link die je hebt ontvangen in het vak hieronder om verbinding te maken met je contactpersoon.</string>
|
||||
<string name="smp_servers_preset_server">Vooraf ingestelde server</string>
|
||||
<string name="periodic_notifications_disabled">Periodieke meldingen zijn uitgeschakeld!</string>
|
||||
<string name="icon_descr_server_status_pending">In behandeling</string>
|
||||
<string name="only_group_owners_can_enable_voice">Alleen groepseigenaren kunnen spraakberichten inschakelen.</string>
|
||||
<string name="ask_your_contact_to_enable_voice">Vraag uw contactpersoon om het verzenden van spraakberichten in te schakelen.</string>
|
||||
<string name="ok">OK</string>
|
||||
<string name="network_use_onion_hosts_required_desc">Onion hosts zijn vereist voor verbinding.</string>
|
||||
<string name="network_use_onion_hosts_prefer_desc">Onion hosts worden gebruikt indien beschikbaar.</string>
|
||||
<string name="network_use_onion_hosts_prefer_desc_in_alert">Onion hosts worden gebruikt indien beschikbaar.</string>
|
||||
<string name="network_use_onion_hosts_no_desc">Onion hosts worden niet gebruikt.</string>
|
||||
<string name="opensource_protocol_and_code_anybody_can_run_servers">Open-source protocol en code – iedereen kan de servers draaien.</string>
|
||||
<string name="people_can_connect_only_via_links_you_share">Mensen kunnen alleen verbinding met u maken via de links die u deelt.</string>
|
||||
<string name="only_your_contact_can_delete">Alleen uw contactpersoon kan berichten onherroepelijk verwijderen (u kunt ze markeren voor verwijdering).</string>
|
||||
<string name="only_you_can_send_voice">Alleen jij kunt spraakberichten verzenden.</string>
|
||||
<string name="only_your_contact_can_send_voice">Alleen uw contactpersoon kan spraakberichten verzenden.</string>
|
||||
<string name="prohibit_message_deletion">Verbied het onomkeerbaar verwijderen van berichten.</string>
|
||||
<string name="feature_offered_item">aangeboden %s</string>
|
||||
<string name="store_passphrase_securely_without_recover">Sla de wachtwoordzin veilig op. Als u deze kwijtraakt, heeft u GEEN toegang tot de chat.</string>
|
||||
<string name="store_passphrase_securely">Bewaar de wachtwoordzin veilig, u kunt deze NIET wijzigen als u deze kwijtraakt.</string>
|
||||
<string name="open_chat">Chat openen</string>
|
||||
<string name="restore_database_alert_desc">Voer het vorige wachtwoord in na het herstellen van de databaseback-up. Deze actie kan niet ongedaan gemaakt worden.</string>
|
||||
<string name="icon_descr_call_pending_sent">Oproep in behandeling</string>
|
||||
<string name="simplex_link_mode_browser_warning">Het openen van de link in de browser kan de privacy en beveiliging van de verbinding verminderen. Niet-vertrouwde SimpleX-links worden rood weergegeven.</string>
|
||||
<string name="contact_developers">Werk de app bij en neem contact op met de ontwikkelaars.</string>
|
||||
<string name="only_client_devices_store_contacts_groups_e2e_encrypted_messages">Alleen client-apparaten slaan gebruikersprofielen, contacten, groepen en berichten op die zijn verzonden met <b>2-laags end-to-end-codering</b>.</string>
|
||||
</resources>
|
||||
@@ -1,2 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources></resources>
|
||||
@@ -1,2 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources></resources>
|
||||
@@ -1,259 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="a_plus_b">a + b</string>
|
||||
<string name="chat_item_ttl_day">1天</string>
|
||||
<string name="about_simplex">关于 SimpleX</string>
|
||||
<string name="all_group_members_will_remain_connected">所有群组成员将保持连接。</string>
|
||||
<string name="about_simplex_chat">关于 <xliff:g id="appNameFull">SimpleX Chat</xliff:g></string>
|
||||
<string name="above_then_preposition_continuation">以上,然后:</string>
|
||||
<string name="accept_contact_button">接受</string>
|
||||
<string name="accept_call_on_lock_screen">接受</string>
|
||||
<string name="accept_feature">接受</string>
|
||||
<string name="chat_item_ttl_month">1月</string>
|
||||
<string name="chat_item_ttl_week">1周</string>
|
||||
<string name="color_primary">强化</string>
|
||||
<string name="callstatus_accepted">已接受通话</string>
|
||||
<string name="accept">接受</string>
|
||||
<string name="network_enable_socks_info">通过 SOCKS 代理访问服务器在端口9050?允许该选项前必须开始代理。</string>
|
||||
<string name="smp_servers_add">添加服务器…</string>
|
||||
<string name="smp_servers_add_to_another_device">添加另一设备</string>
|
||||
<string name="group_member_role_admin">管理员</string>
|
||||
<string name="v4_3_improved_server_configuration_desc">扫描二维码来添加服务器。</string>
|
||||
<string name="network_settings">高级网络设置</string>
|
||||
<string name="accept_connection_request__question">接受连接请求?</string>
|
||||
<string name="accept_contact_incognito_button">接受隐身聊天</string>
|
||||
<string name="v4_2_group_links_desc">管理员可以创建链接以加入群组。</string>
|
||||
<string name="accept_requests">接受请求</string>
|
||||
<string name="smp_servers_preset_add">添加预设服务器</string>
|
||||
<string name="connect_via_link">通过链接连接</string>
|
||||
<string name="display_name_connection_established">已建立连接</string>
|
||||
<string name="connection_local_display_name">连接 <xliff:g id="connection ID" example="1">%1$d</xliff:g></string>
|
||||
<string name="connection_error">连接错误</string>
|
||||
<string name="connection_timeout">连接超时</string>
|
||||
<string name="contact_already_exists">联系人已存在</string>
|
||||
<string name="connection_error_auth">连接错误(AUTH)</string>
|
||||
<string name="answer_call">接听来电</string>
|
||||
<string name="delete_chat_profile_question">删除聊天资料?</string>
|
||||
<string name="delete_files_and_media_all">删除所有文件</string>
|
||||
<string name="messages_section_title">消息</string>
|
||||
<string name="delete_messages_after">在此后删除消息</string>
|
||||
<string name="settings_section_title_messages">消息</string>
|
||||
<string name="users_add">添加资料</string>
|
||||
<string name="users_delete_all_chats_deleted">所有聊天记录和消息将被删除——这一行为无法撤销!</string>
|
||||
<string name="clear_chat_warning">所有聊天记录和消息将被删除——这一行为无法撤销!只有您的消息会被删除。</string>
|
||||
<string name="allow_to_send_voice">允许发送语音消息。</string>
|
||||
<string name="allow_voice_messages_question">允许语音消息?</string>
|
||||
<string name="delete_verb">删除</string>
|
||||
<string name="delete_contact_menu_action">删除</string>
|
||||
<string name="delete_group_menu_action">删除</string>
|
||||
<string name="delete_address__question">删除地址?</string>
|
||||
<string name="delete_after">在此后删除</string>
|
||||
<string name="delete_archive">删除档案</string>
|
||||
<string name="deleted_description">已删除</string>
|
||||
<string name="delete_files_and_media_question">删除文件和媒体文件?</string>
|
||||
<string name="full_deletion">为所有人删除</string>
|
||||
<string name="for_me_only">为我删除</string>
|
||||
<string name="delete_files_and_media_for_all_users">为所有聊天资料删除文件</string>
|
||||
<string name="button_delete_group">删除群组</string>
|
||||
<string name="delete_group_question">删除群组?</string>
|
||||
<string name="delete_link">删除链接</string>
|
||||
<string name="delete_link_question">删除链接?</string>
|
||||
<string name="network_session_mode_entity">连接</string>
|
||||
<string name="connection_request_sent">已发送连接请求!</string>
|
||||
<string name="delete_message__question">删除消息?</string>
|
||||
<string name="delete_messages">删除消息</string>
|
||||
<string name="info_row_connection">连接</string>
|
||||
<string name="connect_via_invitation_link">通过邀请链接连接?</string>
|
||||
<string name="connect_via_contact_link">通过联系人链接连接?</string>
|
||||
<string name="connect_via_group_link">通过群组链接连接?</string>
|
||||
<string name="connect_via_link_or_qr">通过群组链接/二维码连接</string>
|
||||
<string name="connect_calls_via_relay">通过继电器连接</string>
|
||||
<string name="allow_your_contacts_irreversibly_delete">允许您的联系人不可撤回地删除已发送消息。</string>
|
||||
<string name="chat_preferences_contact_allows">联系人允许</string>
|
||||
<string name="allow_voice_messages_only_if">仅有您的联系人许可后才允许语音消息。</string>
|
||||
<string name="group_info_member_you">您: <xliff:g id="group_info_you">%1$s</xliff:g></string>
|
||||
<string name="allow_your_contacts_to_send_voice_messages">允许您的联系人发送语音消息。</string>
|
||||
<string name="chat_preferences_always">一直</string>
|
||||
<string name="notifications_mode_service">一直开启</string>
|
||||
<string name="allow_your_contacts_to_send_disappearing_messages">允许您的联系人发送限时消息。</string>
|
||||
<string name="app_version_code">应用程序构建:%s</string>
|
||||
<string name="all_your_contacts_will_remain_connected">所有联系人会保持连接。</string>
|
||||
<string name="allow_verb">允许</string>
|
||||
<string name="allow_direct_messages">允许直接发送消息给成员。</string>
|
||||
<string name="allow_to_send_disappearing">允许发送限时消息。</string>
|
||||
<string name="delete_address">删除地址</string>
|
||||
<string name="delete_chat_archive_question">删除聊天档案?</string>
|
||||
<string name="users_delete_question">删除聊天资料?</string>
|
||||
<string name="button_delete_contact">删除联系人</string>
|
||||
<string name="delete_contact_question">删除联系人?</string>
|
||||
<string name="rcv_group_event_group_deleted">已删除群组</string>
|
||||
<string name="delete_image">删除图片</string>
|
||||
<string name="allow_disappearing_messages_only_if">仅有您的联系人许可后才允许限时消息。</string>
|
||||
<string name="allow_irreversible_message_deletion_only_if">仅有您的联系人许可后才允许不可撤回消息移除。</string>
|
||||
<string name="allow_to_delete_messages">允许不可撤回地删除已发送消息。</string>
|
||||
<string name="users_delete_profile_for">为此删除聊天资料</string>
|
||||
<string name="delete_database">删除数据库</string>
|
||||
<string name="keychain_allows_to_receive_ntfs">在您重启应用程序或者更换密码后安卓密钥库系统用来安全地保存密码——来确保收到通知。</string>
|
||||
<string name="keychain_is_storing_securely">安卓密钥库系统用来安全地保存密码——来确保通知服务运作。</string>
|
||||
<string name="appearance_settings">外观</string>
|
||||
<string name="app_version_title">应用程序版本</string>
|
||||
<string name="full_backup">应用程序数据备份</string>
|
||||
<string name="settings_section_title_icon">应用程序图标</string>
|
||||
<string name="incognito_random_profile_from_contact_description">一个随机资料将被发送到收到您链接的联系人那里</string>
|
||||
<string name="app_version_name">应用程序版本:v%s</string>
|
||||
<string name="notifications_mode_off_desc">仅在运行时应用程序可以接受通知,不会启动后台服务</string>
|
||||
<string name="incognito_random_profile_description">一个随机资料将发送给您的联系人</string>
|
||||
<string name="auth_unavailable">身份验证不可用</string>
|
||||
<string name="auto_accept_images">自动接受图像</string>
|
||||
<string name="attach">附件</string>
|
||||
<string name="icon_descr_audio_call">语音通话</string>
|
||||
<string name="audio_call_no_encryption">语音通话(非端到端加密)</string>
|
||||
<string name="v4_2_auto_accept_contact_requests">自动接受联系人请求</string>
|
||||
<string name="integrity_msg_bad_hash">错误消息散列</string>
|
||||
<string name="integrity_msg_bad_id">错误消息 ID</string>
|
||||
<string name="settings_audio_video_calls">语音和视频通话</string>
|
||||
<string name="accept_automatically">自动地</string>
|
||||
<string name="turning_off_service_and_periodic">激活电池优化,关闭了后台服务和新消息的定期请求。您可以通过设置重新启用它们。</string>
|
||||
<string name="notifications_mode_service_desc">后台服务一直在运行——一旦有消息,就会显示通知。</string>
|
||||
<string name="icon_descr_audio_off">关闭音频</string>
|
||||
<string name="icon_descr_audio_on">开启音频</string>
|
||||
<string name="icon_descr_asked_to_receive">已要求接收图片</string>
|
||||
<string name="network_session_mode_user_description">一个单独的TCP连接(和SOCKS凭证)将被用于<b>,用于您在应用程序中的每个聊天资料</b> 。</string>
|
||||
<string name="network_session_mode_entity_description">每个联系人和群组成员</b> 将使用单独的 TCP 连接(和 SOCKS 凭证)<b>。
|
||||
\n<b>请注意</b>:如果您有很多连接,您的电池和流量消耗可能会大大增加,并且某些连接可能会失败。</string>
|
||||
<string name="back">返回</string>
|
||||
<string name="add_new_contact_to_create_one_time_QR_code"><b>添加新联系人</b>:为您的联系人创建一次性二维码。</string>
|
||||
<string name="onboarding_notifications_mode_off_desc"><b> 最适合电池 </b>。您只会在应用程序运行时收到通知,不会使用后台服务。</string>
|
||||
<string name="onboarding_notifications_mode_periodic_desc"><b> 适合于电池 </b>。后台服务每 10 分钟检查一次新消息。您可能会错过来电和紧急信息。</string>
|
||||
<string name="bold">加粗</string>
|
||||
<string name="both_you_and_your_contacts_can_delete">您和您的联系人都可以不可逆转地删除已发送的消息。</string>
|
||||
<string name="both_you_and_your_contact_can_send_disappearing">您和您的联系人都可以发送限时消息。</string>
|
||||
<string name="both_you_and_your_contact_can_send_voice">您和您的联系人都可以发送语音消息。</string>
|
||||
<string name="it_can_disabled_via_settings_notifications_still_shown"><b> 可以通过设置禁用它 </b> - 应用程序运行时仍会显示通知。</string>
|
||||
<string name="onboarding_notifications_mode_service_desc"><b> 使用更多电池 </b>!后台服务一直在运行——一旦收到消息,就会显示通知。</string>
|
||||
<string name="impossible_to_recover_passphrase"><b>请注意</b>:如果您丢失密码,您将无法恢复或者更改密码。</string>
|
||||
<string name="call_already_ended">通话已经结束!</string>
|
||||
<string name="scan_QR_code_to_connect_to_contact_who_shows_QR_code"><b>扫描二维码</b> :与向您展示二维码的联系人联系。</string>
|
||||
<string name="alert_title_cant_invite_contacts">无法邀请联系人!</string>
|
||||
<string name="invite_prohibited">无法邀请联系人!</string>
|
||||
<string name="cancel_verb">取消</string>
|
||||
<string name="callstatus_ended">通话结束 <xliff:g id="duration" example="01:15">%1$s</xliff:g></string>
|
||||
<string name="change_verb">更换</string>
|
||||
<string name="icon_descr_call_ended">通话结束</string>
|
||||
<string name="change_database_passphrase_question">更改数据库密码?</string>
|
||||
<string name="callstatus_error">通话错误</string>
|
||||
<string name="rcv_conn_event_switch_queue_phase_completed">为您更改地址</string>
|
||||
<string name="callstatus_in_progress">通话中</string>
|
||||
<string name="icon_descr_call_progress">通话进行中</string>
|
||||
<string name="callstatus_calling">呼叫中…</string>
|
||||
<string name="icon_descr_cancel_live_message">取消实时消息</string>
|
||||
<string name="settings_section_title_calls">通话</string>
|
||||
<string name="call_on_lock_screen">在锁定屏幕上通话:</string>
|
||||
<string name="icon_descr_cancel_image_preview">取消图片预览</string>
|
||||
<string name="feature_cancelled_item">已取消 %s</string>
|
||||
<string name="icon_descr_cancel_file_preview">取消文件预览</string>
|
||||
<string name="cannot_access_keychain">无法访问密钥库来保存数据库密码</string>
|
||||
<string name="cannot_receive_file">无法接收文件</string>
|
||||
<string name="database_initialization_error_title">无法初始化数据库</string>
|
||||
<string name="rcv_group_event_changed_member_role">将 %s 的角色更改为 %s</string>
|
||||
<string name="rcv_group_event_changed_your_role">将您的角色更改为 %s</string>
|
||||
<string name="change_role">改变角色</string>
|
||||
<string name="change_member_role_question">更改群组角色?</string>
|
||||
<string name="icon_descr_cancel_link_preview">取消链接预览</string>
|
||||
<string name="snd_conn_event_switch_queue_phase_changing_for_member">为 %s 更改地址…</string>
|
||||
<string name="rcv_conn_event_switch_queue_phase_changing">更改地址…</string>
|
||||
<string name="snd_conn_event_switch_queue_phase_changing">更改地址…</string>
|
||||
<string name="create_your_profile">创建您的资料</string>
|
||||
<string name="chat_database_deleted">聊天数据库已删除</string>
|
||||
<string name="chat_database_imported">聊天数据库已导入</string>
|
||||
<string name="keychain_error">钥匙串错误</string>
|
||||
<string name="chat_archive_section">聊天档案</string>
|
||||
<string name="chat_archive_header">聊天档案</string>
|
||||
<string name="chat_console">聊天控制台</string>
|
||||
<string name="chat_database_section">聊天数据库</string>
|
||||
<string name="chat_is_stopped_indication">聊天已停止</string>
|
||||
<string name="chat_is_running">聊天进行中</string>
|
||||
<string name="chat_is_stopped">聊天已停止</string>
|
||||
<string name="contact_preferences">联系人偏好设置</string>
|
||||
<string name="your_preferences">您的偏好设置</string>
|
||||
<string name="group_preferences">群组偏好设置</string>
|
||||
<string name="only_group_owners_can_change_prefs">只有群主可以改变群组偏好设置。</string>
|
||||
<string name="save_preferences_question">保存偏好设置?</string>
|
||||
<string name="set_group_preferences">设置群组偏好设置</string>
|
||||
<string name="privacy_redefined">重新定义隐私</string>
|
||||
<string name="v4_3_improved_privacy_and_security">改进的隐私和安全</string>
|
||||
<string name="incognito">隐身聊天</string>
|
||||
<string name="joining_group">加入群组</string>
|
||||
<string name="join_group_incognito_button">加入隐身聊天</string>
|
||||
<string name="settings_section_title_incognito">隐身聊天模式</string>
|
||||
<string name="group_unsupported_incognito_main_profile_sent">这里不支持隐身聊天模式——您的主要资料将被发送给群组成员</string>
|
||||
<string name="tap_to_start_new_chat">点击开始一个新聊天</string>
|
||||
<string name="incognito_random_profile">您的随机资料</string>
|
||||
<string name="description_via_contact_address_link_incognito">通过联系人地址链接隐身聊天</string>
|
||||
<string name="description_via_group_link_incognito">通过群组链接隐身聊天</string>
|
||||
<string name="description_you_shared_one_time_link_incognito">您分享了一次性链接隐身聊天</string>
|
||||
<string name="group_invitation_tap_to_join_incognito">点击以加入隐身聊天</string>
|
||||
<string name="group_main_profile_sent">您的聊天资料将被发送给群组成员</string>
|
||||
<string name="invite_prohibited_description">您正在尝试邀请与您共享隐身聊天资料的联系人加入您使用主要资料的群组</string>
|
||||
<string name="incognito_info_protects">隐身聊天模式可以保护您的主要资料名和头像的隐私——为每个新联系人创建一个新的随机资料。</string>
|
||||
<string name="alert_title_cant_invite_contacts_descr">您正在为该群组使用隐身聊天资料——为防止共享您的主要资料,邀请联系人是不允许的</string>
|
||||
<string name="your_profile_will_be_sent">您的聊天资料将被发送给您的联系人</string>
|
||||
<string name="description_via_one_time_link_incognito">通过一次性链接隐身聊天</string>
|
||||
<string name="only_group_owners_can_enable_voice">只有群主可以启用语音信息。</string>
|
||||
<string name="your_privacy">您的隐私设置</string>
|
||||
<string name="privacy_and_security">隐私和安全</string>
|
||||
<string name="smp_servers_save">保存服务器</string>
|
||||
<string name="incognito_info_allows">它允许在一个聊天资料中有多个匿名连接,而它们之间没有任何共享数据。</string>
|
||||
<string name="incognito_info_find">要查找用于隐身聊天连接的资料,点击聊天顶部的联系人或群组名。</string>
|
||||
<string name="incognito_info_share">当您与某人共享隐身聊天资料时,该资料将用于他们邀请您加入的群组。</string>
|
||||
<string name="v4_3_improved_server_configuration">改进的服务器配置</string>
|
||||
<string name="icon_descr_email">电邮</string>
|
||||
<string name="edit_image">编辑图片</string>
|
||||
<string name="button_edit_group_profile">编辑群组资料</string>
|
||||
<string name="error_encrypting_database">加密数据库错误</string>
|
||||
<string name="error_exporting_chat_database">导出聊天数据库错误</string>
|
||||
<string name="error_importing_database">导入聊天数据库错误</string>
|
||||
<string name="error_joining_group">加入群组错误</string>
|
||||
<string name="error_deleting_user">删除用户资料错误</string>
|
||||
<string name="passphrase_is_different">数据库密码不同于保存在密钥库中的密码。</string>
|
||||
<string name="database_encryption_will_be_updated">数据库加密密码将被更新并存储在密钥库中。</string>
|
||||
<string name="database_will_be_encrypted_and_passphrase_stored">数据库将被加密,密码存储在密钥库中。</string>
|
||||
<string name="restore_passphrase_not_found_desc">在密匙库中没有找到密码,请手动输入。如果你使用备份工具恢复了应用程序的数据,可能会发生这种情况。如果不是这种情况,请联系开发者。</string>
|
||||
<string name="remove_passphrase_from_keychain">从密钥库中删除密码?</string>
|
||||
<string name="save_passphrase_in_keychain">在密钥库中保存密码</string>
|
||||
<string name="simplex_service_notification_title"><xliff:g id="appNameFull">SimpleX Chat</xliff:g> 服务</string>
|
||||
<string name="settings_notifications_mode_title">通知服务</string>
|
||||
<string name="confirm_new_passphrase">确认新口令…</string>
|
||||
<string name="group_member_status_complete">完整的</string>
|
||||
<string name="group_member_status_connected">连接的</string>
|
||||
<string name="chat_preferences">聊天偏好</string>
|
||||
<string name="settings_section_title_chats">聊天</string>
|
||||
<string name="clear_verb">清空</string>
|
||||
<string name="clear_chat_menu_action">清空</string>
|
||||
<string name="clear_chat_button">清除聊天记录</string>
|
||||
<string name="clear_chat_question">清除聊天记录?</string>
|
||||
<string name="chat_with_developers">与开发者聊天</string>
|
||||
<string name="clear_contacts_selection_button">清除</string>
|
||||
<string name="colored">有色</string>
|
||||
<string name="callstate_connected">连接的</string>
|
||||
<string name="connect_button">连接</string>
|
||||
<string name="connect_via_link_verb">连接</string>
|
||||
<string name="choose_file">选择文件</string>
|
||||
<string name="network_session_mode_user">聊天资料</string>
|
||||
<string name="v4_5_transport_isolation_descr">按聊天资料(默认)或按连接(BETA)。</string>
|
||||
<string name="smp_servers_check_address">检查服务器地址并再试一次。</string>
|
||||
<string name="clear_verification">清晰的验证</string>
|
||||
<string name="icon_descr_close_button">关闭键</string>
|
||||
<string name="configure_ICE_servers">配置ICE服务器</string>
|
||||
<string name="confirm_verb">确认</string>
|
||||
<string name="auth_confirm_credential">确认您的证书</string>
|
||||
<string name="server_connected">连接的</string>
|
||||
<string name="icon_descr_server_status_connected">连接的</string>
|
||||
<string name="smp_server_test_connect">连接</string>
|
||||
<string name="notification_contact_connected">连接的</string>
|
||||
<string name="server_connecting">连接的</string>
|
||||
<string name="group_member_status_connecting">连接的</string>
|
||||
<string name="notifications_mode_periodic_desc">每10分钟检查一次新消息,最长1分钟</string>
|
||||
<string name="rcv_group_event_member_connected">连接的</string>
|
||||
<string name="v4_4_verify_connection_security_desc">与你的联系人比较安全码</string>
|
||||
</resources>
|
||||
@@ -1,21 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="chat_item_ttl_week">1 個星期</string>
|
||||
<string name="accept_requests">接受請求</string>
|
||||
<string name="a_plus_b">a + b</string>
|
||||
<string name="about_simplex">關於 SimpleX</string>
|
||||
<string name="accept_call_on_lock_screen">接受</string>
|
||||
<string name="accept_feature">接受</string>
|
||||
<string name="chat_item_ttl_day">1 天</string>
|
||||
<string name="chat_item_ttl_month">1 個月</string>
|
||||
<string name="accept_contact_button">接受</string>
|
||||
<string name="about_simplex_chat">關於<xliff:g id="appNameFull">SimpleX Chat</xliff:g></string>
|
||||
<string name="accept_connection_request__question">接受新連線要求\?</string>
|
||||
<string name="callstatus_accepted">已接受電話</string>
|
||||
<string name="network_enable_socks_info">要在端口 9050 啟動 SOCKS 代理伺服器嗎\? 在啟用這個選項之前,必須先啟動代理伺服器。</string>
|
||||
<string name="group_member_role_admin">管理員</string>
|
||||
<string name="above_then_preposition_continuation">以上,然後:</string>
|
||||
<string name="smp_servers_preset_add">加入預設伺服器</string>
|
||||
<string name="smp_servers_add">新增伺服器…</string>
|
||||
<string name="accept">接受</string>
|
||||
</resources>
|
||||
@@ -1,49 +0,0 @@
|
||||
buildscript {
|
||||
Properties localProperties = new Properties()
|
||||
if (rootProject.file('local.properties').canRead()) {
|
||||
localProperties.load(rootProject.file("local.properties").newDataInputStream())
|
||||
}
|
||||
|
||||
ext {
|
||||
compose_version = localProperties['compose_version'] ?: '1.2.0-beta02'
|
||||
kotlin_version = localProperties['kotlin_version'] ?: '1.6.21'
|
||||
gradle_plugin_version = localProperties['gradle_plugin_version'] ?: '7.2.0'
|
||||
|
||||
// Name that will be shown for debug build. By default it is from strings
|
||||
app_name = localProperties['app_name'] ?: "@string/app_name"
|
||||
// Whether the app is debuggable or not. Specify `false` if you want good performance in debug builds
|
||||
enable_debuggable = localProperties['debuggable'] ?: true
|
||||
// Ending part of package name.
|
||||
// Provide, for example, `application_id_suffix=.debug` in local.properties
|
||||
// to allow debug & release versions to coexist
|
||||
application_id_suffix = localProperties['application_id_suffix'] ?: ''
|
||||
|
||||
// Compression level for debug AND release apk. 0 = disable compression. Max is 9
|
||||
compression_level = localProperties['compression_level'] ?: '0'
|
||||
|
||||
// NOTE: If you need a different version of something, provide it in `local.properties`
|
||||
// like so: compose_version=123, or gradle_plugin_version=1.2.3, etc
|
||||
}
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
}
|
||||
dependencies {
|
||||
classpath "com.android.tools.build:gradle:$gradle_plugin_version"
|
||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
||||
classpath "org.jetbrains.kotlin:kotlin-serialization:1.3.2"
|
||||
|
||||
// NOTE: Do not place your application dependencies here; they belong
|
||||
// in the individual module build.gradle files
|
||||
}
|
||||
}// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
||||
plugins {
|
||||
id 'com.android.application' version "$gradle_plugin_version" apply false
|
||||
id 'com.android.library' version "$gradle_plugin_version" apply false
|
||||
id 'org.jetbrains.kotlin.android' version "$kotlin_version" apply false
|
||||
id 'org.jetbrains.kotlin.plugin.serialization' version "$kotlin_version"
|
||||
}
|
||||
|
||||
task clean(type: Delete) {
|
||||
delete rootProject.buildDir
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
pluginManagement {
|
||||
repositories {
|
||||
gradlePluginPortal()
|
||||
google()
|
||||
mavenCentral()
|
||||
}
|
||||
}
|
||||
dependencyResolutionManagement {
|
||||
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
}
|
||||
}
|
||||
rootProject.name = "SimpleX"
|
||||
include ':app'
|
||||
1
apps/ios/README.md
Normal file
@@ -0,0 +1 @@
|
||||
# SimpleX Chat iOS app
|
||||
@@ -14,9 +14,28 @@ class AppDelegate: NSObject, UIApplicationDelegate {
|
||||
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
|
||||
logger.debug("AppDelegate: didFinishLaunchingWithOptions")
|
||||
application.registerForRemoteNotifications()
|
||||
if #available(iOS 17.0, *) { trackKeyboard() }
|
||||
return true
|
||||
}
|
||||
|
||||
@available(iOS 17.0, *)
|
||||
private func trackKeyboard() {
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillShow), name: UIResponder.keyboardWillShowNotification, object: nil)
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillHide), name: UIResponder.keyboardWillHideNotification, object: nil)
|
||||
}
|
||||
|
||||
@available(iOS 17.0, *)
|
||||
@objc func keyboardWillShow(_ notification: Notification) {
|
||||
if let keyboardFrame: NSValue = notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue {
|
||||
ChatModel.shared.keyboardHeight = keyboardFrame.cgRectValue.height
|
||||
}
|
||||
}
|
||||
|
||||
@available(iOS 17.0, *)
|
||||
@objc func keyboardWillHide(_ notification: Notification) {
|
||||
ChatModel.shared.keyboardHeight = 0
|
||||
}
|
||||
|
||||
func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
|
||||
let token = deviceToken.map { String(format: "%02hhx", $0) }.joined()
|
||||
logger.debug("AppDelegate: didRegisterForRemoteNotificationsWithDeviceToken \(token)")
|
||||
@@ -42,7 +61,7 @@ class AppDelegate: NSObject, UIApplicationDelegate {
|
||||
m.notificationMode != .off {
|
||||
if let verification = ntfData["verification"] as? String,
|
||||
let nonce = ntfData["nonce"] as? String {
|
||||
if let token = ChatModel.shared.deviceToken {
|
||||
if let token = m.deviceToken {
|
||||
logger.debug("AppDelegate: didReceiveRemoteNotification: verification, confirming \(verification)")
|
||||
Task {
|
||||
do {
|
||||
@@ -62,7 +81,7 @@ class AppDelegate: NSObject, UIApplicationDelegate {
|
||||
}
|
||||
} else if let checkMessages = ntfData["checkMessages"] as? Bool, checkMessages {
|
||||
logger.debug("AppDelegate: didReceiveRemoteNotification: checkMessages")
|
||||
if appStateGroupDefault.get().inactive {
|
||||
if appStateGroupDefault.get().inactive && m.ntfEnablePeriodic {
|
||||
receiveMessages(completionHandler)
|
||||
} else {
|
||||
completionHandler(.noData)
|
||||
@@ -76,7 +95,7 @@ class AppDelegate: NSObject, UIApplicationDelegate {
|
||||
}
|
||||
|
||||
func applicationWillTerminate(_ application: UIApplication) {
|
||||
logger.debug("AppDelegate: applicationWillTerminate")
|
||||
logger.debug("DEBUGGING: AppDelegate: applicationWillTerminate")
|
||||
ChatModel.shared.filesToDelete.forEach {
|
||||
removeFile($0)
|
||||
}
|
||||
|
||||
23
apps/ios/Shared/Assets.xcassets/decentralized-light.imageset/Contents.json
vendored
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "decentralized_light.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"filename" : "decentralized_light@2x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"filename" : "decentralized_light@3x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||