Compare commits
785 Commits
eac8592536
...
v0.5.0
| Author | SHA1 | Date | |
|---|---|---|---|
| 45a460a11f | |||
| 7655befcc3 | |||
| c96880d437 | |||
| 21c8079f49 | |||
| 175b2d5f2a | |||
|
|
38ef7efac9 | ||
|
|
aca226b764 | ||
| 2c5fbd8d5d | |||
| 17295320c3 | |||
| 37390eea97 | |||
| ab71754faf | |||
| 17396c3db2 | |||
| 742f8b6d8d | |||
| 161dd5cca8 | |||
| 4dfacc759b | |||
| da57b113f8 | |||
| 4a1d30e8c4 | |||
| 4e4a5c1b52 | |||
| 4e097d8286 | |||
| bfe2a0f58a | |||
|
|
13ece41a8f | ||
|
|
f35cb04359 | ||
|
|
08d11f3e9f | ||
| 63253e9cae | |||
| 26dafb4e9e | |||
| 111c66aa6a | |||
| 5b03fc9b47 | |||
| 672ae736d3 | |||
| e37db3d00c | |||
| 919ed6f0a1 | |||
| 79867e4eaa | |||
| 99600bafb0 | |||
| 59aad79aa0 | |||
| 6fc4672a6e | |||
| 478c639ae7 | |||
| 7f2c978736 | |||
| f472239102 | |||
| f2844122c0 | |||
| 7781b12dac | |||
| 1e795b8b10 | |||
| 7d0896d84f | |||
| fc0aade0ae | |||
| d969622675 | |||
| 23839b6ec6 | |||
| 0b120c1e9c | |||
| e84d8f9455 | |||
| c4973910f4 | |||
| 4ed57bc1d4 | |||
| e8125b661b | |||
| c6b7bba5ee | |||
| cf5ee6c703 | |||
| 63038b77bc | |||
| 7be67a9551 | |||
| 0dc44572b1 | |||
| 1de6bab86e | |||
| 0852e0efbf | |||
|
|
8fe11f30f2 | ||
| 8b0a863949 | |||
| bde689b1b3 | |||
| 2ff2402078 | |||
| cb47fa210a | |||
|
|
599e56875d | ||
|
|
74569ccfd6 | ||
| a009a68941 | |||
| 37455c54e6 | |||
| 16dded8dee | |||
| fa92ba1974 | |||
| 3011af4f0e | |||
| 8a98815fd7 | |||
| 018f40d6db | |||
| 1d7e55c086 | |||
| d57e133d0d | |||
| 191996c5d4 | |||
| bcef2dd818 | |||
| 0358bf15a0 | |||
| 301ff664a8 | |||
| 865846af9a | |||
| 96b2c5f3c4 | |||
| db5e4e3c1f | |||
| e3cd4aa3dd | |||
| fbca84ae2f | |||
| 6034ce5ba4 | |||
| 2e71ffebd4 | |||
| 3ca7d63e8c | |||
| ff2ca7397f | |||
| 843a1171b7 | |||
|
|
f0057e5487 | ||
| d3914e3269 | |||
| 431641e248 | |||
| e259b3ee63 | |||
| e34bbde27e | |||
| f84593ec19 | |||
| 49425440a5 | |||
| ee9332f6f2 | |||
| d06de37924 | |||
| 0872b2a134 | |||
| 95a6a458db | |||
| 112048df36 | |||
| 24a26fe454 | |||
| fe846468ee | |||
| be299ac90f | |||
|
|
549e61a168 | ||
| f7e7c17598 | |||
|
|
26bcaccd81 | ||
|
|
df5810a347 | ||
| 071ea6db5d | |||
| 01f46d4cdc | |||
| 2baf852a9a | |||
| 7f5d8c353a | |||
| f3a5683e13 | |||
| 4323561774 | |||
| 6225ac65d9 | |||
| 178648a5ee | |||
|
|
ef931c566f | ||
| 78dbf5473e | |||
| edcb95ac6f | |||
| 2fc23876e6 | |||
| 293908c40c | |||
|
|
a3d4883406 | ||
|
|
532f0b1bb2 | ||
|
|
a98fe0d9f3 | ||
| 7919d11b00 | |||
| ac5a2c9dd2 | |||
|
|
a7c3bd507f | ||
| 06d132b0b0 | |||
| 837787fdaf | |||
| 9545b3d9cc | |||
|
|
fba2cf86ae | ||
| d17a656d64 | |||
| a5082762b5 | |||
| 4160143f6e | |||
| 9fd6f847fe | |||
| f65f8ec541 | |||
|
|
af481bf465 | ||
|
|
e6ae8182c2 | ||
| 31d0c9c9ec | |||
| de3bf12c18 | |||
| 9d2f6b29f7 | |||
| ee764d2213 | |||
|
|
de7b9adfa6 | ||
| 8c53055341 | |||
| f0225eed4c | |||
| b57bae878f | |||
|
1a83ff37ff
|
|||
| e205785246 | |||
| 3edda978fb | |||
|
|
ebabb0e445 | ||
|
|
e4f98bb82f | ||
|
|
56d6f97168 | ||
|
|
b0067f055f | ||
| 117d263e25 | |||
| 63e66e5dce | |||
| 140a16ec0c | |||
| 44b95bbb5b | |||
| 4eeaa8c37b | |||
| c95f2efd65 | |||
| 9dbf4b5467 | |||
|
|
bd094dfc9a | ||
|
|
7e9d7d6281 | ||
|
|
2cf781459d | ||
|
|
4ff9e3c81e | ||
|
|
e337f1c579 | ||
|
|
7c840334e1 | ||
| 0a120f1073 | |||
|
|
269738e618 | ||
|
|
06eab1d6f5 | ||
|
|
008e816d70 | ||
|
|
2bbbc164b5 | ||
|
|
11f4fd9932 | ||
|
|
a1451c6fbf | ||
| 1e94910ebd | |||
|
|
993da40297 | ||
|
|
09684b1268 | ||
|
|
0abb89cf38 | ||
| 63d251a7f1 | |||
| 799af75bcc | |||
| 8966c490fe | |||
| 4c09dbab60 | |||
| 6fd5c33b0a | |||
|
|
7880f51b76 | ||
| 30e8a885bb | |||
| 42c695a2a1 | |||
|
|
f0a79ca0e0 | ||
| 3ef2f3b8d6 | |||
| ae995b8670 | |||
| 75c2f103bd | |||
| bc7958559a | |||
|
|
11228a0de0 | ||
| b6fe5cc29b | |||
| cb34b51149 | |||
| 6c9421a21a | |||
| 684a3a658d | |||
| 0ab7cccef6 | |||
| c405717bcc | |||
| 7dcd14ef3a | |||
| 4f033438ed | |||
| 21e40c38ca | |||
| 53e3b3c561 | |||
| d6416c77b8 | |||
| 9ade143352 | |||
| dc05876ac7 | |||
| d691d63353 | |||
| b666a06252 | |||
| 27b209b4d8 | |||
| d0cdb2cebe | |||
| 2123f5b51f | |||
| 93511da3f1 | |||
| 267eef2a55 | |||
| 8f69b9ff3d | |||
| 0d182cc4e5 | |||
|
|
06dcd5491b | ||
|
|
3641be4f56 | ||
|
|
18e28c3bbf | ||
|
|
62e39bf066 | ||
| f6bfbff62c | |||
| dd0cb88841 | |||
| 23ed1f6b1a | |||
| d12154b4ba | |||
| b625ee5c4e | |||
| 4e48962fae | |||
| 21832042df | |||
| d48c8371e4 | |||
| 67f6fb8236 | |||
| 369cc3e823 | |||
| 4857245a96 | |||
|
|
2f39d4b60b | ||
|
|
0528aca3ad | ||
| d99e5d8801 | |||
| 5cda06db24 | |||
| c93c3140cf | |||
| f17bba7282 | |||
| fe3dbd265e | |||
| e6eee134bd | |||
| 1f5c75a647 | |||
| d70527d65f | |||
| efb3f4b371 | |||
| 71f5e8f0b4 | |||
| a13bdd2e2b | |||
| 7fbc1ba812 | |||
| 7f864f1d25 | |||
|
|
ef3c11e870 | ||
|
|
c20bc964c3 | ||
| dd2629d073 | |||
| e5fa199925 | |||
| 675a647a8d | |||
| f845c4134c | |||
| 6442e9cab5 | |||
| 0629a3d5bd | |||
| 62d18588d7 | |||
|
|
4ee191e238 | ||
| 351de5ee93 | |||
| dae8ddb3b5 | |||
| 631a62e8ce | |||
| 59877a3e60 | |||
| 08da843d50 | |||
| 949781003a | |||
| 4338c7a777 | |||
| 86de5cd22d | |||
| bf754a4e51 | |||
| 8913977c0a | |||
| f4be336e39 | |||
| 836fe1bf87 | |||
| 7bca95203e | |||
| 059a22cbe8 | |||
| 2740692772 | |||
| c0008fece9 | |||
| d7bb54a088 | |||
| eb41b3f0f7 | |||
| e3cbc47cc8 | |||
| 75767d26b4 | |||
| a01667a8f7 | |||
| e4dec4168c | |||
| 59f1a3a289 | |||
| 9c8aec6543 | |||
| 7c8a368d73 | |||
| 0bda382e40 | |||
| 330b4dd69f | |||
| 7a7e43eb3c | |||
| 5e797d6b54 | |||
| 1b3dd0634b | |||
| b1bdacb834 | |||
| a4b9485019 | |||
| 20489fbb25 | |||
| a2fa000a31 | |||
| 343f0e7dae | |||
| f0f13097c0 | |||
| 0025e83be5 | |||
| ffb8e9f3fc | |||
| 8081931c55 | |||
| 792276d06a | |||
| 58edc256fd | |||
| f30d04a593 | |||
| cc42f32b21 | |||
| 353623c5ae | |||
| a09c30a076 | |||
| 3bfd72fde1 | |||
| 39e6b4a48b | |||
| 32b2e35d42 | |||
| 8e1bcbfd1d | |||
| 336a6d56cd | |||
| a283454cae | |||
| 8b16a8a37b | |||
| 727a1a3423 | |||
| 0c42c117a0 | |||
| d795cb717e | |||
| 1d5d1fdf86 | |||
| d795c34dab | |||
| b38f5c139f | |||
| b623f32fbf | |||
| 19fd079436 | |||
| 7d70a96533 | |||
| dce6e34289 | |||
| 881f080916 | |||
| 051687535b | |||
| 0b420933e0 | |||
| 0b3876c3f0 | |||
| 9711d45a7a | |||
| 8dcba94de7 | |||
| 226dca8c1a | |||
| ad01a7e3e3 | |||
| adde5a4134 | |||
| 9ae1807225 | |||
| e7f8446c02 | |||
| 7b05bf200c | |||
| e992cb309f | |||
| 0f138678ec | |||
| 35658e611a | |||
| 2a25cd44cf | |||
| 29053df245 | |||
| 78ad02ec80 | |||
| e3f2ef22a6 | |||
| f884e181e3 | |||
| e69d7ed0a2 | |||
| d65e11a3ea | |||
| 294d0ee02c | |||
| 6f4abebb32 | |||
| 5d83796b37 | |||
| a06c697fe3 | |||
| 5de2a8b6af | |||
| 7234f67c42 | |||
| 972f5079f9 | |||
| 27d4ed1781 | |||
| 5f074ef695 | |||
| d0f60519fd | |||
| cd7c495cb7 | |||
| 59317d45f9 | |||
| 7c2c9f978d | |||
| d540f0c2f2 | |||
| 340bbb7ca8 | |||
| 0aaffd1249 | |||
| 04be2e8c88 | |||
| 57dbe83901 | |||
| 60c5328eb0 | |||
| 189d9ca9cd | |||
| 5d797b1e66 | |||
| 2f1a40b4d9 | |||
| 02c0cd5af0 | |||
| f2a70cd137 | |||
| 8d88c25f05 | |||
| c1c5625441 | |||
| 462e800907 | |||
| faa5ee2c4f | |||
| 5dad5730ce | |||
| 5017187927 | |||
| 14e7f72bd3 | |||
| 9ef67f5788 | |||
| 79226f6ca8 | |||
| c8c0239e36 | |||
| f1be10bf8c | |||
| 18c3c9d324 | |||
| 4825fe881d | |||
| 081d20fe50 | |||
| c1a66711db | |||
| b113e78423 | |||
| 470e8aac9c | |||
| 39babfbadd | |||
| 86f7e63f65 | |||
| ecd2a71981 | |||
| 2ece9e6209 | |||
| 9310b9c305 | |||
| abad9897b8 | |||
| 0cfffff94c | |||
| 6c53103345 | |||
| 346ef66bca | |||
| e092201030 | |||
| 3c14521ca0 | |||
| 4b43427bf0 | |||
| b7f39fe8ed | |||
| 1f64569bc2 | |||
| 7c56383601 | |||
| 2de50b012b | |||
| 1de90e3ce1 | |||
| 64a175819f | |||
| 4cc507832c | |||
| fd1e14e4cd | |||
|
|
a78db354ab | ||
|
|
a86d83eeba | ||
|
|
02e73ade5e | ||
| 9d0a84b317 | |||
|
|
0cf237914b | ||
|
|
398c23fccb | ||
| 8f68292dfd | |||
|
|
8ef62e7ff1 | ||
|
|
99257f4b28 | ||
|
|
9f529a3a1c | ||
| 8178a0dd8a | |||
| 0f250b6eae | |||
| 716579cc5e | |||
| 25caf3f4a6 | |||
|
|
1c1b598768 | ||
|
|
7cbb56dc2c | ||
| 7f41ec2aac | |||
|
|
ac5fc38de6 | ||
| 1f3c568d0c | |||
|
|
2a186377df | ||
|
|
d529974cd9 | ||
|
|
f378c60bf5 | ||
|
|
e4523a2d33 | ||
|
|
4aacd36c59 | ||
|
|
a291d9ab07 | ||
|
|
9d73fc3a94 | ||
|
|
8a33d88e31 | ||
|
|
6650686d48 | ||
|
|
8570997cb0 | ||
|
|
31ee7b919b | ||
|
|
30f6ecd2f8 | ||
|
|
9e3700001d | ||
|
|
2928602e8d | ||
|
|
09fc55d2c7 | ||
| b391425d48 | |||
| 3b21486647 | |||
| 641ac01b33 | |||
| 233370b448 | |||
| 45bff04329 | |||
| 6d32387e6c | |||
| 4f51cf1f80 | |||
| 46f7e5beaa | |||
| fee39f56fa | |||
| a3e8758dbd | |||
|
|
2b6ed19847 | ||
|
|
34971950ad | ||
|
|
29b22b7dd9 | ||
| 8bc4771345 | |||
| 314c8f8d18 | |||
| dd3e47e492 | |||
| 7f90f3315a | |||
| ceb43c0f0f | |||
| e225cab90a | |||
| 87793a032c | |||
| b3227129d5 | |||
| 5861c7f8cb | |||
| 1181d1c526 | |||
| b3c02324aa | |||
| 3664b5f8c5 | |||
| d58bf448ef | |||
| 95d1e1ed38 | |||
| bbaa41f389 | |||
| 20bff17c74 | |||
| 31a7d18905 | |||
| c4f04b73be | |||
| 188c6199c9 | |||
| 62413eb8e4 | |||
| 1c4697caa7 | |||
| 785272ba21 | |||
| d28e669b5f | |||
| fe3b07aa2f | |||
| a21ecf9bbf | |||
| 55113543dd | |||
| 76041671eb | |||
| be2d4ec29f | |||
| dfa221768c | |||
| 9b2278a0ff | |||
| 24b0a0c7bb | |||
| 023ad574a8 | |||
| 74772dc6b5 | |||
| 8fc7734827 | |||
| 43659b01bd | |||
| de2e2f3987 | |||
| 28591a6787 | |||
| e78dae0950 | |||
| 5b86f69444 | |||
| 92a7d30e43 | |||
| fa311bfb95 | |||
| c1988a9bcd | |||
| 27185b21b5 | |||
| bad4295aec | |||
| b891f29e11 | |||
| 35a752e565 | |||
| 6c5189744a | |||
| 81e9a7d420 | |||
| 3a01025471 | |||
| e652ecca44 | |||
| c244d54d22 | |||
| cff9000d6b | |||
| dc8804de3a | |||
| 92467630cd | |||
| 452734a433 | |||
| 49c7b18d57 | |||
| f7665403b9 | |||
| 9ae047b2d0 | |||
| 4523d87028 | |||
| c34c0ffd0f | |||
| a179d0f6cc | |||
| 6c1b7c54d0 | |||
| bbb59ac2cc | |||
| f16d33decd | |||
| c4e5504c1d | |||
| 0fb8230e50 | |||
| 86be724246 | |||
| 27b3ad0da5 | |||
| 25167ed078 | |||
| 7fb0cf139b | |||
| 6e8d54c91b | |||
| a6191fd8af | |||
| bfeea6ffa5 | |||
| 48451385e9 | |||
| 0e894f84cc | |||
| 0ca12232a8 | |||
| c2d28efe62 | |||
| 0496c38496 | |||
| dd4c481c4f | |||
| 7f1b5233e8 | |||
|
|
41aae3cab9 | ||
| 9838fbc95f | |||
| f5c59823bf | |||
| 241a8b4d53 | |||
| 25d193e930 | |||
| e6924cc02d | |||
| 60985c6b37 | |||
| a015399b57 | |||
| 4b6c7998f3 | |||
| 26312e313f | |||
| b63b5d7fd2 | |||
| ca2943a94d | |||
| 32a4cd9361 | |||
| 2320e4ed17 | |||
| dee479a918 | |||
| 6895ef1e32 | |||
| 5c51eefa3e | |||
| 0d7ae321a7 | |||
| b4063a64e0 | |||
| 65154f2f5c | |||
| 19a22bd0d1 | |||
| a7da7baf5a | |||
| a344a94112 | |||
| f44861fead | |||
| 1c4a30ebb4 | |||
| 70e2ca3d3e | |||
| 0d4aee1625 | |||
| ad6aa33b7c | |||
| 284b5fa4df | |||
| b9aac0c3d7 | |||
| 6ce90e08ef | |||
| 5ac80d8d60 | |||
| 56e1fa52d8 | |||
| 3ae1b7d168 | |||
| d8f654c81c | |||
| cbcbd4d6dc | |||
| be899b5611 | |||
| 361bbe8d85 | |||
| 1e017af277 | |||
| c4c22a36bb | |||
| 84924b480b | |||
| 358074f4ee | |||
| 084314fbcf | |||
| c42f301ae0 | |||
| c8cd37e451 | |||
| 9f8f3a5407 | |||
| 6f1493808f | |||
| c9d32694db | |||
| 8632a2fc81 | |||
| 46a09d5b62 | |||
| b7e5bbc7d2 | |||
| ed264f0c16 | |||
| f1820575ad | |||
| d2e42d0a3c | |||
| 842cf5aaaa | |||
| c8f727e982 | |||
| fd3c9190de | |||
| 69439d2b13 | |||
| 6d41fee73f | |||
| 0de99adeed | |||
| f71fd7c82c | |||
| 0a6b0b8fa5 | |||
| 5e0ce8f098 | |||
| 9fc5989bd4 | |||
| cbe81861a5 | |||
| 76a03cc2fa | |||
| 3774760548 | |||
| 4b1942b949 | |||
|
|
2f03c02b58 | ||
|
|
639143934f | ||
|
|
81bbbcd8e4 | ||
|
|
bedd46756d | ||
|
|
bb6b342d82 | ||
| b6eb12cf30 | |||
| 80f8129011 | |||
| 86daad2455 | |||
| e71cbd5ba9 | |||
| c0fb9beef7 | |||
| db4b69a24a | |||
| 7746784949 | |||
| 024bd48aba | |||
| cb13c9faa4 | |||
| 009ec759a3 | |||
| 6ba16ad020 | |||
| 43b0b34cdd | |||
| 94e6eb2d10 | |||
| 578eea5d9f | |||
| 724450e049 | |||
| 1759baebad | |||
| 896ef50b9a | |||
| c4d52b6687 | |||
| 5c611a59aa | |||
| 7068b989ef | |||
| 820fda78e7 | |||
| d758423ec6 | |||
| 5472f097a4 | |||
| e373f5cffe | |||
| f04729261b | |||
| b6c8778aec | |||
| 8dfe8d55a0 | |||
| 36b7d5ce42 | |||
| 8d780c3252 | |||
| a841d5de2d | |||
| fdd8d306f7 | |||
| 9510a0fced | |||
| c3ec9dfb11 | |||
| 82c136b684 | |||
| ea4bb752b9 | |||
| bac673df99 | |||
| df2c2f5e4b | |||
| 8c3863f970 | |||
| bc49e31164 | |||
| ce4c54b0d5 | |||
| 7b09cdeefd | |||
| 39dc96ab7a | |||
| 2d13ff328e | |||
| 53dd598547 | |||
| 40b4a540a8 | |||
| 33ae53c199 | |||
| 97e9b0636b | |||
| b0b21e9d53 | |||
| 53d5402502 | |||
| a190a9564e | |||
| 7846520788 | |||
| 3444683983 | |||
| 00118ddafe | |||
| 525ba293e3 | |||
| 071f6c08fd | |||
| da70236a45 | |||
| cfdda2d293 | |||
| aba265d787 | |||
| bbcb37bc4e | |||
| eff7d7493d | |||
| 730916758e | |||
| 9acfe2751e | |||
| 386569d7cf | |||
| 39a7e1eb19 | |||
| f492845235 | |||
| ab42fc8b57 | |||
| a5a9fce330 | |||
| a70286dda4 | |||
| 2b3e587be4 | |||
| ebfac9730b | |||
| fbd3c6ca92 | |||
| 1cd3dabcea | |||
| eba17880d0 | |||
| c168f910a9 | |||
| 98dd704fda | |||
| 4ecebe8982 | |||
| 8f1d17636e | |||
| fb1c202586 | |||
| d7a4ce022e | |||
| 64c3796429 | |||
| 80a517beaa | |||
| cec31550f8 | |||
| bee760adf5 | |||
| 155d5747f8 | |||
| fd531a360e | |||
| c3884a460d | |||
| 5f5c30673d | |||
| f423cd5611 | |||
| 7e059e13ef | |||
| d965fbd57e | |||
| 55854ec586 | |||
| 8886c8e695 | |||
| d58f5f9a01 | |||
| e060b0f549 | |||
| 73913c4ae6 | |||
| 21878ae135 | |||
| a08a110ef6 | |||
| f723c43603 | |||
| d88876c928 | |||
| f15a3e6bf4 | |||
| 4852237bf8 | |||
| 9a0bc87636 | |||
| d73d27dccc | |||
| 6fa5e73226 | |||
| 1ff9ea256b | |||
| 7fca7e0246 | |||
| 846270b714 | |||
| 50e7c5683f | |||
| 6883a9570f | |||
| 8f34bc001d | |||
| 2f95e5452b | |||
| 59a6307a21 | |||
| c8d52e6c41 | |||
| 044766bf8a | |||
| 1f7c851228 | |||
| ca90c658ff | |||
| 19de68e4f0 | |||
| dc17d7d304 | |||
| 2372dbf6b3 | |||
| 3382e35447 | |||
| a9cc4f55b8 | |||
| 63fbf7ebe4 | |||
| d1d6b67fd6 | |||
| 87160e8648 | |||
| e5553699c5 | |||
| adcfdc1a73 | |||
| 70464a2b71 | |||
| 0852a75d9f | |||
| 9affa0e89a | |||
| cc13078ec5 | |||
| 35285343b1 | |||
| cb6bce0c56 | |||
| 5c1eda72c3 | |||
| 4542accc33 | |||
| 8f5470076b | |||
| 3427c3c761 | |||
| 0cc8d0947b | |||
| c3795450a9 | |||
| 868d924836 | |||
| d8f634d67c | |||
| 09b97ab4c5 | |||
| 8b4d7dd569 | |||
| a63205d5e1 | |||
| e7a4e93366 | |||
| 2c23f40415 | |||
| daf4ee79f2 | |||
| 38a73d2890 | |||
| b004d8364c | |||
| 2498e23bd5 | |||
| 3a80d50cf5 | |||
| 8c12eb47ce | |||
| cfec6afc7d | |||
| a3bdabca3c | |||
| f094a326ac | |||
| d24cab9c1a | |||
| c7d1ecce35 | |||
| 306fd99b84 | |||
| ab63bc44a6 | |||
| ef15f15458 | |||
| db86136aa8 | |||
| c344aed471 | |||
| 79b0a4ba7a | |||
| 4ce5e29a81 | |||
| 524dec0991 | |||
| 05074ed4f0 | |||
| 4e4ed58605 | |||
| 6a109fe03d | |||
| a783aab229 | |||
| 43dc2285b3 | |||
| eac8e3fb44 | |||
| 035d29fabc | |||
| ad2b10972c | |||
| e65e1f3ec4 | |||
| 10b86812cd | |||
| fe4c794f68 | |||
| 6115d748e3 | |||
| b0f266bb0a | |||
| 646c99feb5 | |||
| e8461d7059 | |||
| b2efd9f22f | |||
| 8e426b7fd6 | |||
| c13a65b204 | |||
| 503a24e003 | |||
| dfddd3d3d0 | |||
| 26e01bb7f8 | |||
| 5f88626ddf | |||
| e04bb29bb2 | |||
| c9690e028b | |||
| 8709f0bd8e | |||
| 13d7f33c37 |
1
.github/FUNDING.yml
vendored
Normal file
1
.github/FUNDING.yml
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
ko_fi: papatutuwawa
|
||||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -60,3 +60,6 @@ lib/i18n/*.dart
|
|||||||
|
|
||||||
# Android artifacts
|
# Android artifacts
|
||||||
.android
|
.android
|
||||||
|
|
||||||
|
# Build scripts
|
||||||
|
release-*/
|
||||||
|
|||||||
2
.gitlint
2
.gitlint
@@ -7,7 +7,7 @@ line-length=72
|
|||||||
[title-trailing-punctuation]
|
[title-trailing-punctuation]
|
||||||
[title-hard-tab]
|
[title-hard-tab]
|
||||||
[title-match-regex]
|
[title-match-regex]
|
||||||
regex=^(feat|fix|chore|refactor|docs|release|test)\((xmpp|service|ui|shared|meta|tests|i18n)+(,(xmpp|service|ui|shared|meta|tests|i18n))*\): .*$
|
regex=^((feat|fix|chore|refactor)\((service|ui|shared|all|tests|i18n|docs|flake|android|ios|linux|windows|macos)+(,(service|ui|shared|all|tests|i18n|docs|flake|android|ios|linux|windows|macos))*\)|release): [A-Z0-9].*$
|
||||||
|
|
||||||
|
|
||||||
[body-trailing-whitespace]
|
[body-trailing-whitespace]
|
||||||
|
|||||||
108
CONTRIBUTING.md
Normal file
108
CONTRIBUTING.md
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
# Contribution Guide
|
||||||
|
|
||||||
|
Thanks for your interest in the Moxxy XMPP client! This document contains guidelines and guides for working
|
||||||
|
on the Moxxy codebase.
|
||||||
|
|
||||||
|
## Non-Code Contributions
|
||||||
|
### Translations
|
||||||
|
|
||||||
|
You can contribute to Moxxy by translating parts of Moxxy into a language you can speak. To do that, head over to [Codeberg's Weblate instance](https://translate.codeberg.org/projects/moxxy/moxxy/), where you can start translating.
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
Before building or working on Moxxy, please make sure that your development environment is correctly set up.
|
||||||
|
Moxxy requires Flutter 3.7.3, since we use a fork of the Flutter library, and the JDK 17. Building Moxxy
|
||||||
|
is currently only supported for Android.
|
||||||
|
|
||||||
|
### Android Studio
|
||||||
|
|
||||||
|
If you use Android Studio, make sure that you use version "Flamingo Canary 3", as that one comes bundled with
|
||||||
|
JDK 17, instead of JDK 11 ([See here](https://codeberg.org/moxxy/moxxy/issues/252)). If that is
|
||||||
|
not an option, you can manually add a JDK 17 installation in Android Studio and tell the Flutter addon
|
||||||
|
to use that installation instead.
|
||||||
|
|
||||||
|
### NixOS
|
||||||
|
|
||||||
|
If you use NixOS or Nix, you can use the dev shell provided by the Flake in the repository's root. It contains
|
||||||
|
the correct JDK and Flutter version. However, make sure that other environment variables, like
|
||||||
|
`ANDROID_HOME` and `ANDROID_AVD_HOME`, are correctly set.
|
||||||
|
|
||||||
|
## Building
|
||||||
|
|
||||||
|
Currently, Moxxy contains a git submodule. While it is not utilised at the moment, it contains
|
||||||
|
the list of suggested XMPP providers to use for auto-registration. To properly clone the
|
||||||
|
repository, use `git clone --recursive https://codeberg.org/moxxy/moxxy.git`
|
||||||
|
|
||||||
|
In order to build Moxxy, you first have to run the code generator. To do that, first install all dependencies with
|
||||||
|
`flutter pub get`. Next, run the code generator using `flutter pub run build_runner build`. This builds required
|
||||||
|
data classes and the i18n support. Next, run `dart run pigeon --input pigeon/quirks.dart` to generate the communication
|
||||||
|
channels with the native code.
|
||||||
|
|
||||||
|
Finally, you can build Moxxy using `flutter run`, if you want to test a change, or `flutter build apk --release` to build
|
||||||
|
an optimized release build. The release builds found in the repository's releases are build using `flutter build apk --release --split-per-abi`.
|
||||||
|
|
||||||
|
## Contributing
|
||||||
|
|
||||||
|
If you want to fix a small issue, you can just fork, create a new branch, and start working right away. However, if you want to work
|
||||||
|
on a bigger feature, please first create an issue (if an issue does not already exist) or join the [development chat](xmpp:moxxy@muc.moxxy.org?join) (xmpp:moxxy@muc.moxxy.org?join)
|
||||||
|
to discuss the feature first.
|
||||||
|
|
||||||
|
Before creating a pull request, please make sure you checked every item on the following checklist:
|
||||||
|
|
||||||
|
- [ ] I formatted the code with the dart formatter (`dart format`) before running the linter
|
||||||
|
- [ ] I ran the linter (`flutter analyze`) and introduced no new linter warnings
|
||||||
|
- [ ] I ran the tests (`flutter test`) and introduced no new failing tests
|
||||||
|
- [ ] I used [gitlint](https://github.com/jorisroovers/gitlint) to ensure propper formatting of my commig messages
|
||||||
|
|
||||||
|
If you think that your code is ready for a pull request, but you are not sure if it is ready, prefix the PR's title with "WIP: ", so that discussion
|
||||||
|
can happen there. If you think your PR is ready for review, remove the "WIP: " prefix.
|
||||||
|
|
||||||
|
### Android
|
||||||
|
|
||||||
|
In case you modified the Android-native code, please also make sure that you checked every item on the following checklist:
|
||||||
|
|
||||||
|
- [ ] I checked that [ktlint](https://github.com/pinterest/ktlint) is not showing any linting issues (`ktlint android/app/src/main/kotlin/org/moxxy/moxxyv2/ '!android/app/src/main/kotlin/org/moxxy/moxxyv2/quirks'`)
|
||||||
|
|
||||||
|
### Tips
|
||||||
|
#### `data_classes.yaml`
|
||||||
|
|
||||||
|
When you add, remove, or modify data classes in `data_classes.yaml`, you need to rebuild the classes using `flutter pub run build_runner build`. However, there appears
|
||||||
|
to be a bug in my own build runner script, which prevents the data classes from being
|
||||||
|
rebuilt if they are changed. To fix this, remove the generated data classes by running
|
||||||
|
`rm lib/shared/*.moxxy.dart`, after which build_runner will rebuild the data classes.
|
||||||
|
|
||||||
|
### Code Guidelines
|
||||||
|
#### Translations
|
||||||
|
|
||||||
|
If your code adds new strings that should be translated, only add them to the base
|
||||||
|
language, which is English. Even if you know more than English, do not add the keys
|
||||||
|
to other language files. To prevent merge conflicts between Weblate and the repository,
|
||||||
|
all other languages are managed via [Codeberg's Weblate instance](https://translate.codeberg.org/projects/moxxy/moxxy/).
|
||||||
|
|
||||||
|
#### Commit messages
|
||||||
|
|
||||||
|
Commit messages should be uniformly formatted. `gitlint` is a linter for commit messages that enforces those guidelines. They are defined in the `.gitlint` file
|
||||||
|
at the root of the repository. `gitlint` can be installed as a pre-commit hook using
|
||||||
|
`gitlint install-hook`. That way, `gitlint` runs on every commit and warns you if the
|
||||||
|
commit message violates any of the defined rules.
|
||||||
|
|
||||||
|
Commit messages always follow the following format:
|
||||||
|
|
||||||
|
```
|
||||||
|
<type>(<areas>): <summary>
|
||||||
|
|
||||||
|
<full message>
|
||||||
|
```
|
||||||
|
|
||||||
|
`<type>` is the type of action that was performed in the commit and is one of the following: `feat` (Addition of a feature), `fix` (Fix a bug or other issue), `chore` (Bump dependency versions, fix formatter issues), `refactor` (A bigger "moving around" or rewriting of code), `docs` (Commits that just touch the documentation, be it code or, for example, the README).
|
||||||
|
|
||||||
|
`<areas>` are the areas inside the code that are touched by the change. They are a comma-separated list of one or more of the following: `service` (Everything inside `lib/service`), `ui` (Everything inside `lib/ui`), `shared` (Everything inside `lib/shared`), `all` (A bit of everything is involved), `tests` (Everyting inside `test` or `integration_test`), `i18n` (The translation files have been modified), `docs` (Documentation of any kind), `flake` (The NixOS flake has been modified).
|
||||||
|
|
||||||
|
`<summary>` is the summary of the entire commit in a few words. Make that that the entire
|
||||||
|
first line is not longer than 72 characters. `<summary>` also must start with an uppercase
|
||||||
|
letter or a number.
|
||||||
|
|
||||||
|
The `<full message>` is optional. In case your commit requires more explanation, write it
|
||||||
|
there. Make sure that there is an empty line between the full message and the summary line.
|
||||||
|
|
||||||
|
The exception to these rules is a commit message of the format `release: Release version x.y.z`, as it touches everything and is thus implicitly using `(all)` as an area code.
|
||||||
39
README.md
39
README.md
@@ -2,38 +2,29 @@
|
|||||||
|
|
||||||
An experimental XMPP client that tries to be as easy, modern and beautiful as possible.
|
An experimental XMPP client that tries to be as easy, modern and beautiful as possible.
|
||||||
|
|
||||||
The code is also available on [codeberg](https://codeberg.org/moxxy/moxxyv2).
|
The code is also available on [Codeberg](https://codeberg.org/moxxy/moxxy).
|
||||||
|
|
||||||
[<img src="https://gitlab.com/IzzyOnDroid/repo/-/raw/master/assets/IzzyOnDroid.png" alt="Get it on IzzyOnDroid" height="80" />](https://apt.izzysoft.de/fdroid/index/apk/org.moxxy.moxxyv2)
|
[<img src="https://gitlab.com/IzzyOnDroid/repo/-/raw/master/assets/IzzyOnDroid.png" alt="Get it on IzzyOnDroid" height="80">](https://apt.izzysoft.de/fdroid/index/apk/org.moxxy.moxxyv2)
|
||||||
|
|
||||||
|
Or [get the latest APK from Codeberg](https://codeberg.org/moxxy/moxxy/releases/latest).
|
||||||
|
|
||||||
## Screenshots
|
## Screenshots
|
||||||
|
|
||||||
[<img src="https://codeberg.org/moxxy/moxxyv2/raw/branch/master/fastlane/metadata/android/en-US/images/phoneScreenshots/1.png" width="20%"></img>](./fastlane/metadata/android/en-US/images/phoneScreenshots/1.png)
|
[<img src="https://codeberg.org/moxxy/moxxyv2/raw/branch/master/fastlane/metadata/android/en-US/images/phoneScreenshots/1.png" width="20%"></img>](./fastlane/metadata/android/en-US/images/phoneScreenshots/1.png)
|
||||||
[<img src="https://codeberg.org/moxxy/moxxyv2/raw/branch/master/fastlane/metadata/android/en-US/images/phoneScreenshots/2.png" width="20%"></img>](./fastlane/metadata/android/en-US/images/phoneScreenshots/2.png)
|
[<img src="https://codeberg.org/moxxy/moxxyv2/raw/branch/master/fastlane/metadata/android/en-US/images/phoneScreenshots/2.png" width="20%"></img>](./fastlane/metadata/android/en-US/images/phoneScreenshots/2.png)
|
||||||
|
|
||||||
## Developing and Building
|
## Building and Contributing
|
||||||
|
|
||||||
Clone using `git clone --recursive https://github.com/Polynomdivision/moxxyv2.git`.
|
For build and contribution guidelines, please refer to [`CONTRIBUTING.md`](./CONTRIBUTING.md)
|
||||||
|
|
||||||
In order to build Moxxy, you need to have [Flutter](https://docs.flutter.dev/get-started/install) set
|
|
||||||
up. If you are running NixOS or using Nix, you can also use the Flake at the root of the repository
|
|
||||||
by running `nix develop` to get a development shell including everything that is needed. Note
|
|
||||||
that if you decide to use the Flake, `ANDROID_HOME` and `ANDROID_AVD_HOME` must be set to the respective directories.
|
|
||||||
|
|
||||||
Before building Moxxy, you need to generate all needed data classes. To do this, run
|
|
||||||
`flutter pub get` to install all dependencies. Then run `flutter pub run build_runner build` to generate
|
|
||||||
state classes, data classes and the database schemata. After that is done, you can either
|
|
||||||
build the app with `flutter build apk --debug` to create a debug build,
|
|
||||||
`flutter build apk --release` to create a relase build or just run the app in development
|
|
||||||
mode with `flutter run`.
|
|
||||||
|
|
||||||
After implementing a change or a feature, please ensure that nothing is broken by the change
|
|
||||||
by running `flutter test` afterwards. Also make sure that the code passes the linter by
|
|
||||||
running `flutter analyze`. This project also uses [gitlint](https://github.com/jorisroovers/gitlint)
|
|
||||||
to ensure uniform formatting of commit messages.
|
|
||||||
|
|
||||||
Also, feel free to join the development chat at `moxxy@muc.moxxy.org`.
|
Also, feel free to join the development chat at `moxxy@muc.moxxy.org`.
|
||||||
|
|
||||||
|
### Translating
|
||||||
|
|
||||||
|
If you want to contribute by translating Moxxy, you can do that on [Codeberg's Weblate instance](https://translate.codeberg.org/projects/moxxy/moxxy/).
|
||||||
|
|
||||||
|
[](https://translate.codeberg.org/engage/moxxy/)
|
||||||
|
|
||||||
## A Bit of History
|
## A Bit of History
|
||||||
|
|
||||||
This project is the successor of moxxyv1, which was written in *React Native* and abandoned
|
This project is the successor of moxxyv1, which was written in *React Native* and abandoned
|
||||||
@@ -46,3 +37,9 @@ See `./LICENSE`.
|
|||||||
## Special Thanks
|
## Special Thanks
|
||||||
|
|
||||||
- New logo designed by [Synoh](https://twitter.com/synoh_manda)
|
- New logo designed by [Synoh](https://twitter.com/synoh_manda)
|
||||||
|
|
||||||
|
## Support
|
||||||
|
|
||||||
|
If you like what I do and you want to support me, feel free to donate to me on Ko-Fi.
|
||||||
|
|
||||||
|
[<img src="https://codeberg.org/moxxy/moxxyv2/raw/branch/master/assets/repo/kofi.png" height="36" style="height: 36px; border: 0px;"></img>](https://ko-fi.com/papatutuwawa)
|
||||||
|
|||||||
@@ -6,13 +6,12 @@ linter:
|
|||||||
use_setters_to_change_properties: false
|
use_setters_to_change_properties: false
|
||||||
avoid_positional_boolean_parameters: false
|
avoid_positional_boolean_parameters: false
|
||||||
avoid_bool_literals_in_conditional_expressions: false
|
avoid_bool_literals_in_conditional_expressions: false
|
||||||
|
file_names: false
|
||||||
|
|
||||||
analyzer:
|
analyzer:
|
||||||
exclude:
|
exclude:
|
||||||
- "**/*.g.dart"
|
- "**/*.g.dart"
|
||||||
- "**/*.freezed.dart"
|
- "**/*.freezed.dart"
|
||||||
- "**/*.moxxy.dart"
|
- "**/*.moxxy.dart"
|
||||||
- "test/"
|
|
||||||
- "integration_test/"
|
|
||||||
- "lib/service/database/migrations/*.dart"
|
|
||||||
- "lib/i18n/*.dart"
|
- "lib/i18n/*.dart"
|
||||||
|
- "pigeon/quirks.dart"
|
||||||
|
|||||||
@@ -26,12 +26,11 @@ apply plugin: 'kotlin-android'
|
|||||||
apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"
|
apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"
|
||||||
|
|
||||||
android {
|
android {
|
||||||
//compileSdkVersion flutter.compileSdkVersion
|
|
||||||
compileSdkVersion 33
|
compileSdkVersion 33
|
||||||
|
|
||||||
compileOptions {
|
compileOptions {
|
||||||
sourceCompatibility JavaVersion.VERSION_17
|
sourceCompatibility JavaVersion.VERSION_1_8
|
||||||
targetCompatibility JavaVersion.VERSION_17
|
targetCompatibility JavaVersion.VERSION_1_8
|
||||||
}
|
}
|
||||||
|
|
||||||
kotlinOptions {
|
kotlinOptions {
|
||||||
@@ -45,22 +44,22 @@ android {
|
|||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId "org.moxxy.moxxyv2"
|
applicationId "org.moxxy.moxxyv2"
|
||||||
|
|
||||||
// TODO: Remove once https://github.com/fluttercommunity/flutter_launcher_icons/pull/313 is merged
|
minSdkVersion 26
|
||||||
minSdkVersion 23
|
targetSdkVersion 33
|
||||||
targetSdkVersion 31
|
|
||||||
//minSdkVersion flutter.minSdkVersion
|
|
||||||
//targetSdkVersion flutter.targetSdkVersion
|
|
||||||
versionCode flutterVersionCode.toInteger()
|
versionCode flutterVersionCode.toInteger()
|
||||||
versionName flutterVersionName
|
versionName flutterVersionName
|
||||||
}
|
}
|
||||||
|
|
||||||
buildTypes {
|
buildTypes {
|
||||||
release {
|
release {
|
||||||
// TODO: Add your own signing config for the release build.
|
// Externally signed using a security key
|
||||||
// Signing with the debug keys for now, so `flutter run --release` works.
|
signingConfig null
|
||||||
signingConfig signingConfigs.debug
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
compileOnly rootProject.findProject(":moxxy_native")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
flutter {
|
flutter {
|
||||||
@@ -69,4 +68,5 @@ flutter {
|
|||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
|
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
|
||||||
|
implementation "androidx.activity:activity-ktx:1.7.2"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,33 +1,35 @@
|
|||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
package="org.moxxy.moxxyv2">
|
package="org.moxxy.moxxyv2">
|
||||||
<application
|
|
||||||
android:label="Moxxy"
|
<application
|
||||||
android:name="${applicationName}"
|
android:name="${applicationName}"
|
||||||
android:icon="@mipmap/ic_launcher">
|
android:icon="@mipmap/ic_launcher"
|
||||||
|
android:label="Moxxy">
|
||||||
<activity
|
<activity
|
||||||
android:name=".MainActivity"
|
android:name=".MainActivity"
|
||||||
|
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
|
android:hardwareAccelerated="true"
|
||||||
android:launchMode="singleTask"
|
android:launchMode="singleTask"
|
||||||
android:theme="@style/LaunchTheme"
|
android:theme="@style/LaunchTheme"
|
||||||
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
|
|
||||||
android:hardwareAccelerated="true"
|
|
||||||
android:windowSoftInputMode="adjustResize">
|
android:windowSoftInputMode="adjustResize">
|
||||||
<!-- Specifies an Android theme to apply to this Activity as soon as
|
<!-- Specifies an Android theme to apply to this Activity as soon as
|
||||||
the Android process has started. This theme is visible to the user
|
the Android process has started. This theme is visible to the user
|
||||||
while the Flutter UI initializes. After that, this theme continues
|
while the Flutter UI initializes. After that, this theme continues
|
||||||
to determine the Window background behind the Flutter UI. -->
|
to determine the Window background behind the Flutter UI. -->
|
||||||
<meta-data
|
<meta-data
|
||||||
android:name="io.flutter.embedding.android.NormalTheme"
|
android:name="io.flutter.embedding.android.NormalTheme"
|
||||||
android:resource="@style/NormalTheme" />
|
android:resource="@style/NormalTheme" />
|
||||||
<meta-data
|
<meta-data
|
||||||
android:name="flutterEmbedding"
|
android:name="flutterEmbedding"
|
||||||
android:value="2" />
|
android:value="2" />
|
||||||
|
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.MAIN"/>
|
<action android:name="android.intent.action.MAIN" />
|
||||||
<category android:name="android.intent.category.LAUNCHER"/>
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
|
|
||||||
<!-- Allow receiving share intents for all kinds of things -->
|
<!-- Allow receiving share intents for all kinds of things -->
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.SEND" />
|
<action android:name="android.intent.action.SEND" />
|
||||||
<category android:name="android.intent.category.DEFAULT" />
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
@@ -38,20 +40,34 @@
|
|||||||
<category android:name="android.intent.category.DEFAULT" />
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
<data android:mimeType="*/*" />
|
<data android:mimeType="*/*" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
|
|
||||||
|
<!-- Enable usage of direct share -->
|
||||||
|
<meta-data
|
||||||
|
android:name="android.service.chooser.chooser_target_service"
|
||||||
|
android:value="androidx.sharetarget.ChooserTargetServiceCompat" />
|
||||||
|
<meta-data
|
||||||
|
android:name="android.app.shortcuts"
|
||||||
|
android:resource="@xml/share_targets" />
|
||||||
</activity>
|
</activity>
|
||||||
|
|
||||||
<!-- Don't delete the meta-data below.
|
<!-- Don't delete the meta-data below.
|
||||||
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
|
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
|
||||||
<meta-data
|
<meta-data
|
||||||
android:name="flutterEmbedding"
|
android:name="flutterEmbedding"
|
||||||
android:value="2" />
|
android:value="2" />
|
||||||
</application>
|
</application>
|
||||||
<uses-permission android:name="android.permission.INTERNET" />
|
|
||||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
||||||
<queries>
|
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
||||||
<intent>
|
<uses-permission android:name="android.permission.READ_CONTACTS" />
|
||||||
<action android:name="android.intent.action.VIEW" />
|
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||||
<data android:scheme="https" />
|
<uses-permission android:name="android.permission.VIBRATE" />
|
||||||
</intent>
|
|
||||||
</queries>
|
<queries>
|
||||||
|
<intent>
|
||||||
|
<action android:name="android.intent.action.VIEW" />
|
||||||
|
<data android:scheme="https" />
|
||||||
|
</intent>
|
||||||
|
</queries>
|
||||||
</manifest>
|
</manifest>
|
||||||
|
|||||||
@@ -1,6 +1,57 @@
|
|||||||
package org.moxxy.moxxyv2
|
package org.moxxy.moxxyv2
|
||||||
|
|
||||||
|
import android.content.Intent
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.util.Log
|
||||||
import io.flutter.embedding.android.FlutterActivity
|
import io.flutter.embedding.android.FlutterActivity
|
||||||
|
import io.flutter.embedding.engine.FlutterEngine
|
||||||
|
import org.moxxy.moxxyv2.quirks.MoxxyQuirkApi
|
||||||
|
import org.moxxy.moxxyv2.quirks.QuirkNotificationEvent
|
||||||
|
import org.moxxy.moxxyv2.quirks.QuirkNotificationEventType
|
||||||
|
|
||||||
class MainActivity: FlutterActivity() {
|
class MainActivity : FlutterActivity(), MoxxyQuirkApi {
|
||||||
|
private var lastEvent: QuirkNotificationEvent? = null
|
||||||
|
|
||||||
|
private fun handleIntent(intent: Intent?): Boolean {
|
||||||
|
if (intent == null) return false
|
||||||
|
|
||||||
|
when (intent.action) {
|
||||||
|
org.moxxy.moxxy_native.TAP_ACTION -> {
|
||||||
|
Log.d("Moxxy", "Handling tap data")
|
||||||
|
lastEvent = QuirkNotificationEvent(
|
||||||
|
intent.getLongExtra(org.moxxy.moxxy_native.NOTIFICATION_EXTRA_ID_KEY, -1),
|
||||||
|
intent.getStringExtra(org.moxxy.moxxy_native.NOTIFICATION_EXTRA_JID_KEY)!!,
|
||||||
|
QuirkNotificationEventType.OPEN,
|
||||||
|
null,
|
||||||
|
org.moxxy.moxxy_native.notifications.extractPayloadMapFromIntent(intent),
|
||||||
|
)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
Log.d("Moxxy", "Unknown intent action: ${intent.action}")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
|
||||||
|
MoxxyQuirkApi.setUp(flutterEngine.dartExecutor.binaryMessenger, this)
|
||||||
|
super.configureFlutterEngine(flutterEngine)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onNewIntent(intent: Intent) {
|
||||||
|
super.onNewIntent(intent)
|
||||||
|
handleIntent(intent)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
handleIntent(intent)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun earlyNotificationEventQuirk(): QuirkNotificationEvent? {
|
||||||
|
val event = lastEvent
|
||||||
|
lastEvent = null
|
||||||
|
return event
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,150 @@
|
|||||||
|
// Autogenerated from Pigeon (v11.0.1), do not edit directly.
|
||||||
|
// See also: https://pub.dev/packages/pigeon
|
||||||
|
|
||||||
|
package org.moxxy.moxxyv2.quirks
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
|
import io.flutter.plugin.common.BasicMessageChannel
|
||||||
|
import io.flutter.plugin.common.BinaryMessenger
|
||||||
|
import io.flutter.plugin.common.MessageCodec
|
||||||
|
import io.flutter.plugin.common.StandardMessageCodec
|
||||||
|
import java.io.ByteArrayOutputStream
|
||||||
|
import java.nio.ByteBuffer
|
||||||
|
|
||||||
|
private fun wrapResult(result: Any?): List<Any?> {
|
||||||
|
return listOf(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun wrapError(exception: Throwable): List<Any?> {
|
||||||
|
if (exception is FlutterError) {
|
||||||
|
return listOf(
|
||||||
|
exception.code,
|
||||||
|
exception.message,
|
||||||
|
exception.details
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
return listOf(
|
||||||
|
exception.javaClass.simpleName,
|
||||||
|
exception.toString(),
|
||||||
|
"Cause: " + exception.cause + ", Stacktrace: " + Log.getStackTraceString(exception)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Error class for passing custom error details to Flutter via a thrown PlatformException.
|
||||||
|
* @property code The error code.
|
||||||
|
* @property message The error message.
|
||||||
|
* @property details The error details. Must be a datatype supported by the api codec.
|
||||||
|
*/
|
||||||
|
class FlutterError (
|
||||||
|
val code: String,
|
||||||
|
override val message: String? = null,
|
||||||
|
val details: Any? = null
|
||||||
|
) : Throwable()
|
||||||
|
|
||||||
|
enum class QuirkNotificationEventType(val raw: Int) {
|
||||||
|
MARKASREAD(0),
|
||||||
|
REPLY(1),
|
||||||
|
OPEN(2);
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun ofRaw(raw: Int): QuirkNotificationEventType? {
|
||||||
|
return values().firstOrNull { it.raw == raw }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Generated class from Pigeon that represents data sent in messages. */
|
||||||
|
data class QuirkNotificationEvent (
|
||||||
|
/** The notification id. */
|
||||||
|
val id: Long,
|
||||||
|
/** The JID the notification was for. */
|
||||||
|
val jid: String,
|
||||||
|
/** The type of event. */
|
||||||
|
val type: QuirkNotificationEventType,
|
||||||
|
/**
|
||||||
|
* An optional payload.
|
||||||
|
* - type == NotificationType.reply: The reply message text.
|
||||||
|
* Otherwise: undefined.
|
||||||
|
*/
|
||||||
|
val payload: String? = null,
|
||||||
|
/** Extra data. Only set when type == NotificationType.reply. */
|
||||||
|
val extra: Map<String?, String?>? = null
|
||||||
|
|
||||||
|
) {
|
||||||
|
companion object {
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
fun fromList(list: List<Any?>): QuirkNotificationEvent {
|
||||||
|
val id = list[0].let { if (it is Int) it.toLong() else it as Long }
|
||||||
|
val jid = list[1] as String
|
||||||
|
val type = QuirkNotificationEventType.ofRaw(list[2] as Int)!!
|
||||||
|
val payload = list[3] as String?
|
||||||
|
val extra = list[4] as Map<String?, String?>?
|
||||||
|
return QuirkNotificationEvent(id, jid, type, payload, extra)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fun toList(): List<Any?> {
|
||||||
|
return listOf<Any?>(
|
||||||
|
id,
|
||||||
|
jid,
|
||||||
|
type.raw,
|
||||||
|
payload,
|
||||||
|
extra,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
private object MoxxyQuirkApiCodec : StandardMessageCodec() {
|
||||||
|
override fun readValueOfType(type: Byte, buffer: ByteBuffer): Any? {
|
||||||
|
return when (type) {
|
||||||
|
128.toByte() -> {
|
||||||
|
return (readValue(buffer) as? List<Any?>)?.let {
|
||||||
|
QuirkNotificationEvent.fromList(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else -> super.readValueOfType(type, buffer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
override fun writeValue(stream: ByteArrayOutputStream, value: Any?) {
|
||||||
|
when (value) {
|
||||||
|
is QuirkNotificationEvent -> {
|
||||||
|
stream.write(128)
|
||||||
|
writeValue(stream, value.toList())
|
||||||
|
}
|
||||||
|
else -> super.writeValue(stream, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Generated interface from Pigeon that represents a handler of messages from Flutter. */
|
||||||
|
interface MoxxyQuirkApi {
|
||||||
|
fun earlyNotificationEventQuirk(): QuirkNotificationEvent?
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
/** The codec used by MoxxyQuirkApi. */
|
||||||
|
val codec: MessageCodec<Any?> by lazy {
|
||||||
|
MoxxyQuirkApiCodec
|
||||||
|
}
|
||||||
|
/** Sets up an instance of `MoxxyQuirkApi` to handle messages through the `binaryMessenger`. */
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
fun setUp(binaryMessenger: BinaryMessenger, api: MoxxyQuirkApi?) {
|
||||||
|
run {
|
||||||
|
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.moxxyv2.MoxxyQuirkApi.earlyNotificationEventQuirk", codec)
|
||||||
|
if (api != null) {
|
||||||
|
channel.setMessageHandler { _, reply ->
|
||||||
|
var wrapped: List<Any?>
|
||||||
|
try {
|
||||||
|
wrapped = listOf<Any?>(api.earlyNotificationEventQuirk())
|
||||||
|
} catch (exception: Throwable) {
|
||||||
|
wrapped = wrapError(exception)
|
||||||
|
}
|
||||||
|
reply.reply(wrapped)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
channel.setMessageHandler(null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
7
android/app/src/main/res/xml/share_targets.xml
Normal file
7
android/app/src/main/res/xml/share_targets.xml
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<shortcuts xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<share-target android:targetClass="org.moxxy.moxxyv2.MainActivity">
|
||||||
|
<data android:mimeType="*/*" />
|
||||||
|
<category android:name="org.moxxy.moxxyv2.dynamic_share_target" />
|
||||||
|
</share-target>
|
||||||
|
</shortcuts>
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
buildscript {
|
buildscript {
|
||||||
ext.kotlin_version = '1.6.10'
|
ext.kotlin_version = '1.8.21'
|
||||||
repositories {
|
repositories {
|
||||||
google()
|
google()
|
||||||
mavenCentral()
|
mavenCentral()
|
||||||
@@ -26,6 +26,6 @@ subprojects {
|
|||||||
project.evaluationDependsOn(':app')
|
project.evaluationDependsOn(':app')
|
||||||
}
|
}
|
||||||
|
|
||||||
task clean(type: Delete) {
|
tasks.register("clean", Delete) {
|
||||||
delete rootProject.buildDir
|
delete rootProject.buildDir
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,252 +0,0 @@
|
|||||||
{
|
|
||||||
"@@name": "English",
|
|
||||||
"global": {
|
|
||||||
"title": "Moxxy",
|
|
||||||
"moxxySubtitle": "An experiment into building a modern, easy and beautiful XMPP client.",
|
|
||||||
"dialogAccept": "Okay",
|
|
||||||
"dialogCancel": "Cancel",
|
|
||||||
"yes": "Yes",
|
|
||||||
"no": "No"
|
|
||||||
},
|
|
||||||
"notifications": {
|
|
||||||
"permanent": {
|
|
||||||
"idle": "Idle",
|
|
||||||
"ready": "Ready to receive messages",
|
|
||||||
"connecting": "Connecting...",
|
|
||||||
"disconnect": "Disconnected",
|
|
||||||
"error": "Error"
|
|
||||||
},
|
|
||||||
"message": {
|
|
||||||
"reply": "Reply",
|
|
||||||
"markAsRead": "Mark as read"
|
|
||||||
},
|
|
||||||
"channels": {
|
|
||||||
"messagesChannelName": "Messages",
|
|
||||||
"messagesChannelDescription": "The notification channel for received messages",
|
|
||||||
"warningChannelName": "Warnings",
|
|
||||||
"warningChannelDescription": "Warnings related to Moxxy"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"messages": {
|
|
||||||
"image": "Image",
|
|
||||||
"video": "Video",
|
|
||||||
"audio": "Audio",
|
|
||||||
"file": "File",
|
|
||||||
"retracted": "The message has been retracted",
|
|
||||||
"retractedFallback": "A previous message has been retracted but your client does not support it"
|
|
||||||
},
|
|
||||||
"errors": {
|
|
||||||
"omemo": {
|
|
||||||
"couldNotPublish": "Could not publish the cryptographic identity to the server. This means that end-to-end encryption may not work.",
|
|
||||||
"notEncryptedForDevice": "This message was not encrypted for this device",
|
|
||||||
"invalidHmac": "Could not decrypt message",
|
|
||||||
"noDecryptionKey": "No decryption key available",
|
|
||||||
"messageInvalidAfixElement": "Invalid encrypted message"
|
|
||||||
},
|
|
||||||
"connection": {
|
|
||||||
"connectionTimeout": "Could not connect to server"
|
|
||||||
},
|
|
||||||
"login": {
|
|
||||||
"saslFailed": "Invalid login credentials",
|
|
||||||
"startTlsFailed": "Failed to establish a secure connection",
|
|
||||||
"noConnection": "Failed to establish a connection",
|
|
||||||
"unspecified": "Unspecified error"
|
|
||||||
},
|
|
||||||
"message": {
|
|
||||||
"unspecified": "Unknown error",
|
|
||||||
"fileUploadFailed": "The file upload failed",
|
|
||||||
"contactDoesntSupportOmemo": "The contact does not support encryption using OMEMO:2",
|
|
||||||
"fileDownloadFailed": "The file download failed",
|
|
||||||
"serviceUnavailable": "The message could not be delivered to the contact",
|
|
||||||
"remoteServerTimeout": "The message could not be delivered to the contact's server",
|
|
||||||
"remoteServerNotFound": "The message could not be delivered to the contact's server as it cannot be found",
|
|
||||||
"failedToEncrypt": "The message could not be encrypted",
|
|
||||||
"failedToEncryptFile": "The file could not be encrypted",
|
|
||||||
"failedToDecryptFile": "The file could not be decrypted",
|
|
||||||
"fileNotEncrypted": "The chat is encrypted but the file is not encrypted"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"warnings": {
|
|
||||||
"message": {
|
|
||||||
"integrityCheckFailed": "Could not verify file integrity"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"pages": {
|
|
||||||
"intro": {
|
|
||||||
"noAccount": "Have no XMPP account? No worries, creating one is really easy.",
|
|
||||||
"loginButton": "Login",
|
|
||||||
"registerButton": "Register"
|
|
||||||
},
|
|
||||||
"login": {
|
|
||||||
"title": "Login",
|
|
||||||
"xmppAddress": "XMPP-Address",
|
|
||||||
"password": "Password",
|
|
||||||
"advancedOptions": "Advanced options",
|
|
||||||
"createAccount": "Create account on server"
|
|
||||||
},
|
|
||||||
"conversations": {
|
|
||||||
"speeddialNewChat": "New chat",
|
|
||||||
"speeddialJoinGroupchat": "Join groupchat",
|
|
||||||
"overlaySettings": "Settings",
|
|
||||||
"noOpenChats": "You have no open chats",
|
|
||||||
"startChat": "Start a chat"
|
|
||||||
},
|
|
||||||
"conversation": {
|
|
||||||
"unencrypted": "Unencrypted",
|
|
||||||
"encrypted": "Encrypted",
|
|
||||||
"closeChat": "Close chat",
|
|
||||||
"closeChatConfirmTitle": "Close chat",
|
|
||||||
"closeChatConfirmSubtext": "Are you sure you want to close this chat?",
|
|
||||||
"blockUser": "Block user",
|
|
||||||
"online": "Online",
|
|
||||||
"retract": "Retract message",
|
|
||||||
"retractBody": "Are you sure you want to retract the message? Keep in mind that this is only a request that the client does not have to honour.",
|
|
||||||
"forward": "Forward",
|
|
||||||
"edit": "Edit",
|
|
||||||
"quote": "Quote"
|
|
||||||
},
|
|
||||||
"addcontact": {
|
|
||||||
"title": "Add new contact",
|
|
||||||
"xmppAddress": "XMPP-Address",
|
|
||||||
"subtitle": "You can add a contact either by typing in their XMPP address or by scanning their QR code",
|
|
||||||
"buttonAddToContact": "Add to contacts"
|
|
||||||
},
|
|
||||||
"newconversation": {
|
|
||||||
"title": "Start new chat",
|
|
||||||
"addContact": "Add contact",
|
|
||||||
"createGroupchat": "Create groupchat"
|
|
||||||
},
|
|
||||||
"crop": {
|
|
||||||
"setProfilePicture": "Set as profile picture"
|
|
||||||
},
|
|
||||||
"shareselection": {
|
|
||||||
"shareWith": "Share with...",
|
|
||||||
"confirmTitle": "Send file",
|
|
||||||
"confirmBody": "One or more chats are unencrypted. This means that the file will be leaked to the server. Do you still want to continue?"
|
|
||||||
},
|
|
||||||
"profile": {
|
|
||||||
"self": {
|
|
||||||
"devices": "Devices"
|
|
||||||
},
|
|
||||||
"conversation": {
|
|
||||||
"muteChatTooltip": "Mute chat",
|
|
||||||
"unmuteChatTooltip": "Unmute chat",
|
|
||||||
"muteChat": "Mute",
|
|
||||||
"unmuteChat": "Unmute",
|
|
||||||
"devices": "Devices"
|
|
||||||
},
|
|
||||||
"owndevices": {
|
|
||||||
"title": "Own Devices",
|
|
||||||
"thisDevice": "This device",
|
|
||||||
"otherDevices": "Other devices",
|
|
||||||
"deleteDeviceConfirmTitle": "Delete device",
|
|
||||||
"deleteDeviceConfirmBody": "This means that contacts will not be able to encrypt for that device. Continue?",
|
|
||||||
"recreateOwnSessions": "Rebuild sessions",
|
|
||||||
"recreateOwnSessionsConfirmTitle": "Recreate own sessions?",
|
|
||||||
"recreateOwnSessionsConfirmBody": "This will recreate the cryptographic sessions with your own devices. Use only if your own devices throw decryption errors.",
|
|
||||||
"recreateOwnDevice": "Recreate device",
|
|
||||||
"recreateOwnDeviceConfirmTitle": "Recreate own device?",
|
|
||||||
"recreateOwnDeviceConfirmBody": "This will recreate this device's cryptographic identity. It will take some time. If contacts verified your device, they will have to do it again. Continue?"
|
|
||||||
},
|
|
||||||
"devices": {
|
|
||||||
"title": "Devices",
|
|
||||||
"recreateSessions": "Rebuild sessions",
|
|
||||||
"recreateSessionsConfirmTitle": "Rebuild sessions?",
|
|
||||||
"recreateSessionsConfirmBody": "This will recreate the cryptographic sessions with your own devices. Use only if your own devices throw decryption errors."
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"blocklist": {
|
|
||||||
"title": "Blocklist",
|
|
||||||
"noUsersBlocked": "You have no users blocked",
|
|
||||||
"unblockAll": "Unblock all",
|
|
||||||
"unblockAllConfirmTitle": "Are you sure?",
|
|
||||||
"unblockAllConfirmBody": "Are you sure you want to unblock all users?",
|
|
||||||
"unblockJidConfirmTitle": "Unblock ${jid}?",
|
|
||||||
"unblockJidConfirmBody": "Are you sure you want to unblock ${jid}? You will receive messages from this user again."
|
|
||||||
},
|
|
||||||
"settings": {
|
|
||||||
"settings": {
|
|
||||||
"title": "Settings",
|
|
||||||
"conversationsSection": "Conversations",
|
|
||||||
"accountSection": "Account",
|
|
||||||
"signOut": "Sign out",
|
|
||||||
"signOutConfirmTitle": "Sign Out",
|
|
||||||
"signOutConfirmBody": "You are about to sign out. Proceed?",
|
|
||||||
"miscellaneousSection": "Miscellaneous",
|
|
||||||
"debuggingSection": "Debugging"
|
|
||||||
},
|
|
||||||
"about": {
|
|
||||||
"title": "About",
|
|
||||||
"licensed": "Licensed under GPL3",
|
|
||||||
"viewSourceCode": "View source code"
|
|
||||||
},
|
|
||||||
"appearance": {
|
|
||||||
"title": "Appearance",
|
|
||||||
"languageSection": "Language",
|
|
||||||
"language": "App language",
|
|
||||||
"languageSubtext": "Currently selected: $selectedLanguage",
|
|
||||||
"systemLanguage": "Default language"
|
|
||||||
},
|
|
||||||
"licenses": {
|
|
||||||
"title": "Open-Source Licenses",
|
|
||||||
"licensedUnder": "Licensed under $license"
|
|
||||||
},
|
|
||||||
"conversation": {
|
|
||||||
"title": "Chat",
|
|
||||||
"appearance": "Appearance",
|
|
||||||
"selectBackgroundImage": "Select background image",
|
|
||||||
"selectBackgroundImageDescription": "This image will be the background of all your chats",
|
|
||||||
"removeBackgroundImage": "Remove background image",
|
|
||||||
"removeBackgroundImageConfirmTitle": "Remove background image",
|
|
||||||
"removeBackgroundImageConfirmBody": "Are you sure you want to remove your conversation background image?",
|
|
||||||
"newChatsSection": "New Conversations",
|
|
||||||
"newChatsMuteByDefault": "Mute new chats by default",
|
|
||||||
"newChatsE2EE": "Enable end-to-end encryption by default. WARNING: Experimental"
|
|
||||||
},
|
|
||||||
"debugging": {
|
|
||||||
"title": "Debugging options",
|
|
||||||
"generalSection": "General",
|
|
||||||
"generalEnableDebugging": "Enable debugging",
|
|
||||||
"generalEncryptionPassword": "Encryption password",
|
|
||||||
"generalEncryptionPasswordSubtext": "The logs may contain sensitive information so pick a strong passphrase",
|
|
||||||
"generalLoggingIp": "Logging IP",
|
|
||||||
"generalLoggingIpSubtext": "The IP the logs should be sent to",
|
|
||||||
"generalLoggingPort": "Logging Port",
|
|
||||||
"generalLoggingPortSubtext": "The IP the logs should be sent to"
|
|
||||||
},
|
|
||||||
"network": {
|
|
||||||
"title": "Network",
|
|
||||||
"automaticDownloadsSection": "Automatic Downloads",
|
|
||||||
"automaticDownloadsText": "Moxxy will automatically download files on...",
|
|
||||||
"automaticDownloadsMaximumSize": "Maximum Download Size",
|
|
||||||
"automaticDownloadsMaximumSizeSubtext": "The maximum file size for a file to be automatically downloaded",
|
|
||||||
"wifi": "Wifi",
|
|
||||||
"mobileData": "Mobile data"
|
|
||||||
},
|
|
||||||
"privacy": {
|
|
||||||
"title": "Pricacy",
|
|
||||||
"generalSection": "General",
|
|
||||||
"showContactRequests": "Show contact requests",
|
|
||||||
"showContactRequestsSubtext": "This will show people who added you to their contact list but sent no message yet",
|
|
||||||
"profilePictureVisibility": "Make profile picture public",
|
|
||||||
"profilePictureVisibilitSubtext": "If enabled, everyone can see your profile picture. If disabled, only users on your contact list can see your profile picture.",
|
|
||||||
"autoAcceptSubscriptionRequests": "Auto-accept subscription requests",
|
|
||||||
"autoAcceptSubscriptionRequestsSubtext": "If enabled, subscription requests will be automatically accepted if the user is in the contact list.",
|
|
||||||
"conversationsSection": "Conversation",
|
|
||||||
"sendChatMarkers": "Send chat markers",
|
|
||||||
"sendChatMarkersSubtext": "This will tell your conversation partner if you received or read a message",
|
|
||||||
"sendChatStates": "Send chat states",
|
|
||||||
"sendChatStatesSubtext": "This will show your conversation partner if you are typing or looking at the chat",
|
|
||||||
"redirectsSection": "Redirects",
|
|
||||||
"redirectText": "This will redirect $serviceName links that you tap to a proxy service, e.g. $exampleProxy",
|
|
||||||
"currentlySelected": "Currently selected: $proxy",
|
|
||||||
"redirectsTitle": "$serviceName Redirect",
|
|
||||||
"cannotEnableRedirect": "Cannot enable $serviceName redirects",
|
|
||||||
"cannotEnableRedirectSubtext": "You must first set a proxy service to redirect to. To do so, tap the field next to the switch.",
|
|
||||||
"urlEmpty": "URL cannot be empty",
|
|
||||||
"urlInvalid": "Invalid URL",
|
|
||||||
"redirectDialogTitle": "$serviceName Redirect"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,252 +1,409 @@
|
|||||||
{
|
{
|
||||||
"@@name": "Deutsch",
|
"language": "Deutsch",
|
||||||
"global": {
|
"global": {
|
||||||
"title": "Moxxy",
|
"title": "Moxxy",
|
||||||
"moxxySubtitle": "Ein Experiment im Entwickeln eines modernen, einfachen und schönen XMPP-Clients.",
|
"moxxySubtitle": "Ein Experiment im Entwickeln eines modernen, einfachen und schönen XMPP-Clients.",
|
||||||
"dialogAccept": "Okay",
|
"dialogAccept": "Okay",
|
||||||
"dialogCancel": "Abbrechen",
|
"dialogCancel": "Abbrechen",
|
||||||
"yes": "Ja",
|
"yes": "Ja",
|
||||||
"no": "Nein"
|
"no": "Nein"
|
||||||
},
|
},
|
||||||
"notifications": {
|
"notifications": {
|
||||||
"permanent": {
|
"permanent": {
|
||||||
"idle": "Bereit",
|
"idle": "Bereit",
|
||||||
"ready": "Bereit zum Nachrichtenempfang",
|
"ready": "Bereit zum Nachrichtenempfang",
|
||||||
"connecting": "Verbinde...",
|
"connecting": "Verbinde...",
|
||||||
"disconnect": "Keine Verbindung",
|
"disconnect": "Keine Verbindung",
|
||||||
"error": "Fehler"
|
"error": "Fehler"
|
||||||
},
|
},
|
||||||
"message": {
|
"message": {
|
||||||
"reply": "Antworten",
|
"reply": "Antworten",
|
||||||
"markAsRead": "Als gelesen markieren"
|
"markAsRead": "Als gelesen markieren"
|
||||||
},
|
},
|
||||||
"channels": {
|
"channels": {
|
||||||
"messagesChannelName": "Nachrichten",
|
"messagesChannelName": "Nachrichten",
|
||||||
"messagesChannelDescription": "Empfangene Nachrichten",
|
"messagesChannelDescription": "Empfangene Nachrichten",
|
||||||
"warningChannelName": "Warnungen",
|
"warningChannelName": "Warnungen",
|
||||||
"warningChannelDescription": "Warnungen im Bezug auf Moxxy"
|
"warningChannelDescription": "Warnungen im Bezug auf Moxxy"
|
||||||
}
|
},
|
||||||
|
"titles": {
|
||||||
|
"error": "Fehler"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"dateTime": {
|
||||||
|
"justNow": "Gerade",
|
||||||
|
"nMinutesAgo": "vor ${min}min",
|
||||||
|
"mondayAbbrev": "Mon",
|
||||||
|
"tuesdayAbbrev": "Die",
|
||||||
|
"wednessdayAbbrev": "Mit",
|
||||||
|
"thursdayAbbrev": "Don",
|
||||||
|
"fridayAbbrev": "Fre",
|
||||||
|
"saturdayAbbrev": "Sam",
|
||||||
|
"sundayAbbrev": "Son",
|
||||||
|
"january": "Januar",
|
||||||
|
"february": "Februar",
|
||||||
|
"march": "März",
|
||||||
|
"april": "April",
|
||||||
|
"may": "Mai",
|
||||||
|
"june": "Juni",
|
||||||
|
"july": "Juli",
|
||||||
|
"august": "August",
|
||||||
|
"september": "September",
|
||||||
|
"october": "Oktober",
|
||||||
|
"november": "November",
|
||||||
|
"december": "Dezember",
|
||||||
|
"today": "Heute",
|
||||||
|
"yesterday": "Gestern"
|
||||||
},
|
},
|
||||||
"messages": {
|
"messages": {
|
||||||
"image": "Bild",
|
"image": "Bild",
|
||||||
"video": "Video",
|
"video": "Video",
|
||||||
"audio": "Audio",
|
"audio": "Audio",
|
||||||
"file": "Datei",
|
"file": "Datei",
|
||||||
"retracted": "Die Nachricht wurde zurückgezogen",
|
"sticker": "Sticker",
|
||||||
"retractedFallback": "Eine vorherige Nachricht wurde zurückgezogen. Dein Client unterstüzt dies jedoch nicht"
|
"retracted": "Die Nachricht wurde zurückgezogen",
|
||||||
|
"retractedFallback": "Eine vorherige Nachricht wurde zurückgezogen. Dein Client unterstüzt dies jedoch nicht",
|
||||||
|
"you": "Du"
|
||||||
},
|
},
|
||||||
"errors": {
|
"errors": {
|
||||||
"omemo": {
|
"general": {
|
||||||
"couldNotPublish": "Konnte die kryptographische Identität nicht auf dem Server veröffentlichen. Ende-zu-Ende-Verschlüsselung funktioniert eventuell nicht.",
|
"noInternet": "Keine Internetverbindung."
|
||||||
"notEncryptedForDevice": "Die Nachricht wurde nicht für dieses Gerät verschlüsselt",
|
},
|
||||||
"invalidHmac": "Die Nachricht konnte nicht entschlüsselt werden",
|
"filePicker": {
|
||||||
"noDecryptionKey": "Kein Schlüssel zum Entschlüsseln vorhanden",
|
"permissionDenied": "Die Speicherberechtigung wurde nicht erteilt."
|
||||||
"messageInvalidAfixElement": "Ungültige verschlüsselte Nachricht"
|
},
|
||||||
},
|
"omemo": {
|
||||||
"connection": {
|
"couldNotPublish": "Konnte die kryptographische Identität nicht auf dem Server veröffentlichen. Ende-zu-Ende-Verschlüsselung funktioniert eventuell nicht.",
|
||||||
"connectionTimeout": "Verbindung zum Server nicht möglich"
|
"notEncryptedForDevice": "Die Nachricht wurde nicht für dieses Gerät verschlüsselt",
|
||||||
},
|
"invalidHmac": "Die Nachricht konnte nicht entschlüsselt werden",
|
||||||
"login": {
|
"noDecryptionKey": "Kein Schlüssel zum Entschlüsseln vorhanden",
|
||||||
"saslFailed": "Ungültige Logindaten",
|
"messageInvalidAfixElement": "Ungültige verschlüsselte Nachricht",
|
||||||
"startTlsFailed": "Konnte keine sichere Verbindung zum Server aufbauen",
|
"verificationInvalidOmemoUrl": "Ungültiger OMEMO:2 Fingerabdruck",
|
||||||
"noConnection": "Konnte keine Verbindung zum Server aufbauen",
|
"verificationWrongJid": "Falsche XMPP-Addresse",
|
||||||
"unspecified": "Unbestimmter Fehler"
|
"verificationWrongDevice": "Falsches OMEMO:2 Gerät",
|
||||||
},
|
"verificationNotInList": "OMEMO:2 Gerät unbekannt",
|
||||||
"message": {
|
"verificationWrongFingerprint": "Falscher OMEMO:2 Fingerabdruck"
|
||||||
"unspecified": "Unbekannter Fehler",
|
},
|
||||||
"fileUploadFailed": "Das Hochladen der Datei ist fehlgeschlagen",
|
"connection": {
|
||||||
"contactDoesntSupportOmemo": "Der Kontakt unterstützt Verschlüsselung mit OMEMO:2 nicht",
|
"connectionTimeout": "Verbindung zum Server nicht möglich",
|
||||||
"fileDownloadFailed": "Das Herunterladen der Datei ist fehlgeschlagen",
|
"saslAccountDisabled": "Dein Konto ist deaktiviert",
|
||||||
"serviceUnavailable": "Die Nachricht konnte nicht gesendet werden",
|
"saslInvalidCredentials": "Deine Anmeldedaten sind ungültig",
|
||||||
"remoteServerTimeout": "Die Nachricht konnte nicht zugestellt werden",
|
"unrecoverable": "Verbindung zum Server durch nicht behebbaren Fehler verloren"
|
||||||
"remoteServerNotFound": "Die Nachricht konnte nicht gesendet werden, da der Empfängerserver unbekannt ist",
|
},
|
||||||
"failedToEncrypt": "Die Nachricht konnte nicht verschlüsselt werden",
|
"login": {
|
||||||
"failedToEncryptFile": "Die Datei konnte nicht verschlüsselt werden",
|
"saslFailed": "Ungültige Logindaten",
|
||||||
"failedToDecryptFile": "Die Datei konnte nicht entschlüsselt werden",
|
"startTlsFailed": "Konnte keine sichere Verbindung zum Server aufbauen",
|
||||||
"fileNotEncrypted": "Der Chat ist verschlüsselt, aber die Datei wurde unverschlüsselt übertragen"
|
"noConnection": "Konnte keine Verbindung zum Server aufbauen",
|
||||||
}
|
"unspecified": "Unbestimmter Fehler"
|
||||||
|
},
|
||||||
|
"message": {
|
||||||
|
"unspecified": "Unbekannter Fehler",
|
||||||
|
"fileUploadFailed": "Das Hochladen der Datei ist fehlgeschlagen",
|
||||||
|
"contactDoesntSupportOmemo": "Der Kontakt unterstützt Verschlüsselung mit OMEMO:2 nicht",
|
||||||
|
"fileDownloadFailed": "Das Herunterladen der Datei ist fehlgeschlagen",
|
||||||
|
"serviceUnavailable": "Die Nachricht konnte nicht gesendet werden",
|
||||||
|
"remoteServerTimeout": "Die Nachricht konnte nicht zugestellt werden",
|
||||||
|
"remoteServerNotFound": "Die Nachricht konnte nicht gesendet werden, da der Empfängerserver unbekannt ist",
|
||||||
|
"failedToEncrypt": "Die Nachricht konnte nicht verschlüsselt werden",
|
||||||
|
"failedToEncryptFile": "Die Datei konnte nicht verschlüsselt werden",
|
||||||
|
"failedToDecryptFile": "Die Datei konnte nicht entschlüsselt werden",
|
||||||
|
"fileNotEncrypted": "Der Chat ist verschlüsselt, aber die Datei wurde unverschlüsselt übertragen"
|
||||||
|
},
|
||||||
|
"conversation": {
|
||||||
|
"audioRecordingError": "Fehler beim Fertigstellen der Audioaufnahme",
|
||||||
|
"openFileNoAppError": "Keine App vorhanden, um die Datei zu öffnen",
|
||||||
|
"openFileGenericError": "Fehler beim Öffnen der Datei",
|
||||||
|
"messageErrorDialogTitle": "Fehler"
|
||||||
|
},
|
||||||
|
"newChat": {
|
||||||
|
"remoteServerError": "Konnte den Server nicht erreichen.",
|
||||||
|
"groupchatUnsupported": "Das Beitreten eines Gruppenchats ist aktuell nicht unterstützt.",
|
||||||
|
"unknown": "Unbekannter Fehler."
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"warnings": {
|
"warnings": {
|
||||||
"message": {
|
"message": {
|
||||||
"integrityCheckFailed": "Konnte Integrität der Datei nicht überprüfen"
|
"integrityCheckFailed": "Konnte Integrität der Datei nicht überprüfen"
|
||||||
}
|
},
|
||||||
|
"conversation": {
|
||||||
|
"holdForLonger": "Button länger gedrückt halten, um eine Sprachnachricht aufzunehmen"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"pages": {
|
"pages": {
|
||||||
"intro": {
|
"intro": {
|
||||||
"noAccount": "Kein XMPP-Account vorhanden? Einen zu erstellen ist sehr einfach.",
|
"noAccount": "Kein XMPP-Konto vorhanden? Keine Sorge, es ist ganz einfach, eines zu erstellen.",
|
||||||
"loginButton": "Einloggen",
|
"loginButton": "Einloggen",
|
||||||
"registerButton": "Registrieren"
|
"registerButton": "Registrieren"
|
||||||
},
|
},
|
||||||
"login": {
|
"login": {
|
||||||
"title": "Login",
|
"title": "Login",
|
||||||
"xmppAddress": "XMPP-Adresse",
|
"xmppAddress": "XMPP-Adresse",
|
||||||
"password": "Passwort",
|
"password": "Passwort",
|
||||||
"advancedOptions": "Fortgeschrittene Optionen",
|
"advancedOptions": "Fortgeschrittene Optionen",
|
||||||
"createAccount": "Account auf dem Server erstellen"
|
"createAccount": "Konto auf dem Server erstellen"
|
||||||
},
|
},
|
||||||
"conversations": {
|
"conversations": {
|
||||||
"speeddialNewChat": "Neuer chat",
|
"speeddialNewChat": "Neuer chat",
|
||||||
"speeddialJoinGroupchat": "Gruppenchat beitreten",
|
"speeddialJoinGroupchat": "Gruppenchat beitreten",
|
||||||
"overlaySettings": "Einstellungen",
|
"speeddialAddNoteToSelf": "Notiz an mich",
|
||||||
"noOpenChats": "Du hast keine offenen chats",
|
"overlaySettings": "Einstellungen",
|
||||||
"startChat": "Einen chat anfangen"
|
"noOpenChats": "Du hast keine offenen chats",
|
||||||
},
|
"startChat": "Einen chat anfangen",
|
||||||
"conversation": {
|
"closeChat": "Chat schließen",
|
||||||
"unencrypted": "Unverschlüsselt",
|
"closeChatBody": "Bist du dir sicher, dass du den Chat mit ${conversationTitle} schließen möchtest?",
|
||||||
"encrypted": "Verschlüsselt",
|
"markAsRead": "Als gelesen markieren"
|
||||||
"closeChat": "Chat schließen",
|
},
|
||||||
"closeChatConfirmTitle": "Chat schließen",
|
"conversation": {
|
||||||
"closeChatConfirmSubtext": "Bist Du dir sicher, dass du den Chat schließen möchtest?",
|
"unencrypted": "Unverschlüsselt",
|
||||||
"blockUser": "Nutzer blockieren",
|
"encrypted": "Verschlüsselt",
|
||||||
"online": "Online",
|
"closeChat": "Chat schließen",
|
||||||
"retract": "Nachricht löschen",
|
"closeChatConfirmTitle": "Chat schließen",
|
||||||
"retractBody": "Bist du dir sicher, dass du die Nachricht löschen willst? Bedenke, dass dies nur eine Bitte ist, die dein gegenüber nicht beachten muss.",
|
"closeChatConfirmSubtext": "Bist Du dir sicher, dass du den Chat schließen möchtest?",
|
||||||
"forward": "Weiterleiten",
|
"blockShort": "Blockieren",
|
||||||
"edit": "Bearbeiten",
|
"blockUser": "Nutzer blockieren",
|
||||||
"quote": "Zitieren"
|
"online": "Online",
|
||||||
},
|
"retract": "Nachricht löschen",
|
||||||
"addcontact": {
|
"retractBody": "Bist du dir sicher, dass du die Nachricht löschen willst? Bedenke, dass dies nur eine Bitte ist, die dein gegenüber nicht beachten muss.",
|
||||||
"title": "Neuen Kontakt hinzufügen",
|
"forward": "Weiterleiten",
|
||||||
"xmppAddress": "XMPP-Adresse",
|
"edit": "Bearbeiten",
|
||||||
"subtitle": "Du kannst einen Kontakt hinzufügen, indem Du entweder die XMPP-Adresse eingibst oder den QR-Code deines Kontaktes scannst",
|
"quote": "Zitieren",
|
||||||
"buttonAddToContact": "Kontakt hinzufügen"
|
"copy": "Inhalt kopieren",
|
||||||
},
|
"messageCopied": "Nachrichteninhalt in die Zwischenablage kopiert",
|
||||||
"newconversation": {
|
"addReaction": "Reaktion hinzufügen",
|
||||||
"title": "Neuer chat",
|
"showError": "Fehler anzeigen",
|
||||||
"addContact": "Kontakt hinzufügen",
|
"showWarning": "Warnung anzeigen",
|
||||||
"createGroupchat": "Gruppenchat erstellen"
|
"warning": "Warnung",
|
||||||
},
|
"addToContacts": "Zu Kontaken hinzufügen",
|
||||||
"crop": {
|
"addToContactsTitle": "${jid} zu Kontakten hinzufügen",
|
||||||
"setProfilePicture": "Als Profilbild festlegen"
|
"addToContactsBody": "Bist du dir sicher, dass du ${jid} zu deinen Kontakten hinzufügen möchtest?",
|
||||||
},
|
"stickerPickerNoStickersLine1": "Du hast keine Stickerpacks installiert.",
|
||||||
"shareselection": {
|
"stickerPickerNoStickersLine2": "Diese können in den Stickereinstellungen installiert werden.",
|
||||||
"shareWith": "Teilen mit...",
|
"stickerSettings": "Stickereinstellungen",
|
||||||
"confirmTitle": "Dateien senden?",
|
"newDeviceMessage": {
|
||||||
"confirmBody": "Einer oder mehr Chats sind unverschlüsselt. Das bedeutet, dass die Dateien dem Server unverschlüsselt vorliegen. Dateien trotzdem senden?"
|
"one": "Ein neues Gerät wurde hinzugefügt",
|
||||||
},
|
"other": "Mehrere neue Geräte wurden hinzugefügt"
|
||||||
"profile": {
|
},
|
||||||
"self": {
|
"replacedDeviceMessage": {
|
||||||
"devices": "Geräte"
|
"one": "Ein Gerät hat sich verändert",
|
||||||
},
|
"other": "Mehrere Geräte haben sich verändert"
|
||||||
"conversation": {
|
},
|
||||||
"muteChatTooltip": "Chat stummschalten",
|
"messageHint": "Nachricht senden...",
|
||||||
"unmuteChatTooltip": "Chat lautstellen",
|
"sendImages": "Bilder senden",
|
||||||
"muteChat": "Stummschalten",
|
"sendFiles": "Dateien senden",
|
||||||
"unmuteChat": "Lautstellen",
|
"takePhotos": "Bilder aufnehmen"
|
||||||
"devices": "Geräte"
|
},
|
||||||
},
|
"startchat": {
|
||||||
"owndevices": {
|
"title": "Neuer Chat",
|
||||||
"title": "Eigene Geräte",
|
"xmppAddress": "XMPP-Adresse",
|
||||||
"thisDevice": "Dieses Gerät",
|
"subtitle": "Du kannst einen neuen Chat beginnen, indem du entweder eine XMPP-Adresse eingibst oder einen QR-Code scannst.",
|
||||||
"otherDevices": "Andere Geräte",
|
"buttonAddToContact": "Neuen Chat beginnen"
|
||||||
"deleteDeviceConfirmTitle": "Gerät löschen",
|
},
|
||||||
"deleteDeviceConfirmBody": "Das bedeutet, dass Kontakte für dieses Gerät nichtmehr verschlüsseln können. Fortfahren?",
|
"newconversation": {
|
||||||
"recreateOwnSessions": "Sessions neuerstellen",
|
"title": "Neuer Chat",
|
||||||
"recreateOwnSessionsConfirmTitle": "Eigene Sessions neuerstellen?",
|
"startChat": "Neuen Chat beginnen",
|
||||||
"recreateOwnSessionsConfirmBody": "Das wird alle kryptographischen Sessions mit den eigenen Geräten neuerstellen. Verwende dies nur, wenn deine eigenen Geräte Entschlüsselungsfehler erzeugen.",
|
"createGroupchat": "Gruppenchat erstellen"
|
||||||
"recreateOwnDevice": "Gerät neuerstellen",
|
},
|
||||||
"recreateOwnDeviceConfirmTitle": "Gerät neuerstellen?",
|
"crop": {
|
||||||
"recreateOwnDeviceConfirmBody": "Das wird die kryptographische Identität dieses Geräts neu erstellen. Wenn Kontakte die kryptographische Indentität verifiziert haben, dann müssen diese es erneut tun. Fortfahren?"
|
"setProfilePicture": "Als Profilbild festlegen"
|
||||||
},
|
},
|
||||||
"devices": {
|
"shareselection": {
|
||||||
"title": "Devices",
|
"shareWith": "Teilen mit...",
|
||||||
"recreateSessions": "Rebuild sessions",
|
"confirmTitle": "Dateien senden?",
|
||||||
"recreateSessionsConfirmTitle": "Rebuild sessions?",
|
"confirmBody": "Einer oder mehr Chats sind unverschlüsselt. Das bedeutet, dass die Dateien dem Server unverschlüsselt vorliegen. Dateien trotzdem senden?"
|
||||||
"recreateSessionsConfirmBody": "This will recreate the cryptographic sessions with your own devices. Use only if your own devices throw decryption errors."
|
},
|
||||||
}
|
"profile": {
|
||||||
},
|
"general": {
|
||||||
"blocklist": {
|
"omemo": "Sicherheit",
|
||||||
"title": "Blockliste",
|
"profile": "Profil",
|
||||||
"noUsersBlocked": "Du hast niemanden blockiert",
|
"media": "Medien"
|
||||||
"unblockAll": "Alle entblocken",
|
},
|
||||||
"unblockAllConfirmTitle": "Alle entblocken",
|
"conversation": {
|
||||||
"unblockAllConfirmBody": "Bist Du dir sicher, dass du alle geblockten Personen entblocken möchtest?",
|
"notifications": "Benachrichtigungen",
|
||||||
"unblockJidConfirmTitle": "${jid} entblocken?",
|
"notificationsMuted": "Stumm",
|
||||||
"unblockJidConfirmBody": "Bist du dir sicher, dass du ${jid} entblocken möchtest? Du wirst wieder Nachrichten von dieser Person erhalten können."
|
"notificationsEnabled": "Eingeschaltet",
|
||||||
},
|
"sharedMedia": "Medien"
|
||||||
"settings": {
|
},
|
||||||
"settings": {
|
"owndevices": {
|
||||||
"title": "Einstellungen",
|
"title": "Eigene Geräte",
|
||||||
"conversationsSection": "Unterhaltungen",
|
"thisDevice": "Dieses Gerät",
|
||||||
"accountSection": "Account",
|
"otherDevices": "Andere Geräte",
|
||||||
"signOut": "Abmelden",
|
"deleteDeviceConfirmTitle": "Gerät löschen",
|
||||||
"signOutConfirmTitle": "Abmelden",
|
"deleteDeviceConfirmBody": "Das bedeutet, dass Kontakte für dieses Gerät nichtmehr verschlüsseln können. Fortfahren?",
|
||||||
"signOutConfirmBody": "Du bist dabei dich abzumelden. Fortfahren?",
|
"recreateOwnSessions": "Sessions neuerstellen",
|
||||||
"miscellaneousSection": "Unterschiedlich",
|
"recreateOwnSessionsConfirmTitle": "Eigene Sessions neuerstellen?",
|
||||||
"debuggingSection": "Debugging"
|
"recreateOwnSessionsConfirmBody": "Das wird alle kryptographischen Sessions mit den eigenen Geräten neuerstellen. Verwende dies nur, wenn deine eigenen Geräte Entschlüsselungsfehler erzeugen.",
|
||||||
},
|
"recreateOwnDevice": "Gerät neuerstellen",
|
||||||
"about": {
|
"recreateOwnDeviceConfirmTitle": "Gerät neuerstellen?",
|
||||||
"title": "Über",
|
"recreateOwnDeviceConfirmBody": "Das wird die kryptographische Identität dieses Geräts neu erstellen. Wenn Kontakte die kryptographische Indentität verifiziert haben, dann müssen diese es erneut tun. Fortfahren?"
|
||||||
"licensed": "Lizensiert unter GPL3",
|
},
|
||||||
"viewSourceCode": "Quellcode anschauen"
|
"devices": {
|
||||||
},
|
"title": "Sicherheit",
|
||||||
"appearance": {
|
"recreateSessions": "Sessions zurücksetzen",
|
||||||
"title": "Aussehen",
|
"recreateSessionsConfirmTitle": "Sessions zurücksetzen?",
|
||||||
"languageSection": "Sprache",
|
"recreateSessionsConfirmBody": "Dies wird alle Sessions mit Deinen Geräten neu erstellen. Tue dies nur, wenn deine Geräte Fehler beim Entschlüsseln erzeugen.",
|
||||||
"language": "Appsprache",
|
"noSessions": "Es sind keine kryptographischen Sessions vorhanden, die für Ende-zu-Ende-Verschlüsselung verwendet werden."
|
||||||
"languageSubtext": "Aktuell ausgewählt: $selectedLanguage",
|
}
|
||||||
"systemLanguage": "Systemsprache"
|
},
|
||||||
},
|
"blocklist": {
|
||||||
"licenses": {
|
"title": "Blockliste",
|
||||||
"title": "Open-Source Lizenzen",
|
"noUsersBlocked": "Du hast niemanden blockiert",
|
||||||
"licensedUnder": "Lizensiert unter $license"
|
"unblockAll": "Alle entblocken",
|
||||||
},
|
"unblockAllConfirmTitle": "Alle entblocken",
|
||||||
"conversation": {
|
"unblockAllConfirmBody": "Bist Du dir sicher, dass du alle geblockten Personen entblocken möchtest?",
|
||||||
"title": "Chat",
|
"unblockJidConfirmTitle": "${jid} entblocken?",
|
||||||
"appearance": "Aussehen",
|
"unblockJidConfirmBody": "Bist du dir sicher, dass du ${jid} entblocken möchtest? Du wirst wieder Nachrichten von dieser Person erhalten können."
|
||||||
"selectBackgroundImage": "Hintergrundbild auswählen",
|
},
|
||||||
"selectBackgroundImageDescription": "Dieses Bild wird als Hintergrundbild in allen Chats verwendet",
|
"cropbackground": {
|
||||||
"removeBackgroundImage": "Hintergrundbild entfernen",
|
"blur": "Hintergrund weichzeichnen",
|
||||||
"removeBackgroundImageConfirmTitle": "Hintergrundbild entfernen",
|
"setAsBackground": "Als Hintergrundbild festlegen"
|
||||||
"removeBackgroundImageConfirmBody": "Bist Du dir sicher, dass Du das Hintergrundbild entfernen möchtest?",
|
},
|
||||||
"newChatsSection": "Neue Chats",
|
"stickerPack": {
|
||||||
"newChatsMuteByDefault": "Neue Chats standardmäßig stummschalten",
|
"removeConfirmTitle": "Stickerpack entfernen",
|
||||||
"newChatsE2EE": "Ende-zu-Ende-Verschlüsselung standardmäßig aktivieren. WARNUNG: Experimentell"
|
"removeConfirmBody": "Bist Du Dir sicher, dass du das Stickerpack entfernen möchtest?",
|
||||||
},
|
"installConfirmTitle": "Stickerpack installieren",
|
||||||
"debugging": {
|
"installConfirmBody": "Bist Du Dir sicher, dass Du das Stickerpack installieren möchtest?",
|
||||||
"title": "Debuggingoptionen",
|
"restricted": "Dieses Stickerpack ist eingeschränkt. Das bedeutet, dass es im Chat angezeigt wird, jedoch nicht versendet werden kann.",
|
||||||
"generalSection": "Generell",
|
"fetchingFailure": "Konnte das Stickerpack nicht finden"
|
||||||
"generalEnableDebugging": "Debugging einschalten",
|
},
|
||||||
"generalEncryptionPassword": "Verschlüsselungspasswort",
|
"settings": {
|
||||||
"generalEncryptionPasswordSubtext": "Die Logs enthalten eventuell sensible Daten. Wähle also daher eine starke Passphrase",
|
"settings": {
|
||||||
"generalLoggingIp": "Logging-IP",
|
"title": "Einstellungen",
|
||||||
"generalLoggingIpSubtext": "Die IP-Adresse an die die Logs gesendet werden",
|
"conversationsSection": "Unterhaltungen",
|
||||||
"generalLoggingPort": "Logging-Port",
|
"accountSection": "Konto",
|
||||||
"generalLoggingPortSubtext": "Der Port an den die Logs gesendet werden"
|
"signOut": "Abmelden",
|
||||||
},
|
"signOutConfirmTitle": "Abmelden",
|
||||||
"network": {
|
"signOutConfirmBody": "Du bist dabei dich abzumelden. Fortfahren?",
|
||||||
"title": "Netzwerk",
|
"miscellaneousSection": "Unterschiedlich",
|
||||||
"automaticDownloadsSection": "Automatische Downloads",
|
"debuggingSection": "Debugging",
|
||||||
"automaticDownloadsText": "Moxxy läd Dateien automatisch herunter, wenn verbunden mit...",
|
"general": "Generell"
|
||||||
"automaticDownloadsMaximumSize": "Maximale Downloadgröße",
|
},
|
||||||
"automaticDownloadsMaximumSizeSubtext": "Die maximale Dateigröße, die automatisch heruntergeladen werden soll",
|
"about": {
|
||||||
"wifi": "Wifi",
|
"title": "Über",
|
||||||
"mobileData": "Mobile Daten"
|
"licensed": "Lizensiert unter GPL3",
|
||||||
},
|
"version": "Version ${version}",
|
||||||
"privacy": {
|
"viewSourceCode": "Quellcode anschauen",
|
||||||
"title": "Privatsphäre",
|
"nMoreToGo": "Noch ${n}...",
|
||||||
"generalSection": "Generell",
|
"debugMenuShown": "Du bist jetzt ein Entwickler!",
|
||||||
"showContactRequests": "Kontaktanfragen zeigen",
|
"debugMenuAlreadyShown": "Du bist bereits ein Entwickler!"
|
||||||
"showContactRequestsSubtext": "Dies zeigt Personen in der Chatübersicht an, die Dich zu ihrer Kontaktliste hinzugefügt haben, aber noch keine Nachricht gesendet haben",
|
},
|
||||||
"profilePictureVisibility": "Öffentliches Profilbild",
|
"appearance": {
|
||||||
"profilePictureVisibilitSubtext": "Wenn aktiviert, dann kann jeder Dein Profilbild sehen. Wenn deaktiviert, dann können nur Personen aus deiner Kontaktliste kein Profilbild sehen",
|
"title": "Aussehen",
|
||||||
"autoAcceptSubscriptionRequests": "Subscriptionanfragen automatisch annehmen",
|
"languageSection": "Sprache",
|
||||||
"autoAcceptSubscriptionRequestsSubtext": "Wenn aktiviert, dann werden Subscriptionanfragen automatisch angenommen, wenn die Person in deiner Kontaktliste ist",
|
"language": "Appsprache",
|
||||||
"conversationsSection": "Unterhaltungen",
|
"languageSubtext": "Aktuell ausgewählt: $selectedLanguage",
|
||||||
"sendChatMarkers": "Chatmarker senden",
|
"systemLanguage": "Systemsprache"
|
||||||
"sendChatMarkersSubtext": "Dies teilt Deinem Gesprächspartner mit, ob du Nachrichten empfangen oder gelesen hast",
|
},
|
||||||
"sendChatStates": "Chatstates senden",
|
"licenses": {
|
||||||
"sendChatStatesSubtext": "Dies teilt Deinem Gesprächspartner mit, ob du gerade im Chat aktiv bist oder schreibst",
|
"title": "Open-Source Lizenzen",
|
||||||
"redirectsSection": "Weiterleitungen",
|
"licensedUnder": "Lizensiert unter $license"
|
||||||
"redirectText": "Dies leitet Links von $serviceName, die du öffnest, an einen Proxydienst weiter, wie zum Beispiel $exampleProxy",
|
},
|
||||||
"currentlySelected": "Aktuell ausgewählt: $proxy",
|
"conversation": {
|
||||||
"redirectsTitle": "${serviceName}weiterleitung",
|
"title": "Chat",
|
||||||
"cannotEnableRedirect": "Kann ${serviceName}weiterleitung nicht aktivieren",
|
"appearance": "Aussehen",
|
||||||
"cannotEnableRedirectSubtext": "Du must zuerst einen Proxydienst auswählen. Dazu berühre das Feld neben dem Schalter.",
|
"selectBackgroundImage": "Hintergrundbild auswählen",
|
||||||
"urlEmpty": "URL kann nicht leer sein",
|
"selectBackgroundImageDescription": "Dieses Bild wird als Hintergrundbild in allen Chats verwendet",
|
||||||
"urlInvalid": "Ungültige URL",
|
"removeBackgroundImage": "Hintergrundbild entfernen",
|
||||||
"redirectDialogTitle": "${serviceName}weiterleitung"
|
"removeBackgroundImageConfirmTitle": "Hintergrundbild entfernen",
|
||||||
}
|
"removeBackgroundImageConfirmBody": "Bist Du dir sicher, dass Du das Hintergrundbild entfernen möchtest?",
|
||||||
}
|
"newChatsSection": "Neue Chats",
|
||||||
|
"newChatsMuteByDefault": "Neue Chats standardmäßig stummschalten",
|
||||||
|
"newChatsE2EE": "Ende-zu-Ende-Verschlüsselung standardmäßig aktivieren. WARNUNG: Experimentell",
|
||||||
|
"behaviourSection": "Verhalten",
|
||||||
|
"contactsIntegration": "Kontaktintegration",
|
||||||
|
"contactsIntegrationBody": "Wenn aktiviert, dann werden Kontakte aus dem Kontaktbuch verwendet, um Chatnamen und Profilbilder anzuzeigen. Dabei werden keine Daten an den Server gesendet."
|
||||||
|
},
|
||||||
|
"debugging": {
|
||||||
|
"title": "Debuggingoptionen",
|
||||||
|
"generalSection": "Generell",
|
||||||
|
"generalEnableDebugging": "Debugging einschalten",
|
||||||
|
"generalEncryptionPassword": "Verschlüsselungspasswort",
|
||||||
|
"generalEncryptionPasswordSubtext": "Die Logs enthalten eventuell sensible Daten. Wähle also daher eine starke Passphrase",
|
||||||
|
"generalLoggingIp": "Logging-IP",
|
||||||
|
"generalLoggingIpSubtext": "Die IP-Adresse an die die Logs gesendet werden",
|
||||||
|
"generalLoggingPort": "Logging-Port",
|
||||||
|
"generalLoggingPortSubtext": "Der Port an den die Logs gesendet werden"
|
||||||
|
},
|
||||||
|
"network": {
|
||||||
|
"title": "Netzwerk",
|
||||||
|
"automaticDownloadsSection": "Automatische Downloads",
|
||||||
|
"automaticDownloadsText": "Moxxy läd Dateien automatisch herunter, wenn verbunden mit...",
|
||||||
|
"automaticDownloadsMaximumSize": "Maximale Downloadgröße",
|
||||||
|
"automaticDownloadsMaximumSizeSubtext": "Die maximale Dateigröße, die automatisch heruntergeladen werden soll",
|
||||||
|
"automaticDownloadAlways": "Immer",
|
||||||
|
"wifi": "WLAN",
|
||||||
|
"mobileData": "Mobile Daten"
|
||||||
|
},
|
||||||
|
"privacy": {
|
||||||
|
"title": "Privatsphäre",
|
||||||
|
"generalSection": "Generell",
|
||||||
|
"showContactRequests": "Kontaktanfragen zeigen",
|
||||||
|
"showContactRequestsSubtext": "Dies zeigt Personen in der Chatübersicht an, die Dich zu ihrer Kontaktliste hinzugefügt haben, aber noch keine Nachricht gesendet haben",
|
||||||
|
"profilePictureVisibility": "Öffentliches Profilbild",
|
||||||
|
"profilePictureVisibilitSubtext": "Wenn aktiviert, dann kann jeder Dein Profilbild sehen. Wenn deaktiviert, dann können nur Personen aus deiner Kontaktliste kein Profilbild sehen",
|
||||||
|
"conversationsSection": "Unterhaltungen",
|
||||||
|
"sendChatMarkers": "Chatmarker senden",
|
||||||
|
"sendChatMarkersSubtext": "Dies teilt Deinem Gesprächspartner mit, ob du Nachrichten empfangen oder gelesen hast",
|
||||||
|
"sendChatStates": "Chatstates senden",
|
||||||
|
"sendChatStatesSubtext": "Dies teilt Deinem Gesprächspartner mit, ob du gerade im Chat aktiv bist oder schreibst",
|
||||||
|
"redirectsSection": "Weiterleitungen",
|
||||||
|
"redirectText": "Dies leitet Links von $serviceName, die du öffnest, an einen Proxydienst weiter, wie zum Beispiel $exampleProxy",
|
||||||
|
"currentlySelected": "Aktuell ausgewählt: $proxy",
|
||||||
|
"redirectsTitle": "${serviceName}weiterleitung",
|
||||||
|
"cannotEnableRedirect": "Kann ${serviceName}weiterleitung nicht aktivieren",
|
||||||
|
"cannotEnableRedirectSubtext": "Du must zuerst einen Proxydienst auswählen. Dazu berühre das Feld neben dem Schalter.",
|
||||||
|
"urlEmpty": "URL kann nicht leer sein",
|
||||||
|
"urlInvalid": "Ungültige URL",
|
||||||
|
"redirectDialogTitle": "${serviceName}weiterleitung",
|
||||||
|
"stickersPrivacy": "Stickerliste öffentlich halten",
|
||||||
|
"stickersPrivacySubtext": "Wenn eingeschaltet, dann kann jeder die Liste Deiner installierten Stickerpacks sehen."
|
||||||
|
},
|
||||||
|
"stickers": {
|
||||||
|
"title": "Stickers",
|
||||||
|
"stickerSection": "Sticker",
|
||||||
|
"displayStickers": "Sticker im Chat anzeigen",
|
||||||
|
"autoDownload": "Sticker automatisch herunterladen",
|
||||||
|
"autoDownloadBody": "Wenn aktiviert, dann werden Sticker automatisch heruntergeladen, wenn der Sender in der Kontaktliste ist.",
|
||||||
|
"stickerPacksSection": "Stickerpacks",
|
||||||
|
"importStickerPack": "Stickerpack importieren",
|
||||||
|
"importSuccess": "Stickerpack erfolgreich importiert",
|
||||||
|
"importFailure": "Beim Import des Stickerpacks ist ein Fehler aufgetreten",
|
||||||
|
"stickerPackSize": "(${size})"
|
||||||
|
},
|
||||||
|
"storage": {
|
||||||
|
"title": "Speicher",
|
||||||
|
"sizePlaceholder": "Berechne...",
|
||||||
|
"storageManagement": "Speicherverwaltung",
|
||||||
|
"removeOldMedia": {
|
||||||
|
"title": "Alte Medien entfernen",
|
||||||
|
"description": "Löscht alte Medien vom Gerät"
|
||||||
|
},
|
||||||
|
"removeOldMediaDialog": {
|
||||||
|
"title": "Medien löschen",
|
||||||
|
"options": {
|
||||||
|
"all": "Alle Medien",
|
||||||
|
"oneMonth": "Älter als 1 Monat",
|
||||||
|
"oneWeek": "Älter als 1 Woche"
|
||||||
|
},
|
||||||
|
"delete": "Löschen",
|
||||||
|
"confirmation": {
|
||||||
|
"body": "Bist Du dir sicher, dass du alte Medien löschen möchtest?"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"viewMediaFiles": "Medien anzeigen",
|
||||||
|
"mediaFiles": "Medien",
|
||||||
|
"types": {
|
||||||
|
"media": "Medien",
|
||||||
|
"stickers": "Sticker"
|
||||||
|
},
|
||||||
|
"manageStickers": "Stickerpacks verwalten",
|
||||||
|
"storageUsed": "Speicherplatz verbraucht: ${size}"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"sharedMedia": {
|
||||||
|
"empty": {
|
||||||
|
"chat": "Keine Medien für diesen Chat vorhanden",
|
||||||
|
"general": "Keine Medien vorhanden"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
443
assets/i18n/strings_en.i18n.json
Normal file
443
assets/i18n/strings_en.i18n.json
Normal file
@@ -0,0 +1,443 @@
|
|||||||
|
{
|
||||||
|
"language": "English",
|
||||||
|
"global": {
|
||||||
|
"title": "Moxxy",
|
||||||
|
"moxxySubtitle": "An experiment into building a modern, easy and beautiful XMPP client.",
|
||||||
|
"dialogAccept": "Okay",
|
||||||
|
"dialogCancel": "Cancel",
|
||||||
|
"yes": "Yes",
|
||||||
|
"no": "No"
|
||||||
|
},
|
||||||
|
"notifications": {
|
||||||
|
"permanent": {
|
||||||
|
"idle": "Idle",
|
||||||
|
"ready": "Ready to receive messages",
|
||||||
|
"connecting": "Connecting…",
|
||||||
|
"disconnect": "Disconnected",
|
||||||
|
"error": "Error"
|
||||||
|
},
|
||||||
|
"message": {
|
||||||
|
"reply": "Reply",
|
||||||
|
"markAsRead": "Mark as read"
|
||||||
|
},
|
||||||
|
"channels": {
|
||||||
|
"messagesChannelName": "Messages",
|
||||||
|
"messagesChannelDescription": "The notification channel for received messages",
|
||||||
|
"warningChannelName": "Warnings",
|
||||||
|
"warningChannelDescription": "Warnings related to Moxxy",
|
||||||
|
"serviceChannelName": "Foreground Service",
|
||||||
|
"serviceChannelDescription": "Holds the persistent foreground service notification"
|
||||||
|
},
|
||||||
|
"titles": {
|
||||||
|
"error": "Error"
|
||||||
|
},
|
||||||
|
"warnings": {
|
||||||
|
"blockingError": {
|
||||||
|
"title": "Error while blocking user",
|
||||||
|
"body": "Could not block user ${jid} because your server does not support blocking."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"errors": {
|
||||||
|
"messageError": {
|
||||||
|
"title": "Message delivery failure",
|
||||||
|
"body": "Failed to deliver message to ${conversationTitle}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"permissions": {
|
||||||
|
"requests": {
|
||||||
|
"notification": {
|
||||||
|
"reason": "In order to notify of incoming messages, Moxxy needs permission to show notifications."
|
||||||
|
},
|
||||||
|
"batterySaving": {
|
||||||
|
"reason": "In order to receive messages in the background, Moxxy should be excempt from Android's battery saving."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"allow": "Allow",
|
||||||
|
"skip": "Skip"
|
||||||
|
},
|
||||||
|
"dateTime": {
|
||||||
|
"justNow": "Just now",
|
||||||
|
"nMinutesAgo": "${min}min ago",
|
||||||
|
"mondayAbbrev": "Mon",
|
||||||
|
"tuesdayAbbrev": "Tue",
|
||||||
|
"wednessdayAbbrev": "Wed",
|
||||||
|
"thursdayAbbrev": "Thu",
|
||||||
|
"fridayAbbrev": "Fri",
|
||||||
|
"saturdayAbbrev": "Sat",
|
||||||
|
"sundayAbbrev": "Sun",
|
||||||
|
"january": "January",
|
||||||
|
"february": "February",
|
||||||
|
"march": "March",
|
||||||
|
"april": "April",
|
||||||
|
"may": "May",
|
||||||
|
"june": "June",
|
||||||
|
"july": "July",
|
||||||
|
"august": "August",
|
||||||
|
"september": "September",
|
||||||
|
"october": "October",
|
||||||
|
"november": "November",
|
||||||
|
"december": "December",
|
||||||
|
"today": "Today",
|
||||||
|
"yesterday": "Yesterday"
|
||||||
|
},
|
||||||
|
"messages": {
|
||||||
|
"image": "Image",
|
||||||
|
"video": "Video",
|
||||||
|
"audio": "Audio",
|
||||||
|
"file": "File",
|
||||||
|
"sticker": "Sticker",
|
||||||
|
"retracted": "The message has been retracted",
|
||||||
|
"retractedFallback": "A previous message has been retracted but your client does not support it",
|
||||||
|
"you": "You"
|
||||||
|
},
|
||||||
|
"errors": {
|
||||||
|
"general": {
|
||||||
|
"noInternet": "Not connected to the Internet."
|
||||||
|
},
|
||||||
|
"filePicker": {
|
||||||
|
"permissionDenied": "The storage permission has been denied."
|
||||||
|
},
|
||||||
|
"omemo": {
|
||||||
|
"couldNotPublish": "Could not publish the cryptographic identity to the server. This means that end-to-end encryption may not work.",
|
||||||
|
"notEncryptedForDevice": "This message was not encrypted for this device",
|
||||||
|
"invalidHmac": "Could not decrypt message",
|
||||||
|
"noDecryptionKey": "No decryption key available",
|
||||||
|
"messageInvalidAfixElement": "Invalid encrypted message",
|
||||||
|
"verificationInvalidOmemoUrl": "Invalid OMEMO:2 fingerprint",
|
||||||
|
"verificationWrongJid": "Wrong XMPP-address",
|
||||||
|
"verificationWrongDevice": "Wrong OMEMO:2 device",
|
||||||
|
"verificationNotInList": "Wrong OMEMO:2 device",
|
||||||
|
"verificationWrongFingerprint": "Wrong OMEMO:2 fingerprint"
|
||||||
|
},
|
||||||
|
"connection": {
|
||||||
|
"connectionTimeout": "Could not connect to server",
|
||||||
|
"saslAccountDisabled": "Your account is disabled",
|
||||||
|
"saslInvalidCredentials": "Your account credentials are invalid",
|
||||||
|
"unrecoverable": "Connection lost due to unrecoverable error"
|
||||||
|
},
|
||||||
|
"login": {
|
||||||
|
"saslFailed": "Invalid login credentials",
|
||||||
|
"startTlsFailed": "Failed to establish a secure connection",
|
||||||
|
"noConnection": "Failed to establish a connection",
|
||||||
|
"unspecified": "Unspecified error"
|
||||||
|
},
|
||||||
|
"message": {
|
||||||
|
"unspecified": "Unknown error",
|
||||||
|
"fileUploadFailed": "The file upload failed",
|
||||||
|
"contactDoesntSupportOmemo": "The contact does not support encryption using OMEMO:2",
|
||||||
|
"fileDownloadFailed": "The file download failed",
|
||||||
|
"serviceUnavailable": "The message could not be delivered to the contact",
|
||||||
|
"remoteServerTimeout": "The message could not be delivered to the contact's server",
|
||||||
|
"remoteServerNotFound": "The message could not be delivered to the contact's server as it cannot be found",
|
||||||
|
"failedToEncrypt": "The message could not be encrypted",
|
||||||
|
"failedToEncryptFile": "The file could not be encrypted",
|
||||||
|
"failedToDecryptFile": "The file could not be decrypted",
|
||||||
|
"fileNotEncrypted": "The chat is encrypted but the file is not encrypted"
|
||||||
|
},
|
||||||
|
"conversation": {
|
||||||
|
"audioRecordingError": "Failed to finalize audio recording",
|
||||||
|
"openFileNoAppError": "No app found to open this file",
|
||||||
|
"openFileGenericError": "Failed to open file",
|
||||||
|
"messageErrorDialogTitle": "Error"
|
||||||
|
},
|
||||||
|
"newChat": {
|
||||||
|
"remoteServerError": "Failed to contact the remote server.",
|
||||||
|
"groupchatUnsupported": "Joining a groupchat is currently not supported.",
|
||||||
|
"unknown": "Unknown error."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"warnings": {
|
||||||
|
"message": {
|
||||||
|
"integrityCheckFailed": "Could not verify file integrity"
|
||||||
|
},
|
||||||
|
"conversation": {
|
||||||
|
"holdForLonger": "Hold button longer to record a voice message"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"pages": {
|
||||||
|
"intro": {
|
||||||
|
"noAccount": "Have no XMPP account? No worries, creating one is really easy.",
|
||||||
|
"loginButton": "Login",
|
||||||
|
"registerButton": "Register"
|
||||||
|
},
|
||||||
|
"login": {
|
||||||
|
"title": "Login",
|
||||||
|
"xmppAddress": "XMPP-Address",
|
||||||
|
"password": "Password",
|
||||||
|
"advancedOptions": "Advanced options",
|
||||||
|
"createAccount": "Create account on server"
|
||||||
|
},
|
||||||
|
"conversations": {
|
||||||
|
"speeddialNewChat": "New chat",
|
||||||
|
"speeddialJoinGroupchat": "Join groupchat",
|
||||||
|
"speeddialAddNoteToSelf": "Note to self",
|
||||||
|
"overlaySettings": "Settings",
|
||||||
|
"noOpenChats": "You have no open chats",
|
||||||
|
"startChat": "Start a chat",
|
||||||
|
"closeChat": "Close chat",
|
||||||
|
"closeChatBody": "Are you sure you want to close the chat with ${conversationTitle}?",
|
||||||
|
"markAsRead": "Mark as read"
|
||||||
|
},
|
||||||
|
"conversation": {
|
||||||
|
"unencrypted": "Unencrypted",
|
||||||
|
"encrypted": "Encrypted",
|
||||||
|
"closeChat": "Close chat",
|
||||||
|
"closeChatConfirmTitle": "Close chat",
|
||||||
|
"closeChatConfirmSubtext": "Are you sure you want to close this chat?",
|
||||||
|
"blockShort": "Block",
|
||||||
|
"blockUser": "Block user",
|
||||||
|
"online": "Online",
|
||||||
|
"retract": "Retract message",
|
||||||
|
"retractBody": "Are you sure you want to retract the message? Keep in mind that this is only a request that the client does not have to honour.",
|
||||||
|
"forward": "Forward",
|
||||||
|
"edit": "Edit",
|
||||||
|
"quote": "Quote",
|
||||||
|
"copy": "Copy content",
|
||||||
|
"messageCopied": "Message content copied to clipboard",
|
||||||
|
"addReaction": "Add reaction",
|
||||||
|
"showError": "Show error",
|
||||||
|
"showWarning": "Show warning",
|
||||||
|
"warning": "Warning",
|
||||||
|
"addToContacts": "Add to contacts",
|
||||||
|
"addToContactsTitle": "Add ${jid} to contacts",
|
||||||
|
"addToContactsBody": "Are you sure you want to add ${jid} to your contacts?",
|
||||||
|
"stickerPickerNoStickersLine1": "You have no sticker packs installed.",
|
||||||
|
"stickerPickerNoStickersLine2": "They can be installed in the sticker settings.",
|
||||||
|
"stickerSettings": "Sticker settings",
|
||||||
|
"newDeviceMessage": {
|
||||||
|
"one": "A new device has been added",
|
||||||
|
"other": "Multiple new devices have been added"
|
||||||
|
},
|
||||||
|
"replacedDeviceMessage": {
|
||||||
|
"one": "A device has been changed",
|
||||||
|
"other": "Multiple devices have been added"
|
||||||
|
},
|
||||||
|
"messageHint": "Send a message…",
|
||||||
|
"sendImages": "Send images",
|
||||||
|
"sendFiles": "Send files",
|
||||||
|
"takePhotos": "Take photos"
|
||||||
|
},
|
||||||
|
"startchat": {
|
||||||
|
"title": "New Chat",
|
||||||
|
"xmppAddress": "XMPP address",
|
||||||
|
"subtitle": "You can start a new chat by either entering a XMPP address or by scanning their QR code.",
|
||||||
|
"buttonAddToContact": "Start new chat"
|
||||||
|
},
|
||||||
|
"newconversation": {
|
||||||
|
"title": "New chat",
|
||||||
|
"startChat": "Start new chat",
|
||||||
|
"createGroupchat": "New groupchat",
|
||||||
|
"joinGroupChat": "Join groupchat",
|
||||||
|
"nullNickname": "Nickname cannot be null!",
|
||||||
|
"nick": "Nickname",
|
||||||
|
"enterNickname": "Enter Nickname",
|
||||||
|
"nicknameSubtitle": "You need to enter a unique nickname in order to join a MUC."
|
||||||
|
},
|
||||||
|
"crop": {
|
||||||
|
"setProfilePicture": "Set as profile picture"
|
||||||
|
},
|
||||||
|
"shareselection": {
|
||||||
|
"shareWith": "Share with…",
|
||||||
|
"confirmTitle": "Send file",
|
||||||
|
"confirmBody": "One or more chats are unencrypted. This means that the file will be leaked to the server. Do you still want to continue?"
|
||||||
|
},
|
||||||
|
"profile": {
|
||||||
|
"general": {
|
||||||
|
"omemo": "Security",
|
||||||
|
"profile": "Profile",
|
||||||
|
"media": "Media"
|
||||||
|
},
|
||||||
|
"conversation": {
|
||||||
|
"notifications": "Notifications",
|
||||||
|
"notificationsMuted": "Muted",
|
||||||
|
"notificationsEnabled": "Enabled",
|
||||||
|
"sharedMedia": "Media"
|
||||||
|
},
|
||||||
|
"owndevices": {
|
||||||
|
"title": "Own Devices",
|
||||||
|
"thisDevice": "This device",
|
||||||
|
"otherDevices": "Other devices",
|
||||||
|
"deleteDeviceConfirmTitle": "Delete device",
|
||||||
|
"deleteDeviceConfirmBody": "This means that contacts will not be able to encrypt for that device. Continue?",
|
||||||
|
"recreateOwnSessions": "Rebuild sessions",
|
||||||
|
"recreateOwnSessionsConfirmTitle": "Recreate own sessions?",
|
||||||
|
"recreateOwnSessionsConfirmBody": "This will recreate the cryptographic sessions with your own devices. Use only if your own devices throw decryption errors.",
|
||||||
|
"recreateOwnDevice": "Recreate device",
|
||||||
|
"recreateOwnDeviceConfirmTitle": "Recreate own device?",
|
||||||
|
"recreateOwnDeviceConfirmBody": "This will recreate this device's cryptographic identity. It will take some time. If contacts verified your device, they will have to do it again. Continue?"
|
||||||
|
},
|
||||||
|
"devices": {
|
||||||
|
"title": "Security",
|
||||||
|
"recreateSessions": "Rebuild sessions",
|
||||||
|
"recreateSessionsConfirmTitle": "Rebuild sessions?",
|
||||||
|
"recreateSessionsConfirmBody": "This will recreate the cryptographic sessions with your own devices. Use only if your own devices throw decryption errors.",
|
||||||
|
"noSessions": "There are no cryptographic sessions that are used for end-to-end encryption."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"blocklist": {
|
||||||
|
"title": "Blocklist",
|
||||||
|
"noUsersBlocked": "You have no users blocked",
|
||||||
|
"unblockAll": "Unblock all",
|
||||||
|
"unblockAllConfirmTitle": "Are you sure?",
|
||||||
|
"unblockAllConfirmBody": "Are you sure you want to unblock all users?",
|
||||||
|
"unblockJidConfirmTitle": "Unblock ${jid}?",
|
||||||
|
"unblockJidConfirmBody": "Are you sure you want to unblock ${jid}? You will receive messages from this user again."
|
||||||
|
},
|
||||||
|
"cropbackground": {
|
||||||
|
"blur": "Blur background",
|
||||||
|
"setAsBackground": "Set as background image"
|
||||||
|
},
|
||||||
|
"stickerPack": {
|
||||||
|
"removeConfirmTitle": "Remove sticker pack",
|
||||||
|
"removeConfirmBody": "Are you sure you want to remove this sticker pack?",
|
||||||
|
"installConfirmTitle": "Install sticker pack",
|
||||||
|
"installConfirmBody": "Are you sure you want to install this sticker pack?",
|
||||||
|
"restricted": "This sticker pack is restricted. That means that the stickers will be displayed but cannot be sent.",
|
||||||
|
"fetchingFailure": "Could not find the sticker pack"
|
||||||
|
},
|
||||||
|
"sharedMedia": {
|
||||||
|
"empty": {
|
||||||
|
"chat": "No shared media for this chat",
|
||||||
|
"general": "No media files available"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"settings": {
|
||||||
|
"settings": {
|
||||||
|
"title": "Settings",
|
||||||
|
"conversationsSection": "Conversations",
|
||||||
|
"accountSection": "Account",
|
||||||
|
"signOut": "Sign out",
|
||||||
|
"signOutConfirmTitle": "Sign Out",
|
||||||
|
"signOutConfirmBody": "You are about to sign out. Proceed?",
|
||||||
|
"miscellaneousSection": "Miscellaneous",
|
||||||
|
"debuggingSection": "Debugging",
|
||||||
|
"general": "General"
|
||||||
|
},
|
||||||
|
"about": {
|
||||||
|
"title": "About",
|
||||||
|
"licensed": "Licensed under GPL3",
|
||||||
|
"version": "Version ${version}",
|
||||||
|
"viewSourceCode": "View source code",
|
||||||
|
"nMoreToGo": "${n} more to go…",
|
||||||
|
"debugMenuShown": "You are now a developer!",
|
||||||
|
"debugMenuAlreadyShown": "You are already a developer!"
|
||||||
|
},
|
||||||
|
"appearance": {
|
||||||
|
"title": "Appearance",
|
||||||
|
"languageSection": "Language",
|
||||||
|
"language": "App language",
|
||||||
|
"languageSubtext": "Currently selected: $selectedLanguage",
|
||||||
|
"systemLanguage": "Default language"
|
||||||
|
},
|
||||||
|
"licenses": {
|
||||||
|
"title": "Open-Source Licenses",
|
||||||
|
"licensedUnder": "Licensed under $license"
|
||||||
|
},
|
||||||
|
"conversation": {
|
||||||
|
"title": "Chat",
|
||||||
|
"appearance": "Appearance",
|
||||||
|
"selectBackgroundImage": "Select background image",
|
||||||
|
"selectBackgroundImageDescription": "This image will be the background of all your chats",
|
||||||
|
"removeBackgroundImage": "Remove background image",
|
||||||
|
"removeBackgroundImageConfirmTitle": "Remove background image",
|
||||||
|
"removeBackgroundImageConfirmBody": "Are you sure you want to remove your conversation background image?",
|
||||||
|
"newChatsSection": "New Conversations",
|
||||||
|
"newChatsMuteByDefault": "Mute new chats by default",
|
||||||
|
"newChatsE2EE": "Enable end-to-end encryption by default. WARNING: Experimental",
|
||||||
|
"behaviourSection": "Behaviour",
|
||||||
|
"contactsIntegration": "Contacts integration",
|
||||||
|
"contactsIntegrationBody": "When enabled, data from the phonebook will be used to provide chat titles and profile pictures. No data will be sent to the server."
|
||||||
|
},
|
||||||
|
"debugging": {
|
||||||
|
"title": "Debugging options",
|
||||||
|
"generalSection": "General",
|
||||||
|
"generalEnableDebugging": "Enable debugging",
|
||||||
|
"generalEncryptionPassword": "Encryption password",
|
||||||
|
"generalEncryptionPasswordSubtext": "The logs may contain sensitive information so pick a strong passphrase",
|
||||||
|
"generalLoggingIp": "Logging IP",
|
||||||
|
"generalLoggingIpSubtext": "The IP the logs should be sent to",
|
||||||
|
"generalLoggingPort": "Logging Port",
|
||||||
|
"generalLoggingPortSubtext": "The IP the logs should be sent to"
|
||||||
|
},
|
||||||
|
"network": {
|
||||||
|
"title": "Network",
|
||||||
|
"automaticDownloadsSection": "Automatic Downloads",
|
||||||
|
"automaticDownloadsText": "Moxxy will automatically download files on…",
|
||||||
|
"automaticDownloadsMaximumSize": "Maximum Download Size",
|
||||||
|
"automaticDownloadsMaximumSizeSubtext": "The maximum file size for a file to be automatically downloaded",
|
||||||
|
"automaticDownloadAlways": "Always",
|
||||||
|
"wifi": "Wifi",
|
||||||
|
"mobileData": "Mobile data"
|
||||||
|
},
|
||||||
|
"privacy": {
|
||||||
|
"title": "Privacy",
|
||||||
|
"generalSection": "General",
|
||||||
|
"showContactRequests": "Show contact requests",
|
||||||
|
"showContactRequestsSubtext": "This will show people who added you to their contact list but sent no message yet",
|
||||||
|
"profilePictureVisibility": "Make profile picture public",
|
||||||
|
"profilePictureVisibilitSubtext": "If enabled, everyone can see your profile picture. If disabled, only users on your contact list can see your profile picture.",
|
||||||
|
"conversationsSection": "Conversation",
|
||||||
|
"sendChatMarkers": "Send chat markers",
|
||||||
|
"sendChatMarkersSubtext": "This will tell your conversation partner if you received or read a message",
|
||||||
|
"sendChatStates": "Send chat states",
|
||||||
|
"sendChatStatesSubtext": "This will show your conversation partner if you are typing or looking at the chat",
|
||||||
|
"redirectsSection": "Redirects",
|
||||||
|
"redirectText": "This will redirect $serviceName links that you tap to a proxy service, e.g. $exampleProxy",
|
||||||
|
"currentlySelected": "Currently selected: $proxy",
|
||||||
|
"redirectsTitle": "$serviceName Redirect",
|
||||||
|
"cannotEnableRedirect": "Cannot enable $serviceName redirects",
|
||||||
|
"cannotEnableRedirectSubtext": "You must first set a proxy service to redirect to. To do so, tap the field next to the switch.",
|
||||||
|
"urlEmpty": "URL cannot be empty",
|
||||||
|
"urlInvalid": "Invalid URL",
|
||||||
|
"redirectDialogTitle": "$serviceName Redirect",
|
||||||
|
"stickersPrivacy": "Keep sticker list public",
|
||||||
|
"stickersPrivacySubtext": "If enabled, everyone will be able to see your list of installed sticker packs."
|
||||||
|
},
|
||||||
|
"stickers": {
|
||||||
|
"title": "Stickers",
|
||||||
|
"stickerSection": "Sticker",
|
||||||
|
"displayStickers": "Display stickers in chat",
|
||||||
|
"autoDownload": "Automatically download stickers",
|
||||||
|
"autoDownloadBody": "If enabled, stickers are automatically downloaded when the sender is in your contact list.",
|
||||||
|
"stickerPacksSection": "Sticker packs",
|
||||||
|
"importStickerPack": "Import sticker pack",
|
||||||
|
"importSuccess": "Sticker pack successfully imported",
|
||||||
|
"importFailure": "Failed to import sticker pack",
|
||||||
|
"stickerPackSize": "(${size})"
|
||||||
|
},
|
||||||
|
"stickerPacks": {
|
||||||
|
"title": "Sticker Packs"
|
||||||
|
},
|
||||||
|
"storage": {
|
||||||
|
"title": "Storage",
|
||||||
|
"storageUsed": "Storage used: ${size}",
|
||||||
|
"sizePlaceholder": "Computing…",
|
||||||
|
"storageManagement": "Storage Management",
|
||||||
|
"removeOldMedia": {
|
||||||
|
"title": "Remove old media",
|
||||||
|
"description": "Removes old media files from the device"
|
||||||
|
},
|
||||||
|
"removeOldMediaDialog": {
|
||||||
|
"title": "Delete media files",
|
||||||
|
"options": {
|
||||||
|
"all": "All media files",
|
||||||
|
"oneWeek": "Older than 1 week",
|
||||||
|
"oneMonth": "Older than 1 month"
|
||||||
|
},
|
||||||
|
"delete": "Delete",
|
||||||
|
"confirmation": {
|
||||||
|
"body": "Are you sure you want to delete old media files?"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"viewMediaFiles": "View media files",
|
||||||
|
"mediaFiles": "Media Files",
|
||||||
|
"types": {
|
||||||
|
"media": "Media",
|
||||||
|
"stickers": "Stickers"
|
||||||
|
},
|
||||||
|
"manageStickers": "Manage sticker packs"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
435
assets/i18n/strings_fr.i18n.json
Normal file
435
assets/i18n/strings_fr.i18n.json
Normal file
@@ -0,0 +1,435 @@
|
|||||||
|
{
|
||||||
|
"language": "Français",
|
||||||
|
"global": {
|
||||||
|
"yes": "Oui",
|
||||||
|
"no": "Non",
|
||||||
|
"title": "Moxxy",
|
||||||
|
"dialogAccept": "OK",
|
||||||
|
"dialogCancel": "Annuler",
|
||||||
|
"moxxySubtitle": "Une expérience de construire un facile, moderne et belle client XMPP."
|
||||||
|
},
|
||||||
|
"notifications": {
|
||||||
|
"permanent": {
|
||||||
|
"ready": "Prêt pour recevoir des messages",
|
||||||
|
"disconnect": "Déconnecté",
|
||||||
|
"error": "Erreur",
|
||||||
|
"connecting": "Connexion…",
|
||||||
|
"idle": "Inactif"
|
||||||
|
},
|
||||||
|
"message": {
|
||||||
|
"markAsRead": "Marquer comme lu",
|
||||||
|
"reply": "Répondre"
|
||||||
|
},
|
||||||
|
"channels": {
|
||||||
|
"messagesChannelName": "Messages",
|
||||||
|
"warningChannelName": "Alertes",
|
||||||
|
"warningChannelDescription": "Alertes liées à Moxxy",
|
||||||
|
"messagesChannelDescription": "La voie des notifications pour des messages reçu"
|
||||||
|
},
|
||||||
|
"errors": {
|
||||||
|
"messageError": {
|
||||||
|
"body": "N'est pas parvenu à livrée la message au ${conversationTitle}",
|
||||||
|
"title": "Échec au livraison du message"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"titles": {
|
||||||
|
"error": "Erreur"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"messages": {
|
||||||
|
"file": "Fichier",
|
||||||
|
"image": "Image",
|
||||||
|
"video": "Vidéo",
|
||||||
|
"audio": "Audio",
|
||||||
|
"sticker": "Autocollant",
|
||||||
|
"retracted": "La message a été retirée",
|
||||||
|
"you": "Toi",
|
||||||
|
"retractedFallback": "Un précédent message a été retiré, mais votre client ne le prend pas en charge"
|
||||||
|
},
|
||||||
|
"permissions": {
|
||||||
|
"allow": "Autoriser",
|
||||||
|
"skip": "Sauter",
|
||||||
|
"requests": {
|
||||||
|
"notification": {
|
||||||
|
"reason": "Pour notifier des nouvelles messages, Moxxy à besoin de permission pour afficher les notifications."
|
||||||
|
},
|
||||||
|
"batterySaving": {
|
||||||
|
"reason": "Pour recevoir des messages dans l'arrière-plan, Moxxy devrait être exempté de la fonction d'économie de la batterie d'Android."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"dateTime": {
|
||||||
|
"justNow": "A l'instant",
|
||||||
|
"nMinutesAgo": "Il y a ${min}min",
|
||||||
|
"mondayAbbrev": "Lun",
|
||||||
|
"tuesdayAbbrev": "Mar",
|
||||||
|
"wednessdayAbbrev": "Mer",
|
||||||
|
"thursdayAbbrev": "Jeu",
|
||||||
|
"fridayAbbrev": "Ven",
|
||||||
|
"saturdayAbbrev": "Sam",
|
||||||
|
"sundayAbbrev": "Dim",
|
||||||
|
"january": "Janvier",
|
||||||
|
"february": "Février",
|
||||||
|
"march": "Mars",
|
||||||
|
"april": "Avril",
|
||||||
|
"may": "Mai",
|
||||||
|
"june": "Juin",
|
||||||
|
"july": "Juillet",
|
||||||
|
"august": "Août",
|
||||||
|
"september": "Septembre",
|
||||||
|
"october": "Octobre",
|
||||||
|
"november": "Novembre",
|
||||||
|
"december": "Décembre",
|
||||||
|
"today": "Aujourd’hui",
|
||||||
|
"yesterday": "Hier"
|
||||||
|
},
|
||||||
|
"errors": {
|
||||||
|
"general": {
|
||||||
|
"noInternet": "N'est pas connecté au l'Internet."
|
||||||
|
},
|
||||||
|
"omemo": {
|
||||||
|
"notEncryptedForDevice": "Cet message n'était pas chiffré pour cette appareil",
|
||||||
|
"invalidHmac": "Ne pouvait pas déchiffrer la message",
|
||||||
|
"noDecryptionKey": "Aucune clé de déchiffrer disponible",
|
||||||
|
"messageInvalidAfixElement": "Message chiffré invalide",
|
||||||
|
"verificationWrongJid": "Mauvais adresse XMPP",
|
||||||
|
"verificationWrongDevice": "Mauvais appareil OMEMO:2",
|
||||||
|
"verificationNotInList": "Mauvais appareil OMEMO:2",
|
||||||
|
"verificationWrongFingerprint": "Empreinte OMEMO:2 invalide",
|
||||||
|
"verificationInvalidOmemoUrl": "Empreinte OMEMO:2 invalide",
|
||||||
|
"couldNotPublish": "Ne pouvait pas publier l'identité cryptographique au la serveur. Cela signifie que chiffrement de bout en bout risque de ne pas fonctionner."
|
||||||
|
},
|
||||||
|
"filePicker": {
|
||||||
|
"permissionDenied": "L'autorisation de permission de storage a été déclinée."
|
||||||
|
},
|
||||||
|
"login": {
|
||||||
|
"saslFailed": "Identifiants non-valides",
|
||||||
|
"startTlsFailed": "Impossible d'établir une connexion sécurisée",
|
||||||
|
"unspecified": "Erreur non-connue",
|
||||||
|
"noConnection": "Impossible d'établir une connexion"
|
||||||
|
},
|
||||||
|
"message": {
|
||||||
|
"unspecified": "Erreur inconnue",
|
||||||
|
"fileUploadFailed": "Le téléversement du fichier a échoué",
|
||||||
|
"contactDoesntSupportOmemo": "Le chiffrement OMEMO:2 n'est pas pris en charge par l'autre personne",
|
||||||
|
"serviceUnavailable": "Le message n'a pas pu être délivré",
|
||||||
|
"failedToEncrypt": "Le message n'a pas pu être chiffré",
|
||||||
|
"failedToEncryptFile": "Le fichier n'a pas pu être chiffré",
|
||||||
|
"failedToDecryptFile": "Le fichier n'a pas pu être déchiffré",
|
||||||
|
"fileNotEncrypted": "La conversation est chiffrée, mais pas le fichier",
|
||||||
|
"fileDownloadFailed": "Le téléchargement du fichier a échoué",
|
||||||
|
"remoteServerTimeout": "Le message n'a pas pu être délivré au serveur de destination",
|
||||||
|
"remoteServerNotFound": "Serveur introuvable ; Le message n'a pas pu être délivré"
|
||||||
|
},
|
||||||
|
"connection": {
|
||||||
|
"connectionTimeout": "Impossible de joindre le serveur",
|
||||||
|
"saslAccountDisabled": "Votre compte est désactivé",
|
||||||
|
"saslInvalidCredentials": "Vos identifiants ne sont pas valides",
|
||||||
|
"unrecoverable": "Connexion perdue due à une erreur"
|
||||||
|
},
|
||||||
|
"conversation": {
|
||||||
|
"audioRecordingError": "Erreur d'enregistrement audio",
|
||||||
|
"openFileNoAppError": "Pas d'application pour ce type de fichier",
|
||||||
|
"messageErrorDialogTitle": "Erreur",
|
||||||
|
"openFileGenericError": "Impossible d'ouvrir le fichier"
|
||||||
|
},
|
||||||
|
"newChat": {
|
||||||
|
"remoteServerError": "Impossible de contacter le serveur distant.",
|
||||||
|
"groupchatUnsupported": "Les conversations de groupes ne sont pas encore prises en charge.",
|
||||||
|
"unknown": "Erreur inconnue."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"pages": {
|
||||||
|
"settings": {
|
||||||
|
"stickers": {
|
||||||
|
"title": "Autocollants",
|
||||||
|
"stickerSection": "Autocollant",
|
||||||
|
"stickerPacksSection": "Paquets d'autocollants",
|
||||||
|
"stickerPackSize": "(${size})",
|
||||||
|
"displayStickers": "Afficher les autocollants dans les conversations",
|
||||||
|
"autoDownload": "Téléchargement automatique des autocollants",
|
||||||
|
"importStickerPack": "Importer série d'autocollants",
|
||||||
|
"importSuccess": "Série d'autocollants importée avec succès",
|
||||||
|
"importFailure": "Erreur lors de l'import de la série d'autocollants",
|
||||||
|
"autoDownloadBody": "Si activée, les autocollants seront téléchargés automatiquement avec les personnes de votre liste de contact."
|
||||||
|
},
|
||||||
|
"stickerPacks": {
|
||||||
|
"title": "Paquets d'autocollants"
|
||||||
|
},
|
||||||
|
"storage": {
|
||||||
|
"types": {
|
||||||
|
"stickers": "Autocollants",
|
||||||
|
"media": "Médias"
|
||||||
|
},
|
||||||
|
"title": "Stockage",
|
||||||
|
"storageUsed": "Espace utilisé : ${size}",
|
||||||
|
"storageManagement": "Gestion de stockage",
|
||||||
|
"removeOldMedia": {
|
||||||
|
"title": "Supprimer les anciens médias",
|
||||||
|
"description": "Supprimer les anciens fichiers médias de votre appareil"
|
||||||
|
},
|
||||||
|
"removeOldMediaDialog": {
|
||||||
|
"title": "Supprimer les fichiers médias",
|
||||||
|
"options": {
|
||||||
|
"oneWeek": "Plus anciens qu'une semaine",
|
||||||
|
"oneMonth": "Plus anciens qu'un mois",
|
||||||
|
"all": "Tous les fichiers médias"
|
||||||
|
},
|
||||||
|
"delete": "Supprimer",
|
||||||
|
"confirmation": {
|
||||||
|
"body": "Confirmer la suppresion des anciens fichiers médias ?"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"viewMediaFiles": "Voir les fichiers médias",
|
||||||
|
"mediaFiles": "Fichiers médias",
|
||||||
|
"manageStickers": "Gestion des autocollants",
|
||||||
|
"sizePlaceholder": "Calcul…"
|
||||||
|
},
|
||||||
|
"settings": {
|
||||||
|
"title": "Paramètres",
|
||||||
|
"conversationsSection": "Conversations",
|
||||||
|
"accountSection": "Compte",
|
||||||
|
"signOut": "Déconnexion",
|
||||||
|
"signOutConfirmBody": "Confirmer la déconnexion ?",
|
||||||
|
"miscellaneousSection": "Autres",
|
||||||
|
"debuggingSection": "Développement",
|
||||||
|
"general": "Général",
|
||||||
|
"signOutConfirmTitle": "Déconnexion"
|
||||||
|
},
|
||||||
|
"about": {
|
||||||
|
"title": "À propos",
|
||||||
|
"licensed": "Sous licence GPLv3",
|
||||||
|
"version": "Version ${version}",
|
||||||
|
"viewSourceCode": "Voir le code source",
|
||||||
|
"debugMenuShown": "Mode développement disponible !",
|
||||||
|
"debugMenuAlreadyShown": "Le mode développement est déjà disponible !",
|
||||||
|
"nMoreToGo": "Plus que ${n}…"
|
||||||
|
},
|
||||||
|
"appearance": {
|
||||||
|
"title": "Apparence",
|
||||||
|
"languageSection": "Langue",
|
||||||
|
"language": "Langue de l'application",
|
||||||
|
"systemLanguage": "Langue par défaut",
|
||||||
|
"languageSubtext": "Langue actuelle : $selectedLanguage"
|
||||||
|
},
|
||||||
|
"licenses": {
|
||||||
|
"title": "Licences Open-Source",
|
||||||
|
"licensedUnder": "Sous licence $license"
|
||||||
|
},
|
||||||
|
"conversation": {
|
||||||
|
"title": "Conversation",
|
||||||
|
"appearance": "Apparence",
|
||||||
|
"selectBackgroundImage": "Sélectionner l'image d'arrière-plan",
|
||||||
|
"removeBackgroundImage": "Supprimer l'image d'arrière-plan",
|
||||||
|
"removeBackgroundImageConfirmTitle": "Suppression de l'image d'arrière-plan",
|
||||||
|
"removeBackgroundImageConfirmBody": "Confirmer la suppression de l'image d'arrière-plan de conversation ?",
|
||||||
|
"newChatsSection": "Nouvelles conversations",
|
||||||
|
"newChatsMuteByDefault": "Mettre les nouvelles conversation en sourdine par défaut",
|
||||||
|
"behaviourSection": "Comportement",
|
||||||
|
"contactsIntegration": "Intégration de contacts",
|
||||||
|
"selectBackgroundImageDescription": "Cette image sera en arrière-plan de toutes vos conversations",
|
||||||
|
"newChatsE2EE": "Activer le chiffrement de bout en bout par défaut. ATTENTION : Expérimental",
|
||||||
|
"contactsIntegrationBody": "Lorsque activé, les données de votre appareil fourniront les titres de conversation, et images de profils. Aucune donnée n'est envoyée au serveur."
|
||||||
|
},
|
||||||
|
"debugging": {
|
||||||
|
"title": "Option de développement",
|
||||||
|
"generalSection": "Général",
|
||||||
|
"generalEnableDebugging": "Activer le mode développement",
|
||||||
|
"generalEncryptionPassword": "Mot de passe de chiffrement",
|
||||||
|
"generalEncryptionPasswordSubtext": "Les journaux contiennent des informations sensibles, choisissez un mot de passe fort",
|
||||||
|
"generalLoggingIp": "IP de journaux",
|
||||||
|
"generalLoggingIpSubtext": "L'IP à laquelle envoyer les journaux",
|
||||||
|
"generalLoggingPort": "Port de journaux",
|
||||||
|
"generalLoggingPortSubtext": "L'IP où envoyer les journaux"
|
||||||
|
},
|
||||||
|
"network": {
|
||||||
|
"title": "Réseau",
|
||||||
|
"automaticDownloadsSection": "Téléchargements automatiques",
|
||||||
|
"automaticDownloadsText": "Moxxy téléchargera automatiquement les fichiers sur…",
|
||||||
|
"automaticDownloadsMaximumSize": "Taille maximale de téléchargement",
|
||||||
|
"automaticDownloadAlways": "Toujours",
|
||||||
|
"wifi": "WiFi",
|
||||||
|
"mobileData": "Données mobiles",
|
||||||
|
"automaticDownloadsMaximumSizeSubtext": "La taille maximale des fichiers à télécharger automatiquement"
|
||||||
|
},
|
||||||
|
"privacy": {
|
||||||
|
"title": "Vie privée",
|
||||||
|
"generalSection": "Général",
|
||||||
|
"showContactRequests": "Afficher les requêtes de contact",
|
||||||
|
"showContactRequestsSubtext": "Cela affiche les personnes qui vous ont ajouté à leurs listes de contacts mais ne vous ont pas encore envoyé de message",
|
||||||
|
"profilePictureVisibility": "Rendre publique l'image de profil",
|
||||||
|
"profilePictureVisibilitSubtext": "Lorsque activée, tout le monde peut voir votre image de profil. Désactivée, seulement vos contacts peuvent la voir.",
|
||||||
|
"conversationsSection": "Conversation",
|
||||||
|
"sendChatMarkers": "Envoyer des marqueurs de conversation",
|
||||||
|
"sendChatStates": "Envoyer l'état de conversation",
|
||||||
|
"sendChatStatesSubtext": "Informe votre destinataire si vous être en train d'écrire, ou lire la conversation",
|
||||||
|
"sendChatMarkersSubtext": "Informe votre destinataire lorsqu'un message est reçu, ou lu",
|
||||||
|
"redirectsSection": "Redirections",
|
||||||
|
"redirectText": "Redirige les liens $serviceName vers un service de proxy, comme $exampleProxy",
|
||||||
|
"currentlySelected": "Service sélectionné : $proxy",
|
||||||
|
"redirectsTitle": "$serviceName Redirection",
|
||||||
|
"cannotEnableRedirect": "Impossible d'activer les redirections $serviceName",
|
||||||
|
"urlEmpty": "URL vide",
|
||||||
|
"urlInvalid": "URL non-valide",
|
||||||
|
"redirectDialogTitle": "$serviceName Redirect",
|
||||||
|
"stickersPrivacy": "Rendre publique la liste d'autoocollants",
|
||||||
|
"stickersPrivacySubtext": "Si activée, tout le monde pourra voir vos autocollants installés.",
|
||||||
|
"cannotEnableRedirectSubtext": "Service de redirection manquant, appuyer sur le champ à côté de l'interrupteur"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"conversation": {
|
||||||
|
"stickerSettings": "Paramètres des autocollants",
|
||||||
|
"unencrypted": "Non-chiffré",
|
||||||
|
"encrypted": "Chiffré",
|
||||||
|
"closeChat": "Fermer la conversation",
|
||||||
|
"closeChatConfirmTitle": "Fermeture de conversation",
|
||||||
|
"blockShort": "Bloquer",
|
||||||
|
"blockUser": "Bloquer cette personne",
|
||||||
|
"online": "En ligne",
|
||||||
|
"retract": "Supprimer le message",
|
||||||
|
"forward": "Transférer",
|
||||||
|
"edit": "Modifier",
|
||||||
|
"quote": "Citer",
|
||||||
|
"copy": "Copier le contenu",
|
||||||
|
"messageCopied": "Message copié vers le presse-papier",
|
||||||
|
"addReaction": "Ajouter une réaction",
|
||||||
|
"showError": "Afficher l'erreur",
|
||||||
|
"showWarning": "Afficher l'avertissement",
|
||||||
|
"warning": "Avertissement",
|
||||||
|
"addToContacts": "Ajouter aux contacts",
|
||||||
|
"addToContactsBody": "Confirmer l'ajout de ${jid} aux contacts ?",
|
||||||
|
"stickerPickerNoStickersLine1": "Pas d'autocollants installés.",
|
||||||
|
"newDeviceMessage": {
|
||||||
|
"one": "Un nouvel appareil a été ajouté",
|
||||||
|
"other": "De nouveaux appareils ont été ajoutés"
|
||||||
|
},
|
||||||
|
"replacedDeviceMessage": {
|
||||||
|
"one": "Un appareil a été modifié",
|
||||||
|
"other": "Plusieurs appareils ont été ajoutés"
|
||||||
|
},
|
||||||
|
"sendImages": "Envoyer des images",
|
||||||
|
"sendFiles": "Envoyer des fichiers",
|
||||||
|
"takePhotos": "Prendre des photos",
|
||||||
|
"messageHint": "Envoyer un message…",
|
||||||
|
"closeChatConfirmSubtext": "Confirmer la fermeture de cette conversation ?",
|
||||||
|
"retractBody": "Confirmer la demande de suppression du message ? Cela n'est qu'une demande que le client n'a pas a honorer.",
|
||||||
|
"addToContactsTitle": "Ajouter ${jid} aux contacts",
|
||||||
|
"stickerPickerNoStickersLine2": "Des autocollants sont disponibles à l'installation dans les paramètres."
|
||||||
|
},
|
||||||
|
"intro": {
|
||||||
|
"noAccount": "Pas de compte XMPP ? Aucun soucis, en créer un est très simple.",
|
||||||
|
"loginButton": "Connexion",
|
||||||
|
"registerButton": "Inscription"
|
||||||
|
},
|
||||||
|
"login": {
|
||||||
|
"title": "Connexion",
|
||||||
|
"xmppAddress": "Adresse XMPP",
|
||||||
|
"password": "Mot de passe",
|
||||||
|
"advancedOptions": "Paramètres avancés",
|
||||||
|
"createAccount": "Créer un compte sur le serveur"
|
||||||
|
},
|
||||||
|
"conversations": {
|
||||||
|
"speeddialNewChat": "Nouvelle conversation",
|
||||||
|
"speeddialJoinGroupchat": "Rejoindre une conversation de groupe",
|
||||||
|
"speeddialAddNoteToSelf": "Note personnelle",
|
||||||
|
"overlaySettings": "Paramètres",
|
||||||
|
"noOpenChats": "Aucune conversation d'ouverte",
|
||||||
|
"startChat": "Démarrer une conversation",
|
||||||
|
"closeChatBody": "Souhaitez-vous fermer la conversation avec ${conversationTitle} ?",
|
||||||
|
"markAsRead": "Marquer comme lue",
|
||||||
|
"closeChat": "Fermer une conversation"
|
||||||
|
},
|
||||||
|
"startchat": {
|
||||||
|
"title": "Nouvelle conversation",
|
||||||
|
"xmppAddress": "Adresse XMPP",
|
||||||
|
"buttonAddToContact": "Démarrer une nouvelle conversation",
|
||||||
|
"subtitle": "Vous pouvez démarrer une conversation à l'aide d'une adresse XMPP ou en scannant un code QR."
|
||||||
|
},
|
||||||
|
"newconversation": {
|
||||||
|
"title": "Nouvelle conversation",
|
||||||
|
"startChat": "Démarrer une nouvelle conversation",
|
||||||
|
"createGroupchat": "Nouvelle conversation de groupe",
|
||||||
|
"joinGroupChat": "Rejoindre une conversation de groupe",
|
||||||
|
"nullNickname": "Le pseudonyme ne peut pas être vide !",
|
||||||
|
"nick": "Pseudonyme",
|
||||||
|
"enterNickname": "Entrer un pseudonyme",
|
||||||
|
"nicknameSubtitle": "Un pseudonyme unique est requis pour rejoindre une CUM."
|
||||||
|
},
|
||||||
|
"crop": {
|
||||||
|
"setProfilePicture": "Utiliser comme image de profil"
|
||||||
|
},
|
||||||
|
"shareselection": {
|
||||||
|
"shareWith": "Partager avec…",
|
||||||
|
"confirmTitle": "Envoyer le fichier",
|
||||||
|
"confirmBody": "Une ou plusieurs conversations ne sont pas chiffrées. Le fichier sera accessible au serveur. Confirmer ?"
|
||||||
|
},
|
||||||
|
"profile": {
|
||||||
|
"general": {
|
||||||
|
"omemo": "Sécurité",
|
||||||
|
"profile": "Profil",
|
||||||
|
"media": "Média"
|
||||||
|
},
|
||||||
|
"conversation": {
|
||||||
|
"notifications": "Notifications",
|
||||||
|
"notificationsMuted": "En sourdine",
|
||||||
|
"notificationsEnabled": "Activées",
|
||||||
|
"sharedMedia": "Média"
|
||||||
|
},
|
||||||
|
"owndevices": {
|
||||||
|
"title": "Appareils disponibles",
|
||||||
|
"otherDevices": "Autres appareils",
|
||||||
|
"thisDevice": "Cet appareil",
|
||||||
|
"deleteDeviceConfirmTitle": "Supprimer appareil",
|
||||||
|
"recreateOwnSessions": "Recréer sessions",
|
||||||
|
"recreateOwnSessionsConfirmTitle": "Recréer ses propres sessions ?",
|
||||||
|
"recreateOwnDevice": "Recréer appareil",
|
||||||
|
"recreateOwnDeviceConfirmTitle": "Récréer cet appareil ?",
|
||||||
|
"deleteDeviceConfirmBody": "Vos contact ne pourront pas chiffrer pour cet appareil, continuer ?",
|
||||||
|
"recreateOwnSessionsConfirmBody": "Cela recréera les sessions cryptographiques de vos appareils. À n'utiliser qu'en cas d'erreur de déchiffrement.",
|
||||||
|
"recreateOwnDeviceConfirmBody": "Cela récréera l'identité cryptographique de cet appareil. Cette opération peut prendre du temps. Les contact ayant vérifié cet appareils auront à le faire de nouveau, continuer ?"
|
||||||
|
},
|
||||||
|
"devices": {
|
||||||
|
"title": "Sécurité",
|
||||||
|
"recreateSessions": "Recréer sessions",
|
||||||
|
"recreateSessionsConfirmTitle": "Recréer les sessions ?",
|
||||||
|
"noSessions": "Il n'y a pas de sessions cryptographiques utilisées pour du chiffrement de bout en bout.",
|
||||||
|
"recreateSessionsConfirmBody": "Cela recréera les sessions cryptographiques de vos appareils. À n'utiliser qu'en cas d'erreur de déchiffrement sur ceux-ci."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"blocklist": {
|
||||||
|
"title": "Liste de blocage",
|
||||||
|
"noUsersBlocked": "Aucune personne bloquée",
|
||||||
|
"unblockAll": "Tout débloquer",
|
||||||
|
"unblockAllConfirmTitle": "Confirmer ?",
|
||||||
|
"unblockAllConfirmBody": "Confirmer le débloquage de toutes les personnes ?",
|
||||||
|
"unblockJidConfirmTitle": "Débloquer ${jid} ?",
|
||||||
|
"unblockJidConfirmBody": "Confirmer le débloquage de ${jid} ? Vous recevrez de nouveau les messages de sa part."
|
||||||
|
},
|
||||||
|
"cropbackground": {
|
||||||
|
"blur": "Flouter l'arrière-plan",
|
||||||
|
"setAsBackground": "Utiliser comme arrière-plan"
|
||||||
|
},
|
||||||
|
"stickerPack": {
|
||||||
|
"removeConfirmTitle": "Supprimer autocollant",
|
||||||
|
"removeConfirmBody": "Confirmer la suppression de cette série d'autocollants ?",
|
||||||
|
"installConfirmTitle": "Installer autocollant",
|
||||||
|
"restricted": "Cette série est limitée. Les autocollants s'afficheront mais ne pourront être envoyés.",
|
||||||
|
"fetchingFailure": "Autocollants introuvables",
|
||||||
|
"installConfirmBody": "Confirmer l'installation de cette série d'autocollants ?"
|
||||||
|
},
|
||||||
|
"sharedMedia": {
|
||||||
|
"empty": {
|
||||||
|
"chat": "Pas de média partagé dans cette conversation",
|
||||||
|
"general": "Pas de fichier média disponible"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"warnings": {
|
||||||
|
"conversation": {
|
||||||
|
"holdForLonger": "Maintenez le bouton pressé pour enregistrer un message vocal"
|
||||||
|
},
|
||||||
|
"message": {
|
||||||
|
"integrityCheckFailed": "Impossible de vérifier l'intégrité du fichier"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
443
assets/i18n/strings_gl.i18n.json
Normal file
443
assets/i18n/strings_gl.i18n.json
Normal file
@@ -0,0 +1,443 @@
|
|||||||
|
{
|
||||||
|
"dateTime": {
|
||||||
|
"justNow": "Neste momento",
|
||||||
|
"nMinutesAgo": "fai ${min}min",
|
||||||
|
"mondayAbbrev": "Lun",
|
||||||
|
"tuesdayAbbrev": "Mar",
|
||||||
|
"wednessdayAbbrev": "Mér",
|
||||||
|
"thursdayAbbrev": "Xov",
|
||||||
|
"fridayAbbrev": "Ven",
|
||||||
|
"saturdayAbbrev": "Sáb",
|
||||||
|
"sundayAbbrev": "Dom",
|
||||||
|
"january": "Xaneiro",
|
||||||
|
"february": "Febreiro",
|
||||||
|
"march": "Marzo",
|
||||||
|
"april": "Abril",
|
||||||
|
"may": "Maio",
|
||||||
|
"june": "Xuño",
|
||||||
|
"july": "Xullo",
|
||||||
|
"august": "Agosto",
|
||||||
|
"september": "Setembro",
|
||||||
|
"october": "Outubro",
|
||||||
|
"november": "Novembro",
|
||||||
|
"december": "Decembro",
|
||||||
|
"today": "Hoxe",
|
||||||
|
"yesterday": "Onte"
|
||||||
|
},
|
||||||
|
"messages": {
|
||||||
|
"image": "Imaxe",
|
||||||
|
"video": "Vídeo",
|
||||||
|
"audio": "Audio",
|
||||||
|
"file": "Ficheiro",
|
||||||
|
"sticker": "Adhesivo",
|
||||||
|
"retracted": "A mensaxe foi editada",
|
||||||
|
"you": "Ti",
|
||||||
|
"retractedFallback": "Unha mensaxe anterior foi editada pero o teu cliente non soporta esa función"
|
||||||
|
},
|
||||||
|
"errors": {
|
||||||
|
"general": {
|
||||||
|
"noInternet": "Sen conexión a Internet."
|
||||||
|
},
|
||||||
|
"filePicker": {
|
||||||
|
"permissionDenied": "Non se concedeu permiso de acceso á almacenaxe."
|
||||||
|
},
|
||||||
|
"omemo": {
|
||||||
|
"couldNotPublish": "Non se puido publicar a identidade criptográfica no servidor. Debido a isto a cifraxe de extremo-a-extremo podería non funcionar.",
|
||||||
|
"invalidHmac": "Non se descifrou a mensaxe",
|
||||||
|
"noDecryptionKey": "Non se dispón de chave de descifrado",
|
||||||
|
"messageInvalidAfixElement": "Mensaxe cifrada non válida",
|
||||||
|
"verificationInvalidOmemoUrl": "Impresión dixital OMEMO:2 non válida",
|
||||||
|
"verificationWrongJid": "Enderezo XMPP incorrecto",
|
||||||
|
"verificationWrongDevice": "Dispositivo OMEMO:2 incorrecto",
|
||||||
|
"verificationNotInList": "Dispositivo OMEMO:2 incorrecto",
|
||||||
|
"verificationWrongFingerprint": "Impresión dixital OMEMO:2 incorrecta",
|
||||||
|
"notEncryptedForDevice": "Non se cifrou a mensaxe para este dispositivo"
|
||||||
|
},
|
||||||
|
"connection": {
|
||||||
|
"connectionTimeout": "Non hai conexión co servidor",
|
||||||
|
"saslAccountDisabled": "A conta está desactivada",
|
||||||
|
"saslInvalidCredentials": "As credenciais da conta non son válidas",
|
||||||
|
"unrecoverable": "Perdeuse a conexión debido a un erro irremediable"
|
||||||
|
},
|
||||||
|
"login": {
|
||||||
|
"saslFailed": "Credenciais de acceso incorrectas",
|
||||||
|
"startTlsFailed": "Non se puido establecer unha conexión segura",
|
||||||
|
"noConnection": "Fallou o establecemento da conexión",
|
||||||
|
"unspecified": "Erro inconcreto"
|
||||||
|
},
|
||||||
|
"message": {
|
||||||
|
"unspecified": "Erro descoñecido",
|
||||||
|
"fileUploadFailed": "Fallou a subida do ficheiro",
|
||||||
|
"contactDoesntSupportOmemo": "O contacto non ten soporte para a cifraxe usando OMEMO:2",
|
||||||
|
"fileDownloadFailed": "Fallou a descarga do ficheiro",
|
||||||
|
"serviceUnavailable": "Non se puido entregar a mensaxe ao contacto",
|
||||||
|
"remoteServerTimeout": "A mensaxe non se puido entregar ao servidor do contacto",
|
||||||
|
"remoteServerNotFound": "A mensaxe non se puido entregar ao servidor do contacto e non sabemos onde está",
|
||||||
|
"failedToEncrypt": "Non se puido cifrar a mensaxe",
|
||||||
|
"failedToEncryptFile": "Non se puido cifrar o ficheiro",
|
||||||
|
"failedToDecryptFile": "Non se puido descifrar o ficheiro",
|
||||||
|
"fileNotEncrypted": "A conversa está cifrada pero o ficheiro non está cifrado"
|
||||||
|
},
|
||||||
|
"conversation": {
|
||||||
|
"audioRecordingError": "Fallou a finalización da gravación de audio",
|
||||||
|
"openFileNoAppError": "Non se atopa unha app para abrir o ficheiro",
|
||||||
|
"openFileGenericError": "Non se puido abrir o ficheiro",
|
||||||
|
"messageErrorDialogTitle": "Erro"
|
||||||
|
},
|
||||||
|
"newChat": {
|
||||||
|
"remoteServerError": "Non se puido contactar co servidor remoto.",
|
||||||
|
"groupchatUnsupported": "Por agora non hai soporte para unirse a conversas en grupo.",
|
||||||
|
"unknown": "Erro descoñecido."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"warnings": {
|
||||||
|
"message": {
|
||||||
|
"integrityCheckFailed": "Non se puido verificar a integridade do ficheiro"
|
||||||
|
},
|
||||||
|
"conversation": {
|
||||||
|
"holdForLonger": "Mantén premido o botón para gravar a mensaxe de voz"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"pages": {
|
||||||
|
"intro": {
|
||||||
|
"noAccount": "Non tes unha conta XMPP? Sen problema, crear unha é doado.",
|
||||||
|
"loginButton": "Acceder",
|
||||||
|
"registerButton": "Crear conta"
|
||||||
|
},
|
||||||
|
"login": {
|
||||||
|
"title": "Acceder",
|
||||||
|
"xmppAddress": "Enderezo-XMPP",
|
||||||
|
"password": "Contrasinal",
|
||||||
|
"advancedOptions": "Opcións avanzadas",
|
||||||
|
"createAccount": "Crear conta no servidor"
|
||||||
|
},
|
||||||
|
"conversations": {
|
||||||
|
"speeddialNewChat": "Nova conversa",
|
||||||
|
"speeddialJoinGroupchat": "Entrar na conversa en grupo",
|
||||||
|
"speeddialAddNoteToSelf": "Notas propias",
|
||||||
|
"overlaySettings": "Axustes",
|
||||||
|
"noOpenChats": "Non tes conversas abertas",
|
||||||
|
"startChat": "Inicia unha conversa",
|
||||||
|
"closeChat": "Pecha a conversa",
|
||||||
|
"markAsRead": "Marcar como lido",
|
||||||
|
"closeChatBody": "Tes a certeza de querer pechar a conversa con ${conversationTitle}?"
|
||||||
|
},
|
||||||
|
"conversation": {
|
||||||
|
"unencrypted": "Sen cifrar",
|
||||||
|
"encrypted": "Cifrada",
|
||||||
|
"closeChat": "Pechar conversa",
|
||||||
|
"closeChatConfirmTitle": "Pechar conversa",
|
||||||
|
"closeChatConfirmSubtext": "Tes a certeza de querer pechar esta conversa?",
|
||||||
|
"blockShort": "Bloquear",
|
||||||
|
"blockUser": "Bloquear conta",
|
||||||
|
"online": "En liña",
|
||||||
|
"retract": "Editar mensaxe",
|
||||||
|
"retractBody": "Tes a certeza de querer editar a mensaxe? Ten en conta que esta só é unha solicitude e que o cliente non ten porque facerlle caso.",
|
||||||
|
"forward": "Reenviar",
|
||||||
|
"edit": "Editar",
|
||||||
|
"quote": "Citar",
|
||||||
|
"showWarning": "Mostrar aviso",
|
||||||
|
"warning": "Aviso",
|
||||||
|
"addToContacts": "Engadir aos contactos",
|
||||||
|
"addToContactsTitle": "Engadir a ${jid} aos contactos",
|
||||||
|
"addToContactsBody": "Tes a certeza de querer engadir a ${jid} aos teus contactos?",
|
||||||
|
"stickerPickerNoStickersLine1": "Non tes paquetes de adhesivos instalados.",
|
||||||
|
"stickerPickerNoStickersLine2": "Pódelos instalar a través dos axustes dos adhesivos.",
|
||||||
|
"stickerSettings": "Axustes dos adhesivos",
|
||||||
|
"newDeviceMessage": {
|
||||||
|
"one": "Engadiuse un novo dispositivo",
|
||||||
|
"other": "Engadíronse varios dispositivos novos"
|
||||||
|
},
|
||||||
|
"replacedDeviceMessage": {
|
||||||
|
"one": "Un dos dispositivos cambiou",
|
||||||
|
"other": "Engadíronse varios dispositivos"
|
||||||
|
},
|
||||||
|
"messageHint": "Enviar unha mensaxe…",
|
||||||
|
"sendImages": "Enviar imaxes",
|
||||||
|
"sendFiles": "Enviar ficheiros",
|
||||||
|
"takePhotos": "Facer fotos",
|
||||||
|
"copy": "Copiar contido",
|
||||||
|
"messageCopied": "Copiouse o contido da mensaxe no portapapeis",
|
||||||
|
"addReaction": "Engadir reacción",
|
||||||
|
"showError": "Mostar erro"
|
||||||
|
},
|
||||||
|
"startchat": {
|
||||||
|
"title": "Nova Conversa",
|
||||||
|
"xmppAddress": "Enderezo XMPP",
|
||||||
|
"subtitle": "Podes iniciar unha nova conversa escribindo un enderezo XMPP ou escaneando o seu código QR.",
|
||||||
|
"buttonAddToContact": "Iniciar nova conversa"
|
||||||
|
},
|
||||||
|
"newconversation": {
|
||||||
|
"title": "Nova conversa",
|
||||||
|
"startChat": "Iniciar nova conversa",
|
||||||
|
"createGroupchat": "Nova conversa en grupo",
|
||||||
|
"nullNickname": "O alcume non pode estar baleiro!",
|
||||||
|
"nick": "Alcume",
|
||||||
|
"nicknameSubtitle": "Tes que escribir un alcume único para poder unirte á MUC.",
|
||||||
|
"joinGroupChat": "Unirse a conversa en grupo",
|
||||||
|
"enterNickname": "Escribe un alcume"
|
||||||
|
},
|
||||||
|
"crop": {
|
||||||
|
"setProfilePicture": "Establecer imaxe de perfil"
|
||||||
|
},
|
||||||
|
"shareselection": {
|
||||||
|
"shareWith": "Compartir con…",
|
||||||
|
"confirmTitle": "Enviar ficheiro",
|
||||||
|
"confirmBody": "Unha ou varias conversas non están cifradas. Isto significa que o ficheiro podería ser visto no servidor. Desexas continuar?"
|
||||||
|
},
|
||||||
|
"profile": {
|
||||||
|
"general": {
|
||||||
|
"omemo": "Seguridade",
|
||||||
|
"profile": "Perfil",
|
||||||
|
"media": "Multimedia"
|
||||||
|
},
|
||||||
|
"conversation": {
|
||||||
|
"notifications": "Notificacións",
|
||||||
|
"notificationsMuted": "Acalada",
|
||||||
|
"notificationsEnabled": "Activada",
|
||||||
|
"sharedMedia": "Multimedia"
|
||||||
|
},
|
||||||
|
"owndevices": {
|
||||||
|
"title": "Dispositivos propios",
|
||||||
|
"thisDevice": "Este dispositivo",
|
||||||
|
"otherDevices": "Outros dispositivos",
|
||||||
|
"deleteDeviceConfirmTitle": "Eliminar dispositivo",
|
||||||
|
"deleteDeviceConfirmBody": "Isto significa que os contactos non poderán cifrar as mensaxes para ese dispositivo. Continuar?",
|
||||||
|
"recreateOwnSessions": "Reconstruír sesións",
|
||||||
|
"recreateOwnSessionsConfirmTitle": "Recrear as sesións propias?",
|
||||||
|
"recreateOwnSessionsConfirmBody": "Isto volverá a crear as sesións criptográficas dos teus dispositivos. Fai isto só se os teus dispositivos mostran erros ao descrifrar.",
|
||||||
|
"recreateOwnDevice": "Recrear dispositivo",
|
||||||
|
"recreateOwnDeviceConfirmTitle": "Recrear o dispositivo propio?",
|
||||||
|
"recreateOwnDeviceConfirmBody": "Isto recreará a identidade criptográfica deste dispositivo? Podería levarlle un anaco. Se os contactos verificaron este dispositivo, terán que facelo outra vez. Continuar?"
|
||||||
|
},
|
||||||
|
"devices": {
|
||||||
|
"title": "Seguridade",
|
||||||
|
"recreateSessions": "Reconstruír sesións",
|
||||||
|
"recreateSessionsConfirmTitle": "Reconstruír sesións?",
|
||||||
|
"recreateSessionsConfirmBody": "Isto volverá a crear as sesións criptográficas dos teus dispositivos. Usa isto só se os teus dispositivos mostran erros ao descifrar.",
|
||||||
|
"noSessions": "Non hai sesións criptográficas en uso para a cifraxe de extremo-a-extremo."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"sharedMedia": {
|
||||||
|
"empty": {
|
||||||
|
"general": "Non hai dispoñibles ficheiros multimedia",
|
||||||
|
"chat": "Non hai multimedia compartido nesta conversa"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"settings": {
|
||||||
|
"about": {
|
||||||
|
"debugMenuAlreadyShown": "Xa es desenvolvedora!",
|
||||||
|
"title": "Acerca de",
|
||||||
|
"licensed": "Baixo licenza GPL3",
|
||||||
|
"version": "Versión ${version}",
|
||||||
|
"viewSourceCode": "Ver código fonte",
|
||||||
|
"nMoreToGo": "${n} máis para rematar…",
|
||||||
|
"debugMenuShown": "Agora xa es desenvolvedora!"
|
||||||
|
},
|
||||||
|
"settings": {
|
||||||
|
"title": "Axustes",
|
||||||
|
"conversationsSection": "Conversas",
|
||||||
|
"accountSection": "Conta",
|
||||||
|
"signOut": "Pechar sesión",
|
||||||
|
"signOutConfirmTitle": "Pechar Sesión",
|
||||||
|
"signOutConfirmBody": "Vas pechar a sesión. Continuar?",
|
||||||
|
"miscellaneousSection": "Varios",
|
||||||
|
"debuggingSection": "Depuración",
|
||||||
|
"general": "Xeral"
|
||||||
|
},
|
||||||
|
"appearance": {
|
||||||
|
"title": "Aparencia",
|
||||||
|
"languageSection": "Idioma",
|
||||||
|
"language": "Idioma da app",
|
||||||
|
"languageSubtext": "Idioma actual: $selectedLanguage",
|
||||||
|
"systemLanguage": "Idioma por defecto"
|
||||||
|
},
|
||||||
|
"licenses": {
|
||||||
|
"title": "Licenzas Open-Source",
|
||||||
|
"licensedUnder": "Baixo licenza $license"
|
||||||
|
},
|
||||||
|
"conversation": {
|
||||||
|
"title": "Conversa",
|
||||||
|
"appearance": "Aparencia",
|
||||||
|
"selectBackgroundImage": "Elexir imaxe de fondo",
|
||||||
|
"selectBackgroundImageDescription": "Esta será a imaxe de fondo en todas as túas conversas",
|
||||||
|
"removeBackgroundImage": "Retirar imaxe de fondo",
|
||||||
|
"removeBackgroundImageConfirmTitle": "Retirar imaxe de fondo",
|
||||||
|
"removeBackgroundImageConfirmBody": "Tes a certeza de querer eliminar a imaxe de fondo da conversa?",
|
||||||
|
"newChatsSection": "Novas Conversas",
|
||||||
|
"newChatsMuteByDefault": "Por defecto acalar as novas conversas",
|
||||||
|
"newChatsE2EE": "Por defecto activar a cifraxe de extremo-a-extremo. AVISO: Experimental",
|
||||||
|
"behaviourSection": "Comportamento",
|
||||||
|
"contactsIntegration": "Integración dos contactos",
|
||||||
|
"contactsIntegrationBody": "Ao activala, os datos da libreta de enderezos usaranse para proporcionar título ás conversas e imaxes de perfil. Non se transmite información ao servidor."
|
||||||
|
},
|
||||||
|
"debugging": {
|
||||||
|
"title": "Opción de depuración",
|
||||||
|
"generalSection": "Xeral",
|
||||||
|
"generalEnableDebugging": "Activar depuración",
|
||||||
|
"generalEncryptionPassword": "Contrasinal de cifraxe",
|
||||||
|
"generalLoggingIp": "IP de rexistro",
|
||||||
|
"generalLoggingIpSubtext": "O IP ao que se deben enviar os rexistros",
|
||||||
|
"generalLoggingPort": "Porto de rexistro",
|
||||||
|
"generalLoggingPortSubtext": "O IP ao que se deben enviar os rexistros",
|
||||||
|
"generalEncryptionPasswordSubtext": "Os rexistros poderían conter información sensible así que mellor elixe un contrasinal forte"
|
||||||
|
},
|
||||||
|
"network": {
|
||||||
|
"title": "Rede",
|
||||||
|
"automaticDownloadsSection": "Descargas automáticas",
|
||||||
|
"automaticDownloadsText": "Moxxy descargará automáticamente os ficheiros en…",
|
||||||
|
"automaticDownloadsMaximumSize": "Tamaño máximo de descarga",
|
||||||
|
"automaticDownloadsMaximumSizeSubtext": "O tamaño máximo dos ficheiros a descargar de xeito automático",
|
||||||
|
"automaticDownloadAlways": "Sempre",
|
||||||
|
"wifi": "Wifi",
|
||||||
|
"mobileData": "Datos móbiles"
|
||||||
|
},
|
||||||
|
"privacy": {
|
||||||
|
"profilePictureVisibility": "Facer pública a foto de perfil",
|
||||||
|
"profilePictureVisibilitSubtext": "Se o activas, calquera poderá ver a túa foto de perfil. Se o desactivas, só as persoas da túa lista de contactos poderán ver a foto de perfil.",
|
||||||
|
"conversationsSection": "Conversa",
|
||||||
|
"sendChatMarkers": "Enviar marcadores de conversa",
|
||||||
|
"title": "Privacidade",
|
||||||
|
"generalSection": "Xeral",
|
||||||
|
"sendChatMarkersSubtext": "Vai indicar aos teus correspondentes se recibiches ou liches a mensaxe",
|
||||||
|
"showContactRequests": "Mostrar solicitudes de contacto",
|
||||||
|
"showContactRequestsSubtext": "Mostrarache as persoas que te engadiron á súa lista de contactos pero que aínda non che enviaron mensaxes",
|
||||||
|
"sendChatStates": "Enviar estados da conversa",
|
||||||
|
"sendChatStatesSubtext": "Vai indicar aos teus correspondentes se estás escribindo ou lendo na conversa",
|
||||||
|
"redirectsSection": "Reenvíos",
|
||||||
|
"redirectsTitle": "Redirixir $serviceName",
|
||||||
|
"cannotEnableRedirect": "Non se pode activar a redirección $serviceName",
|
||||||
|
"cannotEnableRedirectSubtext": "Primeiro tes que establecer o servizo proxy ao que queres redirixir. Para facelo toca no campo a carón do botón.",
|
||||||
|
"urlEmpty": "O URL non pode estar baleiro",
|
||||||
|
"urlInvalid": "URL non válido",
|
||||||
|
"redirectDialogTitle": "Redirixir $serviceName",
|
||||||
|
"stickersPrivacy": "Manter como pública a lista de adhesivos",
|
||||||
|
"stickersPrivacySubtext": "Se o activas, calquera poderá ver a túa lista de paquetes de adhesivos instalados.",
|
||||||
|
"redirectText": "Redireccionará as ligazóns a $serviceName nas que premas cara o servizo proxy, ex. $exampleProxy",
|
||||||
|
"currentlySelected": "Seleccionado actualmente: $proxy"
|
||||||
|
},
|
||||||
|
"stickers": {
|
||||||
|
"title": "Adhesivos",
|
||||||
|
"stickerSection": "Adhesivo",
|
||||||
|
"displayStickers": "Mostar adhesivos na conversa",
|
||||||
|
"autoDownload": "Descargar automaticamente adhesivos",
|
||||||
|
"autoDownloadBody": "Se o activas, descargarás automaticamente os adhesivos cando o remitente estea na túa lista de contactos.",
|
||||||
|
"stickerPacksSection": "Paquetes de adhesivos",
|
||||||
|
"importStickerPack": "Importar paquete de adhesivos",
|
||||||
|
"importSuccess": "Paquete de adhesivos importado correctamente",
|
||||||
|
"importFailure": "Fallou a importación do paquete de adhesivos",
|
||||||
|
"stickerPackSize": "(${size})"
|
||||||
|
},
|
||||||
|
"stickerPacks": {
|
||||||
|
"title": "Paquetes de Adhesivos"
|
||||||
|
},
|
||||||
|
"storage": {
|
||||||
|
"title": "Almacenaxe",
|
||||||
|
"storageUsed": "Almacenaxe utilizada: ${size}",
|
||||||
|
"sizePlaceholder": "Calculando…",
|
||||||
|
"storageManagement": "Xestión da almacenaxe",
|
||||||
|
"removeOldMedia": {
|
||||||
|
"title": "Eliminar multimedia antigo",
|
||||||
|
"description": "Elimina ficheiros multimedia antigos do dispositivo"
|
||||||
|
},
|
||||||
|
"removeOldMediaDialog": {
|
||||||
|
"title": "Eliminar ficheiros multimedia",
|
||||||
|
"options": {
|
||||||
|
"all": "Todos os ficheiros multimedia",
|
||||||
|
"oneWeek": "Anteriores a 1 semana",
|
||||||
|
"oneMonth": "Anteriores a 1 mes"
|
||||||
|
},
|
||||||
|
"delete": "Eliminar",
|
||||||
|
"confirmation": {
|
||||||
|
"body": "Tes a certeza de querer eliminar os ficheiros multimedia antigos?"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"viewMediaFiles": "Ver ficheiros multimedia",
|
||||||
|
"mediaFiles": "Ficheiros multimedia",
|
||||||
|
"types": {
|
||||||
|
"media": "Multimedia",
|
||||||
|
"stickers": "Adhesivos"
|
||||||
|
},
|
||||||
|
"manageStickers": "Xestión dos paquetes de adhesivos"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"blocklist": {
|
||||||
|
"title": "Lista de bloqueo",
|
||||||
|
"noUsersBlocked": "Non tes contas bloqueadas",
|
||||||
|
"unblockAll": "Desbloquear todo",
|
||||||
|
"unblockAllConfirmTitle": "Tes certeza?",
|
||||||
|
"unblockAllConfirmBody": "Tes a certeza de querer desbloquear todas as contas?",
|
||||||
|
"unblockJidConfirmTitle": "Desbloquear a ${jid}?",
|
||||||
|
"unblockJidConfirmBody": "Tes a certeza de querer desbloquear a ${jid}? Volverás a recibir mensaxes desde esta conta."
|
||||||
|
},
|
||||||
|
"cropbackground": {
|
||||||
|
"blur": "Desenfocar o fondo",
|
||||||
|
"setAsBackground": "Establecer como imaxe de fondo"
|
||||||
|
},
|
||||||
|
"stickerPack": {
|
||||||
|
"removeConfirmTitle": "Eliminar paquete de adhesivos",
|
||||||
|
"removeConfirmBody": "Tes a certeza de querer eliminar este paquete de adhesivos?",
|
||||||
|
"installConfirmTitle": "Instalar paquete de adhesivos",
|
||||||
|
"installConfirmBody": "Tes a certeza de querer instalar este paquete de adhesivos?",
|
||||||
|
"restricted": "Este paquete de adhesivos ten restricións. Significa que poden ser vistos pero non enviados.",
|
||||||
|
"fetchingFailure": "Non se atopa o paquete de adhesivos"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"global": {
|
||||||
|
"title": "Moxxy",
|
||||||
|
"dialogAccept": "Oká",
|
||||||
|
"dialogCancel": "Cancelar",
|
||||||
|
"yes": "Si",
|
||||||
|
"no": "Non",
|
||||||
|
"moxxySubtitle": "Un experimento para crear un cliente XMPP moderno, bonito e doado de usar."
|
||||||
|
},
|
||||||
|
"notifications": {
|
||||||
|
"permanent": {
|
||||||
|
"idle": "Detido",
|
||||||
|
"ready": "Preparado para recibir mensaxes",
|
||||||
|
"connecting": "Conectando…",
|
||||||
|
"disconnect": "Desconectado",
|
||||||
|
"error": "Erro"
|
||||||
|
},
|
||||||
|
"message": {
|
||||||
|
"reply": "Responder",
|
||||||
|
"markAsRead": "Marcar como lido"
|
||||||
|
},
|
||||||
|
"channels": {
|
||||||
|
"messagesChannelName": "Mensaxes",
|
||||||
|
"warningChannelName": "Avisos",
|
||||||
|
"warningChannelDescription": "Avisos en relación a Moxxy",
|
||||||
|
"messagesChannelDescription": "A canle de notificación para as mensaxes recibidas",
|
||||||
|
"serviceChannelName": "Servizo en primeiro plano",
|
||||||
|
"serviceChannelDescription": "Mantén activa a notificación do servizo en primeiro plano"
|
||||||
|
},
|
||||||
|
"titles": {
|
||||||
|
"error": "Erro"
|
||||||
|
},
|
||||||
|
"errors": {
|
||||||
|
"messageError": {
|
||||||
|
"title": "Fallou a entrega da mensaxe",
|
||||||
|
"body": "Fallou a entrega da mensaxe a ${conversationTitle}"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"warnings": {
|
||||||
|
"blockingError": {
|
||||||
|
"title": "Erro ao bloquear a usuaria",
|
||||||
|
"body": "Non se puido bloquear a ${jid} porque o teu servidor non ten soporte para bloqueos."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"language": "Galego",
|
||||||
|
"permissions": {
|
||||||
|
"requests": {
|
||||||
|
"batterySaving": {
|
||||||
|
"reason": "Para poder recibir mensaxes cando está en segundo plano, Moxxy ten que evitar que Android force o aforro de batería."
|
||||||
|
},
|
||||||
|
"notification": {
|
||||||
|
"reason": "Para poder informar das mensaxes que chegan, Moxxy precisa permiso para mostrar notificacións."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"allow": "Permitir",
|
||||||
|
"skip": "Evitar"
|
||||||
|
}
|
||||||
|
}
|
||||||
172
assets/i18n/strings_ja.i18n.json
Normal file
172
assets/i18n/strings_ja.i18n.json
Normal file
@@ -0,0 +1,172 @@
|
|||||||
|
{
|
||||||
|
"language": "日本語",
|
||||||
|
"global": {
|
||||||
|
"yes": "はい",
|
||||||
|
"no": "いいえ",
|
||||||
|
"dialogCancel": "キャンセル",
|
||||||
|
"title": "Moxxy",
|
||||||
|
"dialogAccept": "OK"
|
||||||
|
},
|
||||||
|
"dateTime": {
|
||||||
|
"thursdayAbbrev": "木",
|
||||||
|
"fridayAbbrev": "金",
|
||||||
|
"saturdayAbbrev": "土",
|
||||||
|
"january": "1月",
|
||||||
|
"february": "2月",
|
||||||
|
"march": "3月",
|
||||||
|
"may": "5月",
|
||||||
|
"june": "6月",
|
||||||
|
"july": "7月",
|
||||||
|
"september": "9月",
|
||||||
|
"october": "10月",
|
||||||
|
"justNow": "ちょうど今",
|
||||||
|
"nMinutesAgo": "${min}分前",
|
||||||
|
"mondayAbbrev": "月",
|
||||||
|
"tuesdayAbbrev": "火",
|
||||||
|
"wednessdayAbbrev": "水",
|
||||||
|
"sundayAbbrev": "日",
|
||||||
|
"april": "4月",
|
||||||
|
"august": "8月",
|
||||||
|
"november": "11月",
|
||||||
|
"december": "12月",
|
||||||
|
"today": "今日",
|
||||||
|
"yesterday": "昨日"
|
||||||
|
},
|
||||||
|
"messages": {
|
||||||
|
"audio": "音声",
|
||||||
|
"you": "自分",
|
||||||
|
"image": "画像",
|
||||||
|
"video": "ビデオ",
|
||||||
|
"file": "ファイル",
|
||||||
|
"sticker": "スタンプ",
|
||||||
|
"retracted": "メッセージ取り消された",
|
||||||
|
"retractedFallback": "前のメッセージを取り消しましたが、このクライエントはサポートしていません"
|
||||||
|
},
|
||||||
|
"errors": {
|
||||||
|
"connection": {
|
||||||
|
"connectionTimeout": "サーバー接続中にタイムアウトが発生しました",
|
||||||
|
"saslInvalidCredentials": "ユーザー名またはパスワードが無効",
|
||||||
|
"saslAccountDisabled": "あなたのアカウントは停止されています",
|
||||||
|
"unrecoverable": "回復不能のエラーで接続が途絶えました"
|
||||||
|
},
|
||||||
|
"login": {
|
||||||
|
"noConnection": "接続できませんでした",
|
||||||
|
"startTlsFailed": "接続できませんでした",
|
||||||
|
"unspecified": "特定できないエラー",
|
||||||
|
"saslFailed": "無効なログイン証明書です"
|
||||||
|
},
|
||||||
|
"message": {
|
||||||
|
"fileUploadFailed": "アップロードに失敗しました",
|
||||||
|
"fileDownloadFailed": "ダウンロードに失敗しました",
|
||||||
|
"serviceUnavailable": "配信に失敗しました",
|
||||||
|
"unspecified": "不明なエラー",
|
||||||
|
"failedToDecryptFile": "そのファイルは復号化できません",
|
||||||
|
"fileNotEncrypted": "チャットは暗号化されますが、ファイルは暗号化されません"
|
||||||
|
},
|
||||||
|
"omemo": {
|
||||||
|
"notEncryptedForDevice": "このデバイス向けにメッセージは暗号化されませんでした",
|
||||||
|
"couldNotPublish": "サーバに対して暗号化IDを発行できませんでした、端末間暗号通信はできません",
|
||||||
|
"invalidHmac": "メッセージを復号できませんでした",
|
||||||
|
"noDecryptionKey": "復号キーがありません",
|
||||||
|
"messageInvalidAfixElement": "無効な暗号化メッセージです",
|
||||||
|
"verificationInvalidOmemoUrl": "無効な OMEMO:2 フィンガープリントです",
|
||||||
|
"verificationWrongJid": "正しくない XMPP アドレスです",
|
||||||
|
"verificationWrongDevice": "正しくない OMEMO:2 機器です",
|
||||||
|
"verificationNotInList": "正しくない OMEMO:2 機器です",
|
||||||
|
"verificationWrongFingerprint": "正しくない OMEMO:2 フィンガープリントです"
|
||||||
|
},
|
||||||
|
"conversation": {
|
||||||
|
"messageErrorDialogTitle": "エラー",
|
||||||
|
"audioRecordingError": "音声録音の処理に失敗しました",
|
||||||
|
"openFileNoAppError": "このファイルを開くアプリケーションを発見できませんでした",
|
||||||
|
"openFileGenericError": "ファイルを開くのに失敗しました"
|
||||||
|
},
|
||||||
|
"filePicker": {
|
||||||
|
"permissionDenied": "書き込み用メディアへの権限がありません"
|
||||||
|
},
|
||||||
|
"general": {
|
||||||
|
"noInternet": "インターネットに接続されていません"
|
||||||
|
},
|
||||||
|
"newChat": {
|
||||||
|
"remoteServerError": "リモートサーバーへの接続に失敗しました",
|
||||||
|
"groupchatUnsupported": "グループチャットへの参加は現在サポートされていません",
|
||||||
|
"unknown": "不明なエラー"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"warnings": {
|
||||||
|
"conversation": {
|
||||||
|
"holdForLonger": "長押しすると音声記録できます"
|
||||||
|
},
|
||||||
|
"message": {
|
||||||
|
"integrityCheckFailed": "ファイルの整合性を確認できませんでした"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"pages": {
|
||||||
|
"intro": {
|
||||||
|
"loginButton": "ログイン",
|
||||||
|
"registerButton": "新規登録",
|
||||||
|
"noAccount": "XMPPアドレスをお持ちですか?XMPPアドレスの作成は簡単です。"
|
||||||
|
},
|
||||||
|
"login": {
|
||||||
|
"title": "ログイン",
|
||||||
|
"xmppAddress": "XMPPアドレス",
|
||||||
|
"password": "パスワード",
|
||||||
|
"advancedOptions": "詳細設定",
|
||||||
|
"createAccount": "サーバーにアカウントを作成する"
|
||||||
|
},
|
||||||
|
"conversations": {
|
||||||
|
"speeddialJoinGroupchat": "グループチャットに参加",
|
||||||
|
"speeddialAddNoteToSelf": "自分用メモ",
|
||||||
|
"speeddialNewChat": "新しいチャット",
|
||||||
|
"overlaySettings": "設定"
|
||||||
|
},
|
||||||
|
"conversation": {
|
||||||
|
"blockShort": "ブロック",
|
||||||
|
"blockUser": "ユーザをブロック",
|
||||||
|
"retract": "メッセージを取り消す",
|
||||||
|
"retractBody": "本当にメッセージを取り消しますか? これはただのリクエストであり面目や体裁に関わるものではありません",
|
||||||
|
"forward": "送る",
|
||||||
|
"edit": "編集する",
|
||||||
|
"online": "オンライン",
|
||||||
|
"quote": "引用する",
|
||||||
|
"copy": "内容をコピー",
|
||||||
|
"addReaction": "リアクションを追加する",
|
||||||
|
"showError": "エラーを表示",
|
||||||
|
"showWarning": "警告を表示",
|
||||||
|
"warning": "警告",
|
||||||
|
"messageCopied": "メッセージはクリップボードにコピーされました"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"notifications": {
|
||||||
|
"permanent": {
|
||||||
|
"connecting": "接続中…",
|
||||||
|
"error": "エラー",
|
||||||
|
"idle": "待機中",
|
||||||
|
"ready": "メッセージを受信する準備ができました",
|
||||||
|
"disconnect": "切断済み"
|
||||||
|
},
|
||||||
|
"message": {
|
||||||
|
"reply": "返信",
|
||||||
|
"markAsRead": "既読"
|
||||||
|
},
|
||||||
|
"channels": {
|
||||||
|
"warningChannelName": "警告",
|
||||||
|
"messagesChannelName": "メッセージ",
|
||||||
|
"messagesChannelDescription": "受信メッセージのためのお知らせチャンネル",
|
||||||
|
"warningChannelDescription": "Moxxy に関する警告"
|
||||||
|
},
|
||||||
|
"titles": {
|
||||||
|
"error": "エラー"
|
||||||
|
},
|
||||||
|
"errors": {
|
||||||
|
"messageError": {
|
||||||
|
"title": "メッセージの送信に失敗しました",
|
||||||
|
"body": "${conversationTitle} に送信したメッセージは届きませんでした"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"permissions": {
|
||||||
|
"skip": "スキップ",
|
||||||
|
"allow": "許可する"
|
||||||
|
}
|
||||||
|
}
|
||||||
443
assets/i18n/strings_nl.i18n.json
Normal file
443
assets/i18n/strings_nl.i18n.json
Normal file
@@ -0,0 +1,443 @@
|
|||||||
|
{
|
||||||
|
"language": "Nederlands",
|
||||||
|
"global": {
|
||||||
|
"title": "Moxxy",
|
||||||
|
"dialogAccept": "Oké",
|
||||||
|
"dialogCancel": "Annuleren",
|
||||||
|
"yes": "Ja",
|
||||||
|
"no": "Nee",
|
||||||
|
"moxxySubtitle": "Een xmpp-experiment: het bouwen van een moderne, eenvoudige en mooie client."
|
||||||
|
},
|
||||||
|
"notifications": {
|
||||||
|
"permanent": {
|
||||||
|
"idle": "Inactief",
|
||||||
|
"ready": "Klaar om berichten te ontvangen",
|
||||||
|
"connecting": "Bezig met verbinden…",
|
||||||
|
"disconnect": "Verbinding verbroken",
|
||||||
|
"error": "Foutmelding"
|
||||||
|
},
|
||||||
|
"message": {
|
||||||
|
"reply": "Beantwoorden",
|
||||||
|
"markAsRead": "Markeren als gelezen"
|
||||||
|
},
|
||||||
|
"channels": {
|
||||||
|
"messagesChannelName": "Berichten",
|
||||||
|
"warningChannelName": "Waarschuwingen",
|
||||||
|
"warningChannelDescription": "Aan Moxxy gerelateerde waarschuwingen",
|
||||||
|
"messagesChannelDescription": "Het meldingskanaal voor het ontvangen van berichten",
|
||||||
|
"serviceChannelName": "Voorgronddienst",
|
||||||
|
"serviceChannelDescription": "Toont de vaste voorgronddienstmelding"
|
||||||
|
},
|
||||||
|
"titles": {
|
||||||
|
"error": "Foutmelding"
|
||||||
|
},
|
||||||
|
"errors": {
|
||||||
|
"messageError": {
|
||||||
|
"title": "Bericht afleveren mislukt",
|
||||||
|
"body": "Her bericht aan ‘${conversationTitle}’ kan niet worden afgeleverd"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"warnings": {
|
||||||
|
"blockingError": {
|
||||||
|
"title": "Blokkeren mislukt",
|
||||||
|
"body": "De gebruiker ‘${jid}’ kan niet worden geblokkeerd omdat je server dit niet ondersteunt."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"dateTime": {
|
||||||
|
"justNow": "Zojuist",
|
||||||
|
"nMinutesAgo": "${min} min. geleden",
|
||||||
|
"mondayAbbrev": "ma",
|
||||||
|
"tuesdayAbbrev": "di",
|
||||||
|
"wednessdayAbbrev": "woe",
|
||||||
|
"thursdayAbbrev": "do",
|
||||||
|
"fridayAbbrev": "vrij",
|
||||||
|
"saturdayAbbrev": "za",
|
||||||
|
"sundayAbbrev": "zo",
|
||||||
|
"january": "januari",
|
||||||
|
"february": "februari",
|
||||||
|
"march": "maart",
|
||||||
|
"april": "april",
|
||||||
|
"may": "mei",
|
||||||
|
"june": "juni",
|
||||||
|
"july": "juli",
|
||||||
|
"august": "augustus",
|
||||||
|
"september": "september",
|
||||||
|
"october": "oktober",
|
||||||
|
"november": "november",
|
||||||
|
"december": "december",
|
||||||
|
"today": "Vandaag",
|
||||||
|
"yesterday": "Gisteren"
|
||||||
|
},
|
||||||
|
"messages": {
|
||||||
|
"image": "Afbeelding",
|
||||||
|
"video": "Video",
|
||||||
|
"audio": "Audio",
|
||||||
|
"file": "Bestand",
|
||||||
|
"sticker": "Sticker",
|
||||||
|
"retracted": "Dit bericht is herroepen",
|
||||||
|
"retractedFallback": "Er is een eerder bericht herroepen, maar je client heeft hier geen ondersteuning voor",
|
||||||
|
"you": "Ik"
|
||||||
|
},
|
||||||
|
"errors": {
|
||||||
|
"filePicker": {
|
||||||
|
"permissionDenied": "Het opslagrecht is geweigerd."
|
||||||
|
},
|
||||||
|
"omemo": {
|
||||||
|
"notEncryptedForDevice": "Dit bericht is niet versleuteld op dit apparaat",
|
||||||
|
"invalidHmac": "Het bericht kan niet worden ontsleuteld",
|
||||||
|
"noDecryptionKey": "Er is geen sleutel beschikbaar",
|
||||||
|
"messageInvalidAfixElement": "Het bericht is ongeldig versleuteld",
|
||||||
|
"verificationInvalidOmemoUrl": "Ongeldige OMEMO:2-vingerafdruk",
|
||||||
|
"verificationWrongJid": "Ongeldig xmpp-adres",
|
||||||
|
"verificationWrongDevice": "Ongeldig OMEMO:2-apparaat",
|
||||||
|
"verificationNotInList": "Ongeldig OMEMO:2-apparaat",
|
||||||
|
"verificationWrongFingerprint": "Ongeldige OMEMO:2-vingerafdruk",
|
||||||
|
"couldNotPublish": "De versleutelde identiteit kan niet worden gepubliceerd op de server. Hierdoor werkt eind-tot-eindversleuteling mogelijk niet."
|
||||||
|
},
|
||||||
|
"connection": {
|
||||||
|
"saslAccountDisabled": "Je account is uitgeschakeld",
|
||||||
|
"saslInvalidCredentials": "Je inloggegevens zijn ongeldig",
|
||||||
|
"unrecoverable": "De verbinding is verbroken wegens een onoplosbare fout",
|
||||||
|
"connectionTimeout": "Er kan geen verbinding worden gemaakt met de server"
|
||||||
|
},
|
||||||
|
"login": {
|
||||||
|
"saslFailed": "De inloggegevens zijn ongeldig",
|
||||||
|
"noConnection": "Er kan geen verbinding worden opgezet",
|
||||||
|
"unspecified": "Onbekende foutmelding",
|
||||||
|
"startTlsFailed": "Er kan geen beveiligde verbinding worden opgezet"
|
||||||
|
},
|
||||||
|
"message": {
|
||||||
|
"unspecified": "Onbekende foutmelding",
|
||||||
|
"fileUploadFailed": "Het bestand kan niet worden geüpload",
|
||||||
|
"contactDoesntSupportOmemo": "Deze contactpersoon heeft geen ondersteuning voon OMEMO:2-versleuteling",
|
||||||
|
"fileDownloadFailed": "Het bestand kan niet worden opgehaald",
|
||||||
|
"serviceUnavailable": "Het bericht kan niet worden bezorgd",
|
||||||
|
"remoteServerTimeout": "Het bericht kan niet worden verstuurd naar de server",
|
||||||
|
"failedToEncrypt": "Het bericht kan niet worden versleuteld",
|
||||||
|
"failedToEncryptFile": "Het bestand kan niet worden versleuteld",
|
||||||
|
"failedToDecryptFile": "Het bestand kan niet worden ontsleuteld",
|
||||||
|
"fileNotEncrypted": "Het gesprek is versleuteld, maar het bestand niet",
|
||||||
|
"remoteServerNotFound": "Het bericht kan niet worden verstuurd naar de server omdat het niet bestaat"
|
||||||
|
},
|
||||||
|
"conversation": {
|
||||||
|
"audioRecordingError": "De audio-opname kan niet worden afgerond",
|
||||||
|
"openFileGenericError": "Het bestand kan niet worden geopend",
|
||||||
|
"messageErrorDialogTitle": "Foutmelding",
|
||||||
|
"openFileNoAppError": "Er is geen app die dit bestand kan openen"
|
||||||
|
},
|
||||||
|
"general": {
|
||||||
|
"noInternet": "Er is geen internetverbinding."
|
||||||
|
},
|
||||||
|
"newChat": {
|
||||||
|
"groupchatUnsupported": "Deelnemen aan groepsgesprekken wordt momenteel niet ondersteund.",
|
||||||
|
"unknown": "Onbekende foutmelding.",
|
||||||
|
"remoteServerError": "Er kan geen verbinding worden gemaakt met de externe server."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"warnings": {
|
||||||
|
"message": {
|
||||||
|
"integrityCheckFailed": "De bestandsintegriteit kan niet worden vastgesteld"
|
||||||
|
},
|
||||||
|
"conversation": {
|
||||||
|
"holdForLonger": "Houd langer ingedrukt om een spraakbericht op te nemen"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"pages": {
|
||||||
|
"intro": {
|
||||||
|
"loginButton": "Inloggen",
|
||||||
|
"registerButton": "Registreren",
|
||||||
|
"noAccount": "Geen xmpp-account? Geen zorgen: je maakt er in een handomdraai een aan."
|
||||||
|
},
|
||||||
|
"login": {
|
||||||
|
"title": "Inloggen",
|
||||||
|
"xmppAddress": "Xmpp-adres",
|
||||||
|
"password": "Wachtwoord",
|
||||||
|
"advancedOptions": "Geavanceerde opties",
|
||||||
|
"createAccount": "Account aanmaken op server"
|
||||||
|
},
|
||||||
|
"conversations": {
|
||||||
|
"speeddialNewChat": "Nieuw gesprek",
|
||||||
|
"speeddialJoinGroupchat": "Deelnemen aan groepsgesprek",
|
||||||
|
"speeddialAddNoteToSelf": "Zelfmemo",
|
||||||
|
"overlaySettings": "Instellingen",
|
||||||
|
"noOpenChats": "Er zijn geen openstaande gesprekken",
|
||||||
|
"startChat": "Gesprek starten",
|
||||||
|
"closeChat": "Gesprek sluiten",
|
||||||
|
"closeChatBody": "Weet je zeker dat je het gesprek “${conversationTitle}” wilt sluiten?",
|
||||||
|
"markAsRead": "Markeren als gelezen"
|
||||||
|
},
|
||||||
|
"conversation": {
|
||||||
|
"unencrypted": "Onversleuteld",
|
||||||
|
"encrypted": "Versleuteld",
|
||||||
|
"closeChat": "Gesprek sluiten",
|
||||||
|
"closeChatConfirmSubtext": "Weet je zeker dat je dit gesprek wilt sluiten?",
|
||||||
|
"blockShort": "Blokkeren",
|
||||||
|
"blockUser": "Gebruiker blokkeren",
|
||||||
|
"online": "Online",
|
||||||
|
"retract": "Bericht herroepen",
|
||||||
|
"forward": "Doorsturen",
|
||||||
|
"edit": "Bewerken",
|
||||||
|
"quote": "Citeren",
|
||||||
|
"copy": "Inhoud kopiëren",
|
||||||
|
"addReaction": "Reageren",
|
||||||
|
"showError": "Foutmelding tonen",
|
||||||
|
"showWarning": "Waarschuwing tonen",
|
||||||
|
"addToContacts": "Toevoegen aan contactpersonen",
|
||||||
|
"addToContactsTitle": "${jid} toevoegen aan contactpersonen",
|
||||||
|
"addToContactsBody": "Weet je zeker dat je ${jid} wilt toevoegen aan je contactpersonen?",
|
||||||
|
"stickerPickerNoStickersLine1": "Er zijn geen stickerpakketten beschikbaar.",
|
||||||
|
"stickerSettings": "Stickerinstellingen",
|
||||||
|
"newDeviceMessage": {
|
||||||
|
"one": "Er is een nieuw apparaat toegevoegd",
|
||||||
|
"other": "Er zijn meerdere nieuwe apparaten toegevoegd"
|
||||||
|
},
|
||||||
|
"replacedDeviceMessage": {
|
||||||
|
"one": "Er is een apparaat gewijzigd",
|
||||||
|
"other": "Er zijn meerdere apparaten toegevoegd"
|
||||||
|
},
|
||||||
|
"messageHint": "Verstuur een bericht…",
|
||||||
|
"sendImages": "Afbeeldingen versturen",
|
||||||
|
"sendFiles": "Bestanden versturen",
|
||||||
|
"closeChatConfirmTitle": "Gesprek sluiten",
|
||||||
|
"retractBody": "Weet je zeker dat je dit bericht wilt herroepen? Dit is slechts een verzoek aan de client dat niet in acht hoeft te worden genomen.",
|
||||||
|
"stickerPickerNoStickersLine2": "Installeer pakketten via de stickerinstellingen.",
|
||||||
|
"takePhotos": "Foto's maken",
|
||||||
|
"warning": "Waarschuwing",
|
||||||
|
"messageCopied": "De berichtinhoud is gekopieerd naar het klembord"
|
||||||
|
},
|
||||||
|
"newconversation": {
|
||||||
|
"title": "Nieuw gesprek",
|
||||||
|
"startChat": "Gesprek starten",
|
||||||
|
"createGroupchat": "Nieuw groepsgesprek",
|
||||||
|
"nick": "Bijnaam",
|
||||||
|
"enterNickname": "Voer een bijnaam in",
|
||||||
|
"nicknameSubtitle": "Voer een unieke bijnaam in om deel te kunnen nemen aan een MUC.",
|
||||||
|
"joinGroupChat": "Deelnemen aan groepsgesprek",
|
||||||
|
"nullNickname": "Voer een bijnaam in!"
|
||||||
|
},
|
||||||
|
"crop": {
|
||||||
|
"setProfilePicture": "Instellen als profielfoto"
|
||||||
|
},
|
||||||
|
"shareselection": {
|
||||||
|
"shareWith": "Delen met…",
|
||||||
|
"confirmTitle": "Bestand versturen",
|
||||||
|
"confirmBody": "Een of meerdere gesprekken zijn onversleuteld. Dit houdt in dat het bestand kan worden uitgelezen door de server. Weet je zeker dat je wilt doorgaan?"
|
||||||
|
},
|
||||||
|
"profile": {
|
||||||
|
"general": {
|
||||||
|
"omemo": "Beveiliging",
|
||||||
|
"profile": "Profiel",
|
||||||
|
"media": "Media"
|
||||||
|
},
|
||||||
|
"conversation": {
|
||||||
|
"notifications": "Meldingen",
|
||||||
|
"notificationsMuted": "Gedempt",
|
||||||
|
"notificationsEnabled": "Ingeschakeld",
|
||||||
|
"sharedMedia": "Media"
|
||||||
|
},
|
||||||
|
"owndevices": {
|
||||||
|
"title": "Mijn apparaten",
|
||||||
|
"thisDevice": "Dit apparaat",
|
||||||
|
"otherDevices": "Overige apparaten",
|
||||||
|
"deleteDeviceConfirmTitle": "Apparaat verwijderen",
|
||||||
|
"deleteDeviceConfirmBody": "Let op: hierdoor kunnen contactpersonen het apparaat niet meer versleutelen. Wil je doorgaan?",
|
||||||
|
"recreateOwnSessions": "Sessies heraanmaken",
|
||||||
|
"recreateOwnSessionsConfirmTitle": "Wil je je sessies heraanmaken?",
|
||||||
|
"recreateOwnDevice": "Apparaat heraanmaken",
|
||||||
|
"recreateOwnDeviceConfirmTitle": "Wil je je apparaat heraanmaken?",
|
||||||
|
"recreateOwnSessionsConfirmBody": "Hierdoor worden de versleutelde sessies opnieuw aangemaakt op je apparaten. Let op: doe dit alléén als apparaten ontsleutelfoutmeldingen tonen.",
|
||||||
|
"recreateOwnDeviceConfirmBody": "Hierdoor wordt de versleutelde identiteit van dit apparaat opnieuw aangemaakt. Dit kan even duren. Als contactpersonen je apparaat hebben goedgekeurd, dan dienen ze dit opnieuw te doen. Wil je doorgaan?"
|
||||||
|
},
|
||||||
|
"devices": {
|
||||||
|
"title": "Beveiliging",
|
||||||
|
"recreateSessions": "Sessies heraanmaken",
|
||||||
|
"recreateSessionsConfirmTitle": "Wil je je sessies heraanmaken?",
|
||||||
|
"recreateSessionsConfirmBody": "Hierdoor worden alle versleutelde sessies opnieuw aangemaakt op je apparaten. Let op: doe dit alléén als je apparaten ontsleutelfoutmeldingen tonen.",
|
||||||
|
"noSessions": "Er zijn geen versleutelde sessies die worden gebruikt voor eind-tot-eindversleuteling."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"blocklist": {
|
||||||
|
"title": "Blokkadelijst",
|
||||||
|
"noUsersBlocked": "Er zijn geen geblokkeerde gebruikers",
|
||||||
|
"unblockAll": "Iedereen deblokkeren",
|
||||||
|
"unblockAllConfirmTitle": "Weet je het zeker?",
|
||||||
|
"unblockAllConfirmBody": "Weet je zeker dat je alle gebruikers wilt deblokkeren?",
|
||||||
|
"unblockJidConfirmTitle": "Wil je ${jid} deblokkeren?",
|
||||||
|
"unblockJidConfirmBody": "Weet je zeker dat je ${jid} wilt deblokkeren? Hierdoor ontvang je weer berichten van deze gebruiker."
|
||||||
|
},
|
||||||
|
"cropbackground": {
|
||||||
|
"blur": "Achtergrond vervagen",
|
||||||
|
"setAsBackground": "Instellen als achtergrond"
|
||||||
|
},
|
||||||
|
"stickerPack": {
|
||||||
|
"removeConfirmTitle": "Stickerpakket verwijderen",
|
||||||
|
"installConfirmTitle": "Stickerpakket installeren",
|
||||||
|
"installConfirmBody": "Weet je zeker dat je dit stickerpakket wilt installeren?",
|
||||||
|
"fetchingFailure": "Het stickerpakket is niet gevonden",
|
||||||
|
"removeConfirmBody": "Weet je zeker dat je dit stickerpakket wilt verwijderen?",
|
||||||
|
"restricted": "Dit stickerpakket is beperkt toegankelijk. Dit houdt in dat de stickers kunnen worden getoond, maar niet worden verstuurd."
|
||||||
|
},
|
||||||
|
"settings": {
|
||||||
|
"settings": {
|
||||||
|
"title": "Instellingen",
|
||||||
|
"conversationsSection": "Gesprekken",
|
||||||
|
"accountSection": "Account",
|
||||||
|
"signOut": "Uitloggen",
|
||||||
|
"signOutConfirmTitle": "Uitloggen",
|
||||||
|
"signOutConfirmBody": "Je staat op het punt om uit te loggen. Wil je doorgaan?",
|
||||||
|
"miscellaneousSection": "Overig",
|
||||||
|
"debuggingSection": "Foutopsporing",
|
||||||
|
"general": "Algemeen"
|
||||||
|
},
|
||||||
|
"about": {
|
||||||
|
"title": "Over",
|
||||||
|
"licensed": "Uitgebracht onder de GPL3-licentie",
|
||||||
|
"version": "Versie ${version}",
|
||||||
|
"viewSourceCode": "Broncode bekijken",
|
||||||
|
"nMoreToGo": "Nog ${n} te gaan…",
|
||||||
|
"debugMenuShown": "Je bent nu een ontwikkelaar!",
|
||||||
|
"debugMenuAlreadyShown": "Je bent al een ontwikkelaar!"
|
||||||
|
},
|
||||||
|
"appearance": {
|
||||||
|
"title": "Vormgeving",
|
||||||
|
"languageSection": "Taal",
|
||||||
|
"language": "Apptaal",
|
||||||
|
"languageSubtext": "Huidige taal: $selectedLanguage",
|
||||||
|
"systemLanguage": "Standaardtaal"
|
||||||
|
},
|
||||||
|
"licenses": {
|
||||||
|
"title": "Opensourcelicenties",
|
||||||
|
"licensedUnder": "Uitgebracht onder de $license-licentie"
|
||||||
|
},
|
||||||
|
"conversation": {
|
||||||
|
"title": "Gesprek",
|
||||||
|
"appearance": "Vormgeving",
|
||||||
|
"selectBackgroundImage": "Kies een achtergrond",
|
||||||
|
"removeBackgroundImage": "Afbeelding verwijderen",
|
||||||
|
"removeBackgroundImageConfirmTitle": "Afbeelding verwijderen",
|
||||||
|
"removeBackgroundImageConfirmBody": "Weet je zeker dat je de huidige gespreksachtergrond wilt verwijderen?",
|
||||||
|
"newChatsSection": "Nieuwe gesprekken",
|
||||||
|
"newChatsMuteByDefault": "Nieuwe gesprekken dempen",
|
||||||
|
"newChatsE2EE": "Eind-tot-eindversleuteling standaard inschakelen (WAARSCHUWING: experimenteel)",
|
||||||
|
"behaviourSection": "Gedrag",
|
||||||
|
"contactsIntegration": "Contactpersoonintegratie",
|
||||||
|
"contactsIntegrationBody": "Schakel in om het adresboek te gebruiken om gesprekstitels en profielfoto's in te stellen. Er worden geen gegevens verstuurd naar de server.",
|
||||||
|
"selectBackgroundImageDescription": "Deze afbeelding wordt gebruikt als achtergrond in al je gesprekken"
|
||||||
|
},
|
||||||
|
"debugging": {
|
||||||
|
"title": "Foutopsporingsopties",
|
||||||
|
"generalSection": "Algemeen",
|
||||||
|
"generalEnableDebugging": "Foutopsporing inschakelen",
|
||||||
|
"generalEncryptionPassword": "Versleutelwachtwoord",
|
||||||
|
"generalLoggingIp": "Ip-log",
|
||||||
|
"generalLoggingIpSubtext": "Het ip-adres waar de logboeken naartoe dienen te worden gestuurd",
|
||||||
|
"generalLoggingPort": "Logpoort",
|
||||||
|
"generalLoggingPortSubtext": "Het ip-adres waar de logboeken naartoe dienen te worden gestuurd",
|
||||||
|
"generalEncryptionPasswordSubtext": "Let op: de logboeken kunnen privéinformatie bevatten, dus stel een sterk wachtwoord in"
|
||||||
|
},
|
||||||
|
"network": {
|
||||||
|
"title": "Netwerk",
|
||||||
|
"automaticDownloadsSection": "Automatisch ophalen",
|
||||||
|
"automaticDownloadsMaximumSize": "Maximale downloadomvang",
|
||||||
|
"automaticDownloadsMaximumSizeSubtext": "De maximale bestandsgrootte van automatisch op te halen bestanden",
|
||||||
|
"automaticDownloadAlways": "Ieder netwerk",
|
||||||
|
"wifi": "Wifi",
|
||||||
|
"mobileData": "Mobiel internet",
|
||||||
|
"automaticDownloadsText": "Moxxy zal bestanden automatisch ophalen bij gebruik van…"
|
||||||
|
},
|
||||||
|
"privacy": {
|
||||||
|
"title": "Privacy",
|
||||||
|
"generalSection": "Algemeen",
|
||||||
|
"showContactRequests": "Contactpersoonverzoeken tonen",
|
||||||
|
"profilePictureVisibility": "Profielfoto aan iedereen tonen",
|
||||||
|
"conversationsSection": "Gesprek",
|
||||||
|
"sendChatMarkersSubtext": "Hiermee kan je gesprekspartner zien of je een bericht gelezen of ontvangen hebt",
|
||||||
|
"sendChatMarkers": "Gespreksacties versturen",
|
||||||
|
"sendChatStates": "Gespreksstatussen versturen",
|
||||||
|
"sendChatStatesSubtext": "Hiermee kan je gesprekspartner zien of je aan het typen of het gesprek aan het bekijken bent",
|
||||||
|
"redirectsSection": "Doorverwijzingen",
|
||||||
|
"redirectText": "Hiermee worden $serviceName-links doorverwezen naar een proxy, bijvoorbeeld $exampleProxy",
|
||||||
|
"currentlySelected": "Huidige proxy: $proxy",
|
||||||
|
"redirectsTitle": "$serviceName-doorverwijzing",
|
||||||
|
"cannotEnableRedirect": "$serviceName-doorverwijzingen mislukt",
|
||||||
|
"cannotEnableRedirectSubtext": "Stel eerst een proxy als doorverwijzing in. Druk hiervoor op het veld naast de schakelaar.",
|
||||||
|
"urlEmpty": "Voer een url in",
|
||||||
|
"urlInvalid": "De url is ongeldig",
|
||||||
|
"redirectDialogTitle": "$serviceName-doorverwijzing",
|
||||||
|
"stickersPrivacy": "Stickerlijst aan iedereen tonen",
|
||||||
|
"stickersPrivacySubtext": "Schakel in om je lijst met stickerpakketten aan iedereen te tonen",
|
||||||
|
"showContactRequestsSubtext": "Hiermee worden verzoeken getoond van personen die je hebben toegevoegd, maar nog geen bericht hebben gestuurd",
|
||||||
|
"profilePictureVisibilitSubtext": "Schakel in om iedereen je profielfoto te tonen; schakel uit om alleen gebruikers op je lijst je profielfoto te tonen"
|
||||||
|
},
|
||||||
|
"stickers": {
|
||||||
|
"title": "Stickers",
|
||||||
|
"stickerSection": "Sticker",
|
||||||
|
"displayStickers": "Stickers in gesprekken tonen",
|
||||||
|
"autoDownload": "Stickers automatisch ophalen",
|
||||||
|
"autoDownloadBody": "Schakel in om stickers automatisch op te halen na het toevoegen van de afzender",
|
||||||
|
"stickerPacksSection": "Stickerpakketten",
|
||||||
|
"importStickerPack": "Stickerpakket importeren",
|
||||||
|
"importSuccess": "Het stickerpakket is geïmporteerd",
|
||||||
|
"importFailure": "Het stickerpakket kan niet worden geïmporteerd",
|
||||||
|
"stickerPackSize": "(${size})"
|
||||||
|
},
|
||||||
|
"storage": {
|
||||||
|
"title": "Opslag",
|
||||||
|
"storageUsed": "In gebruik: ${size}",
|
||||||
|
"sizePlaceholder": "Bezig met berekenen…",
|
||||||
|
"storageManagement": "Opslagbeheer",
|
||||||
|
"removeOldMedia": {
|
||||||
|
"title": "Oude media verwijderen",
|
||||||
|
"description": "Verwijdert oude mediabestanden van het apparaat"
|
||||||
|
},
|
||||||
|
"removeOldMediaDialog": {
|
||||||
|
"title": "Mediabestanden verwijderen",
|
||||||
|
"options": {
|
||||||
|
"all": "Alle mediabestanden",
|
||||||
|
"oneWeek": "Ouder dan 1 week",
|
||||||
|
"oneMonth": "Ouder dan 1 maand"
|
||||||
|
},
|
||||||
|
"delete": "Verwijderen",
|
||||||
|
"confirmation": {
|
||||||
|
"body": "Weet je zeker dat je oude mediabestanden wilt verwijderen?"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"viewMediaFiles": "Mediabestanden bekijken",
|
||||||
|
"mediaFiles": "Mediabestanden",
|
||||||
|
"types": {
|
||||||
|
"media": "Media",
|
||||||
|
"stickers": "Stickers"
|
||||||
|
},
|
||||||
|
"manageStickers": "Stickerpakketten beheren"
|
||||||
|
},
|
||||||
|
"stickerPacks": {
|
||||||
|
"title": "Stickerpakketten"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"startchat": {
|
||||||
|
"title": "Nieuw gesprek",
|
||||||
|
"xmppAddress": "Xmpp-adres",
|
||||||
|
"subtitle": "Je kunt een nieuw gesprek starten door een xmpp-adres in te voeren of een QR-code te scannen.",
|
||||||
|
"buttonAddToContact": "Gesprek starten"
|
||||||
|
},
|
||||||
|
"sharedMedia": {
|
||||||
|
"empty": {
|
||||||
|
"chat": "Er is geen gedeelde media in dit gesprek",
|
||||||
|
"general": "Er zijn geen mediabestanden beschikbaar"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"permissions": {
|
||||||
|
"allow": "Toestaan",
|
||||||
|
"skip": "Overslaan",
|
||||||
|
"requests": {
|
||||||
|
"notification": {
|
||||||
|
"reason": "Moxxy heeft het recht om meldingen te tonen nodig om meldingen van inkomende berichten te kunnen tonen."
|
||||||
|
},
|
||||||
|
"batterySaving": {
|
||||||
|
"reason": "Sluit Moxxy uit van Androids accubesparing om berichten op de achtergrond te ontvangen."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
37
assets/i18n/strings_pl.i18n.json
Normal file
37
assets/i18n/strings_pl.i18n.json
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
{
|
||||||
|
"language": "Polski",
|
||||||
|
"global": {
|
||||||
|
"title": "Moxxy",
|
||||||
|
"dialogAccept": "OK",
|
||||||
|
"yes": "Tak",
|
||||||
|
"no": "Nie",
|
||||||
|
"dialogCancel": "Anuluj"
|
||||||
|
},
|
||||||
|
"notifications": {
|
||||||
|
"permanent": {
|
||||||
|
"connecting": "Łączenie…",
|
||||||
|
"disconnect": "Rozłączono",
|
||||||
|
"error": "Błąd"
|
||||||
|
},
|
||||||
|
"channels": {
|
||||||
|
"messagesChannelName": "Wiadomości",
|
||||||
|
"warningChannelName": "Ostrzeżenia"
|
||||||
|
},
|
||||||
|
"titles": {
|
||||||
|
"error": "Błąd"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"permissions": {
|
||||||
|
"allow": "Zezwól",
|
||||||
|
"skip": "Pomiń"
|
||||||
|
},
|
||||||
|
"dateTime": {
|
||||||
|
"mondayAbbrev": "Pon",
|
||||||
|
"tuesdayAbbrev": "Wt",
|
||||||
|
"wednessdayAbbrev": "Śr",
|
||||||
|
"thursdayAbbrev": "Czw",
|
||||||
|
"fridayAbbrev": "Pt",
|
||||||
|
"saturdayAbbrev": "Sob",
|
||||||
|
"sundayAbbrev": "Nd"
|
||||||
|
}
|
||||||
|
}
|
||||||
422
assets/i18n/strings_ru.i18n.json
Normal file
422
assets/i18n/strings_ru.i18n.json
Normal file
@@ -0,0 +1,422 @@
|
|||||||
|
{
|
||||||
|
"global": {
|
||||||
|
"title": "Moxxy",
|
||||||
|
"dialogAccept": "Принять",
|
||||||
|
"dialogCancel": "Отмена",
|
||||||
|
"yes": "Да",
|
||||||
|
"no": "Нет",
|
||||||
|
"moxxySubtitle": "Эксперементальный XMPP-клиент, простой, современный и красивый."
|
||||||
|
},
|
||||||
|
"notifications": {
|
||||||
|
"permanent": {
|
||||||
|
"idle": "Idle",
|
||||||
|
"ready": "Готов к приему сообщений",
|
||||||
|
"connecting": "Подключение…",
|
||||||
|
"disconnect": "Отключен",
|
||||||
|
"error": "Ошибка"
|
||||||
|
},
|
||||||
|
"message": {
|
||||||
|
"reply": "Ответ",
|
||||||
|
"markAsRead": "Прочитано"
|
||||||
|
},
|
||||||
|
"channels": {
|
||||||
|
"messagesChannelName": "Сообщения",
|
||||||
|
"warningChannelName": "Предупреждения",
|
||||||
|
"warningChannelDescription": "Предупреждения, связанные с Moxxy",
|
||||||
|
"messagesChannelDescription": "Канал уведомлений о полученных сообщениях"
|
||||||
|
},
|
||||||
|
"titles": {
|
||||||
|
"error": "Ошибка"
|
||||||
|
},
|
||||||
|
"errors": {
|
||||||
|
"messageError": {
|
||||||
|
"body": "Не удалось доставить сообщение для ${conversationTitle}",
|
||||||
|
"title": "Сообщение не доставлено"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"dateTime": {
|
||||||
|
"justNow": "Сейчас",
|
||||||
|
"nMinutesAgo": "${min}минут назад",
|
||||||
|
"mondayAbbrev": "Пн",
|
||||||
|
"tuesdayAbbrev": "Вт",
|
||||||
|
"wednessdayAbbrev": "Ср",
|
||||||
|
"thursdayAbbrev": "Чт",
|
||||||
|
"fridayAbbrev": "Пт",
|
||||||
|
"sundayAbbrev": "Вс",
|
||||||
|
"january": "Январь",
|
||||||
|
"february": "Февраль",
|
||||||
|
"may": "Май",
|
||||||
|
"june": "Июнь",
|
||||||
|
"october": "Октябрь",
|
||||||
|
"november": "Ноябрь",
|
||||||
|
"december": "Декабрь",
|
||||||
|
"today": "Сегодня",
|
||||||
|
"saturdayAbbrev": "Сб",
|
||||||
|
"march": "Март",
|
||||||
|
"april": "Апрель",
|
||||||
|
"july": "Июль",
|
||||||
|
"august": "Август",
|
||||||
|
"september": "Сентябрь",
|
||||||
|
"yesterday": "Вчера"
|
||||||
|
},
|
||||||
|
"messages": {
|
||||||
|
"image": "Изображение",
|
||||||
|
"video": "Видео",
|
||||||
|
"file": "Файл",
|
||||||
|
"sticker": "Стикер",
|
||||||
|
"you": "Ты",
|
||||||
|
"audio": "Аудио",
|
||||||
|
"retracted": "Сообщение удалено",
|
||||||
|
"retractedFallback": "Предыдущее сообщение было удалено, но это не поддерживается Вашим клиентом"
|
||||||
|
},
|
||||||
|
"errors": {
|
||||||
|
"filePicker": {
|
||||||
|
"permissionDenied": "Доступ к хранилищу не был выдан"
|
||||||
|
},
|
||||||
|
"omemo": {
|
||||||
|
"notEncryptedForDevice": "Сообщение зашифровано, но не для этого устройства",
|
||||||
|
"invalidHmac": "Не удалось расшифровать сообщение",
|
||||||
|
"noDecryptionKey": "Нет ключа для расшифровки",
|
||||||
|
"verificationWrongFingerprint": "Неправильный отпечаток OMEMO:2",
|
||||||
|
"couldNotPublish": "Не удалось опубликовать ключи шифрования на сервере. Это означает, что сквозное шифрование может не работать.",
|
||||||
|
"messageInvalidAfixElement": "Ошибка в зашифрованном сообщении",
|
||||||
|
"verificationInvalidOmemoUrl": "неверный отпечаток OMEMO:2",
|
||||||
|
"verificationWrongJid": "Неправильный XMPP-адрес",
|
||||||
|
"verificationWrongDevice": "Неправильное OMEMO:2 устройство",
|
||||||
|
"verificationNotInList": "Неправильное устройство OMEMO:2"
|
||||||
|
},
|
||||||
|
"connection": {
|
||||||
|
"connectionTimeout": "Нет соединения с сервером",
|
||||||
|
"saslInvalidCredentials": "Данные учетной записи недействительны",
|
||||||
|
"unrecoverable": "Соединение прервано из-за ошибки",
|
||||||
|
"saslAccountDisabled": "Аккаунт отключен"
|
||||||
|
},
|
||||||
|
"login": {
|
||||||
|
"saslFailed": "Неверный логин",
|
||||||
|
"startTlsFailed": "Не удалось установить безопасное соединение",
|
||||||
|
"noConnection": "Не удалось установить соединение",
|
||||||
|
"unspecified": "Неопределенная ошибка"
|
||||||
|
},
|
||||||
|
"message": {
|
||||||
|
"fileDownloadFailed": "Не удалось загрузить файл",
|
||||||
|
"remoteServerTimeout": "Сообщение не доставлено на сервер получателя",
|
||||||
|
"unspecified": "Неизвесная ошибка",
|
||||||
|
"fileUploadFailed": "Не удалось отправить файл",
|
||||||
|
"contactDoesntSupportOmemo": "Получатель не поддерживает OMEMO:2 шифрование",
|
||||||
|
"serviceUnavailable": "Сообщение не доставлено получателю",
|
||||||
|
"remoteServerNotFound": "Cообщение не доставлено, не найден сервер получателя",
|
||||||
|
"failedToEncrypt": "Сообщение не может быть зашифровано",
|
||||||
|
"failedToEncryptFile": "Файл не может быть зашифрован",
|
||||||
|
"failedToDecryptFile": "Файл не может быть расшифрован",
|
||||||
|
"fileNotEncrypted": "Этот чат зашифрован, но файл нет"
|
||||||
|
},
|
||||||
|
"conversation": {
|
||||||
|
"audioRecordingError": "Не удалось завершить аудиозапись",
|
||||||
|
"openFileNoAppError": "Приложения для открытия этого файла не найдены",
|
||||||
|
"openFileGenericError": "Не удалось открыть файл",
|
||||||
|
"messageErrorDialogTitle": "Ошибка"
|
||||||
|
},
|
||||||
|
"newChat": {
|
||||||
|
"groupchatUnsupported": "Вступление в групповой чат пока не поддерживается.",
|
||||||
|
"remoteServerError": "Не удалось связаться с удалённым сервером.",
|
||||||
|
"unknown": "Неизвестная ошибка."
|
||||||
|
},
|
||||||
|
"general": {
|
||||||
|
"noInternet": "Нет подключения к интернету."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"pages": {
|
||||||
|
"intro": {
|
||||||
|
"registerButton": "Регистрация",
|
||||||
|
"loginButton": "Login",
|
||||||
|
"noAccount": "Нет XMPP аккаунта? Зарегистрируйтесь на одном из серверов, это не сложно."
|
||||||
|
},
|
||||||
|
"login": {
|
||||||
|
"title": "Login",
|
||||||
|
"password": "Пароль",
|
||||||
|
"xmppAddress": "XMPP-адрес",
|
||||||
|
"advancedOptions": "Расширенные опции",
|
||||||
|
"createAccount": "Зарегистрироваться на сервере"
|
||||||
|
},
|
||||||
|
"conversations": {
|
||||||
|
"speeddialNewChat": "Написать",
|
||||||
|
"speeddialJoinGroupchat": "Групповой чат",
|
||||||
|
"speeddialAddNoteToSelf": "Заметка себе",
|
||||||
|
"overlaySettings": "Настройки",
|
||||||
|
"startChat": "Начать диалог",
|
||||||
|
"markAsRead": "Пометить как прочитанное",
|
||||||
|
"noOpenChats": "У вас пока нет диалогов",
|
||||||
|
"closeChat": "Завершить диалог",
|
||||||
|
"closeChatBody": "Вы уверены, что хотите завершить диалог с ${conversationTitle}?"
|
||||||
|
},
|
||||||
|
"conversation": {
|
||||||
|
"unencrypted": "Незашифрованно",
|
||||||
|
"encrypted": "Зашифрованно",
|
||||||
|
"closeChat": "Закрыть чат",
|
||||||
|
"closeChatConfirmTitle": "Закрыть чат",
|
||||||
|
"closeChatConfirmSubtext": "Вы уверены что хотите закрыть этот чат?",
|
||||||
|
"blockShort": "Заблокировать",
|
||||||
|
"blockUser": "Заблокировать пользователя",
|
||||||
|
"online": "В сети",
|
||||||
|
"retract": "Отозвать сообщение",
|
||||||
|
"copy": "Копировать содержимое",
|
||||||
|
"addReaction": "Реакция",
|
||||||
|
"showError": "Показать ошибки",
|
||||||
|
"showWarning": "Показать предупреждения",
|
||||||
|
"sendFiles": "Отправить файлы",
|
||||||
|
"takePhotos": "Сделать фотографии",
|
||||||
|
"retractBody": "Вы уверены, что хотите отозвать сообщение? Помните, что это всего лишь просьба, которую клиент не обязан выполнять.",
|
||||||
|
"forward": "Переслать",
|
||||||
|
"edit": "Изменить",
|
||||||
|
"quote": "Цитировать",
|
||||||
|
"addToContacts": "Добавить в контакты",
|
||||||
|
"addToContactsTitle": "Добавить ${jid} в контакты",
|
||||||
|
"addToContactsBody": "Вы уверены, что хотите добавить ${jid} в контакты?",
|
||||||
|
"stickerPickerNoStickersLine1": "Нет установленных стикерпаков.",
|
||||||
|
"stickerPickerNoStickersLine2": "Их можно установить в настройках стикеров.",
|
||||||
|
"stickerSettings": "Настройки стикеров",
|
||||||
|
"newDeviceMessage": {
|
||||||
|
"one": "Добавлено новое устройство",
|
||||||
|
"other": "Добавлено несколько новых устройств"
|
||||||
|
},
|
||||||
|
"replacedDeviceMessage": {
|
||||||
|
"one": "Устройство было изменено",
|
||||||
|
"other": "Добавлено несколько устройств"
|
||||||
|
},
|
||||||
|
"messageHint": "Сообщение...",
|
||||||
|
"sendImages": "Отправить изображение",
|
||||||
|
"messageCopied": "Сообщение скопировано в буфер",
|
||||||
|
"warning": "Предупреждение"
|
||||||
|
},
|
||||||
|
"startchat": {
|
||||||
|
"xmppAddress": "XMPP-адрес",
|
||||||
|
"buttonAddToContact": "Добавить в контакты",
|
||||||
|
"title": "Добавить контакт",
|
||||||
|
"subtitle": "Вы можете добавить контакт введя его XMPP адрес или отсканировав QR код"
|
||||||
|
},
|
||||||
|
"newconversation": {
|
||||||
|
"title": "Новый чат",
|
||||||
|
"startChat": "Добавить контакт",
|
||||||
|
"createGroupchat": "Создать новый групповой чат"
|
||||||
|
},
|
||||||
|
"shareselection": {
|
||||||
|
"shareWith": "Поделиться с...",
|
||||||
|
"confirmTitle": "Отправить файл",
|
||||||
|
"confirmBody": "Один или несколько чатов не зашифрованы, из-за чего файл будет доступен администрации сервера. Вы уверены, что хотите продолжить?"
|
||||||
|
},
|
||||||
|
"profile": {
|
||||||
|
"general": {
|
||||||
|
"omemo": "Безопасность",
|
||||||
|
"profile": "Профиль",
|
||||||
|
"media": "Медиа"
|
||||||
|
},
|
||||||
|
"conversation": {
|
||||||
|
"sharedMedia": "Медиа",
|
||||||
|
"notifications": "Уведомления",
|
||||||
|
"notificationsMuted": "Без звука",
|
||||||
|
"notificationsEnabled": "Включено"
|
||||||
|
},
|
||||||
|
"owndevices": {
|
||||||
|
"thisDevice": "Это устройство",
|
||||||
|
"recreateOwnDevice": "Восстановить устройство",
|
||||||
|
"title": "Мои устройства",
|
||||||
|
"otherDevices": "Другие устройства",
|
||||||
|
"deleteDeviceConfirmTitle": "Удалить устройство",
|
||||||
|
"deleteDeviceConfirmBody": "Контакты не смогут быть зашифрованы для этого устройства. Продолжить?",
|
||||||
|
"recreateOwnSessions": "Пересоздать сеанс",
|
||||||
|
"recreateOwnSessionsConfirmTitle": "Пересоздать свои сеансы?",
|
||||||
|
"recreateOwnSessionsConfirmBody": "Создать новые ключи шифрования для этого устройства. Используйте только в крайнем случае.",
|
||||||
|
"recreateOwnDeviceConfirmTitle": "Восстановить это устройство?",
|
||||||
|
"recreateOwnDeviceConfirmBody": "Это создаст новый криптографический отпечаток устройства, что займёт некоторое время. Если ваше устройство было подтверждено контактами, им придётся сделать это снова. Продолжить?"
|
||||||
|
},
|
||||||
|
"devices": {
|
||||||
|
"title": "Безопасность",
|
||||||
|
"recreateSessionsConfirmTitle": "Пересоздать сеанс?",
|
||||||
|
"noSessions": "Нет криптографических сессий, используемых для сквозного шифрования.",
|
||||||
|
"recreateSessions": "Пересоздать сеанс",
|
||||||
|
"recreateSessionsConfirmBody": "Создать новые ключи шифрования для этого устройства. Используйте только в крайнем случае."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"blocklist": {
|
||||||
|
"unblockAll": "Разблокировать всех",
|
||||||
|
"unblockJidConfirmTitle": "Разблокировать ${jid}?",
|
||||||
|
"title": "Блоклист",
|
||||||
|
"noUsersBlocked": "Нет заблокированных пользователей",
|
||||||
|
"unblockAllConfirmTitle": "Вы уверены?",
|
||||||
|
"unblockAllConfirmBody": "Вы действительно хотите разблокировать всех пользователей?",
|
||||||
|
"unblockJidConfirmBody": "Вы уверены, что хотите разблокировать ${jid}? Вы снова будете получать сообщения от этого пользователя."
|
||||||
|
},
|
||||||
|
"cropbackground": {
|
||||||
|
"blur": "Размыть фон",
|
||||||
|
"setAsBackground": "Установить фоновое изображение"
|
||||||
|
},
|
||||||
|
"crop": {
|
||||||
|
"setProfilePicture": "Загрузить аватар"
|
||||||
|
},
|
||||||
|
"stickerPack": {
|
||||||
|
"removeConfirmTitle": "Удалить стикерпак",
|
||||||
|
"removeConfirmBody": "Вы действительно хотите удалить этот стикерпак?",
|
||||||
|
"installConfirmTitle": "добавить стикерпак",
|
||||||
|
"installConfirmBody": "Вы действительно хотите установить этот стикерпак?",
|
||||||
|
"restricted": "Этот стикерпак ограничен, стикеры будут отображаться, но отправить их нельзя.",
|
||||||
|
"fetchingFailure": "Стикерпак не найден"
|
||||||
|
},
|
||||||
|
"settings": {
|
||||||
|
"about": {
|
||||||
|
"version": "Версия ${version}",
|
||||||
|
"debugMenuShown": "Теперь ты разработчик ^_^",
|
||||||
|
"debugMenuAlreadyShown": "Ты уже разработчик :^",
|
||||||
|
"title": "О нас",
|
||||||
|
"viewSourceCode": "Исходный код",
|
||||||
|
"nMoreToGo": "Осталось еще ${n}...",
|
||||||
|
"licensed": "Лицензировано под GPL3"
|
||||||
|
},
|
||||||
|
"conversation": {
|
||||||
|
"removeBackgroundImageConfirmBody": "Вы действительно хотите удалить фон?",
|
||||||
|
"title": "Чат",
|
||||||
|
"appearance": "Внешний вид",
|
||||||
|
"selectBackgroundImage": "Выбрать фон",
|
||||||
|
"removeBackgroundImage": "Удалить фон",
|
||||||
|
"removeBackgroundImageConfirmTitle": "Удалить фон",
|
||||||
|
"newChatsSection": "Новые чаты",
|
||||||
|
"newChatsMuteByDefault": "Отключать звук в новых чатах по умолчанию",
|
||||||
|
"newChatsE2EE": "Включить оконечное шифрование по умолчанию",
|
||||||
|
"behaviourSection": "Поведение",
|
||||||
|
"contactsIntegration": "Синхронизация контактов",
|
||||||
|
"selectBackgroundImageDescription": "Это изображение будет фоном для ваших чатов",
|
||||||
|
"contactsIntegrationBody": "При включении данные из Контактов будут использованы для названий чатов и фото профилей. На сервер ничего отправлено не будет."
|
||||||
|
},
|
||||||
|
"debugging": {
|
||||||
|
"title": "Опции отладки",
|
||||||
|
"generalSection": "Основные",
|
||||||
|
"generalEnableDebugging": "Включить отладку",
|
||||||
|
"generalEncryptionPassword": "Пароль шифрования",
|
||||||
|
"generalEncryptionPasswordSubtext": "Журналы могут содержать конфиденциальную информацию, поэтому поставте надежный пароль",
|
||||||
|
"generalLoggingIpSubtext": "IP, на который должны отправляться журналы",
|
||||||
|
"generalLoggingIp": "IP для логов",
|
||||||
|
"generalLoggingPort": "порт для логов",
|
||||||
|
"generalLoggingPortSubtext": "IP, на который должны отправляться журналы"
|
||||||
|
},
|
||||||
|
"network": {
|
||||||
|
"automaticDownloadsSection": "Автоматическая загрузка",
|
||||||
|
"title": "Сеть",
|
||||||
|
"automaticDownloadsMaximumSizeSubtext": "Максимальный размер, при котором файлы будут автоматически загружаться",
|
||||||
|
"automaticDownloadsMaximumSize": "Максимальный размер для загрузки",
|
||||||
|
"automaticDownloadAlways": "Всегда",
|
||||||
|
"wifi": "Wifi",
|
||||||
|
"mobileData": "Мобильный интернет",
|
||||||
|
"automaticDownloadsText": "Moxxy будет автоматически загружать файлы до..."
|
||||||
|
},
|
||||||
|
"privacy": {
|
||||||
|
"showContactRequests": "Показывать запрос в контакты",
|
||||||
|
"showContactRequestsSubtext": "Это покажет людей, добавивших вас в свой список контактов",
|
||||||
|
"generalSection": "Основные",
|
||||||
|
"profilePictureVisibility": "Сделать фото профиля публичным",
|
||||||
|
"sendChatMarkers": "Отправлять маркеры",
|
||||||
|
"redirectText": "Это позволит перенаправлять ссылки с ${serviceName} на прокси, такие как ${exampleProxy}",
|
||||||
|
"redirectsSection": "Перенаправление",
|
||||||
|
"currentlySelected": "Выбрано сейчас: $proxy",
|
||||||
|
"redirectsTitle": "$serviceName Перенаправление",
|
||||||
|
"urlEmpty": "URL не может быть пустым",
|
||||||
|
"title": "Приватность",
|
||||||
|
"conversationsSection": "Диалог",
|
||||||
|
"sendChatMarkersSubtext": "Это сообщит вашему собеседнику о получении или прочтении сообщения",
|
||||||
|
"sendChatStates": "Отправлять состояние чата",
|
||||||
|
"sendChatStatesSubtext": "Собеседник будет видеть, когда вы набираете сообщение или просматриваете чат",
|
||||||
|
"cannotEnableRedirect": "Невозможно включить перенаправления $serviceName",
|
||||||
|
"cannotEnableRedirectSubtext": "Сначала нужно добавить прокси сервер. Для этого нажмите слева от переключателя",
|
||||||
|
"urlInvalid": "Недопустимый URL",
|
||||||
|
"redirectDialogTitle": "$serviceName Перенаправление",
|
||||||
|
"stickersPrivacy": "Публиковать список стикеров",
|
||||||
|
"stickersPrivacySubtext": "Если включено, ваши установленные наборы стикеров будут видны всем.",
|
||||||
|
"profilePictureVisibilitSubtext": "Когда включено, все видят Ваш аватар; когда выключено - только контакты"
|
||||||
|
},
|
||||||
|
"stickers": {
|
||||||
|
"importSuccess": "Стикерпаки успешно импортированы",
|
||||||
|
"title": "Стикеры",
|
||||||
|
"stickerSection": "Стикеры",
|
||||||
|
"displayStickers": "Показывать стикеры",
|
||||||
|
"autoDownload": "Загружать стикеры автоматически",
|
||||||
|
"autoDownloadBody": "Стикеры будут загружаться автоматически, если их отправитель у вас в контактах.",
|
||||||
|
"stickerPacksSection": "Стикерпаки",
|
||||||
|
"importStickerPack": "Импортировать стикерпаки",
|
||||||
|
"importFailure": "Ошибка при импорте стикерпаков",
|
||||||
|
"stickerPackSize": "(${size})"
|
||||||
|
},
|
||||||
|
"settings": {
|
||||||
|
"title": "Настройки",
|
||||||
|
"conversationsSection": "Чаты",
|
||||||
|
"accountSection": "Учётная запись",
|
||||||
|
"signOut": "Выйти",
|
||||||
|
"signOutConfirmTitle": "Выйти",
|
||||||
|
"signOutConfirmBody": "Вы хотите выйти, продолжить?",
|
||||||
|
"miscellaneousSection": "Другое",
|
||||||
|
"debuggingSection": "Отладка",
|
||||||
|
"general": "Основные"
|
||||||
|
},
|
||||||
|
"appearance": {
|
||||||
|
"title": "Внешний вид",
|
||||||
|
"languageSection": "Язык",
|
||||||
|
"language": "Язык в приложении",
|
||||||
|
"systemLanguage": "Как в системе",
|
||||||
|
"languageSubtext": "Выбранный язык: ${selectedLanguage}"
|
||||||
|
},
|
||||||
|
"licenses": {
|
||||||
|
"title": "Открытые лицензии",
|
||||||
|
"licensedUnder": "Лицензировано под ${license}"
|
||||||
|
},
|
||||||
|
"storage": {
|
||||||
|
"removeOldMedia": {
|
||||||
|
"description": "Удалит старые медиафайлы с устройства",
|
||||||
|
"title": "Удалить старые медиафайлы"
|
||||||
|
},
|
||||||
|
"title": "Хранилище",
|
||||||
|
"sizePlaceholder": "Вычисление...",
|
||||||
|
"storageManagement": "Управление хранилищем",
|
||||||
|
"removeOldMediaDialog": {
|
||||||
|
"options": {
|
||||||
|
"all": "Все медиафайлы",
|
||||||
|
"oneWeek": "Старее 1 недели",
|
||||||
|
"oneMonth": "Старее 1 месяца"
|
||||||
|
},
|
||||||
|
"delete": "Удалить",
|
||||||
|
"title": "Удалить медиафайлы",
|
||||||
|
"confirmation": {
|
||||||
|
"body": "Вы точно хотите удалить старые медиафайлы?"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"viewMediaFiles": "Просмотреть медиафайлы",
|
||||||
|
"mediaFiles": "Медиафайлы",
|
||||||
|
"types": {
|
||||||
|
"media": "Медиа",
|
||||||
|
"stickers": "Стикеры"
|
||||||
|
},
|
||||||
|
"storageUsed": "Использование хранилища: ${size}",
|
||||||
|
"manageStickers": "Управлять наборами стикеров"
|
||||||
|
},
|
||||||
|
"stickerPacks": {
|
||||||
|
"title": "Наборы стикеров"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"sharedMedia": {
|
||||||
|
"empty": {
|
||||||
|
"chat": "Нет общих медиафайлов для этого чата",
|
||||||
|
"general": "Нет доступных медиаустройств"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"language": "Русский",
|
||||||
|
"warnings": {
|
||||||
|
"message": {
|
||||||
|
"integrityCheckFailed": "Не удалось проверить целостность файла"
|
||||||
|
},
|
||||||
|
"conversation": {
|
||||||
|
"holdForLonger": "Удерживайте для записи"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"permissions": {
|
||||||
|
"allow": "Разрешить",
|
||||||
|
"skip": "Пропустить"
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
assets/images/empty.png
Normal file
BIN
assets/images/empty.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 67 KiB |
BIN
assets/repo/kofi.png
Normal file
BIN
assets/repo/kofi.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.2 KiB |
@@ -5,3 +5,5 @@ targets:
|
|||||||
options:
|
options:
|
||||||
input_directory: assets/i18n
|
input_directory: assets/i18n
|
||||||
output_directory: lib/i18n
|
output_directory: lib/i18n
|
||||||
|
fallback_strategy: base_locale
|
||||||
|
base_locale: en
|
||||||
|
|||||||
43
docs/stickerpacks.md
Normal file
43
docs/stickerpacks.md
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
# Sticker Packs
|
||||||
|
|
||||||
|
Moxxy supports sending and receiving sticker packs using XEP-0449 version 0.1.1. Sticker
|
||||||
|
packs can also be imported using a Moxxy specific format.
|
||||||
|
|
||||||
|
## File Format
|
||||||
|
|
||||||
|
A Moxxy sticker pack is a flat tar archive that contains the following files:
|
||||||
|
|
||||||
|
- `urn.xmpp.stickers.0.xml`
|
||||||
|
- The sticker files
|
||||||
|
|
||||||
|
### `urn.xmpp.stickers.0.xml`
|
||||||
|
|
||||||
|
This file is the sticker pack's metadata file. It describes the sticker pack the same
|
||||||
|
way as the examples in XEP-0449 do. There are, however, some differences:
|
||||||
|
|
||||||
|
- Each `<file />` element must contain a `<name />` element that matches with a file in the tar archive
|
||||||
|
- Each sticker MUST contain at least one HTTP(s) source
|
||||||
|
- The `<hash />` of the `<pack />` element is ignored as Moxxy computes it itself, so it can be omitted
|
||||||
|
|
||||||
|
An example for the metadata file is the following:
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<pack xmlns='urn:xmpp:stickers:0'>
|
||||||
|
<name>Example</name>
|
||||||
|
<summary>Example sticker pack.</summary>
|
||||||
|
<item>
|
||||||
|
<file xmlns='urn:xmpp:file:metadata:0'>
|
||||||
|
<media-type>image/png</media-type>
|
||||||
|
<desc>:some-sticker:</desc>
|
||||||
|
<name>suprise.png</name>
|
||||||
|
<size>531910</size>
|
||||||
|
<dimensions>1030x1030</dimensions>
|
||||||
|
<hash xmlns='urn:xmpp:hashes:2' algo='sha-256'>1Ha4okUGNRAA04KibwWUmklqqBqdhg7+20dfsr/wLik=</hash>
|
||||||
|
</file>
|
||||||
|
<sources xmlns='urn:xmpp:sfs:0'>
|
||||||
|
<url-data xmlns='http://jabber.org/protocol/url-data' target='...' />
|
||||||
|
</sources>
|
||||||
|
</item>
|
||||||
|
<!-- ... -->
|
||||||
|
</pack>
|
||||||
|
```
|
||||||
1
fastlane/metadata/android/de-DE/short_description.txt
Normal file
1
fastlane/metadata/android/de-DE/short_description.txt
Normal file
@@ -0,0 +1 @@
|
|||||||
|
Moxxy ist ein experimenteller XMPP-Client, der modern und einfach sein soll.
|
||||||
1
fastlane/metadata/android/de-DE/title.txt
Normal file
1
fastlane/metadata/android/de-DE/title.txt
Normal file
@@ -0,0 +1 @@
|
|||||||
|
Moxxy
|
||||||
12
fastlane/metadata/android/en-US/changelogs/11.txt
Normal file
12
fastlane/metadata/android/en-US/changelogs/11.txt
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
Many changes in this release are under the hood, but there are many changes nonetheless:
|
||||||
|
|
||||||
|
- Messages that are sent while offline are now queued up until we're online again
|
||||||
|
- Moxxy now makes use of SFS's caching possibilities. Receiving files sent via SFS are thus only downloaded if the file is not already locally available
|
||||||
|
- Messages and shared media files are now shown in paged lists
|
||||||
|
- Reworked various pages, like the Conversation page and the profile page
|
||||||
|
- Rework the reactions UI
|
||||||
|
- Add a "note to self" feature. This was a teaser task in the context of this year's GSoC
|
||||||
|
- Chat states are no longer sent if a chat is no longer focused
|
||||||
|
- Sending a sticker when a message is selected for quoting, the sticker is sent as a reply to that message
|
||||||
|
- The database design was massively overhauled
|
||||||
|
- The emoji/sticker picker should no longer jump around when switching from the keyboard
|
||||||
7
fastlane/metadata/android/en-US/changelogs/12.txt
Normal file
7
fastlane/metadata/android/en-US/changelogs/12.txt
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
This is a hotfix release.
|
||||||
|
|
||||||
|
Sending a message with no attached file results in a gray
|
||||||
|
box being displayed over the entire message list. This release
|
||||||
|
contains a fix for that.
|
||||||
|
|
||||||
|
(I also dropped my fork of the Flutter SDK)
|
||||||
13
fastlane/metadata/android/en-US/changelogs/13.txt
Normal file
13
fastlane/metadata/android/en-US/changelogs/13.txt
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
- (Hopefully) fix OMEMO between two Moxxy clients.
|
||||||
|
- Allow correcting messages older than the last one. Whether all clients will accept such a correction is unclear.
|
||||||
|
- Add (incomplete) translations for Dutch, French, Galician, Japanese, Polish, and Russian.
|
||||||
|
- Fix having to long-press a message bubble on its corner to active the selection menu.
|
||||||
|
- If enabled, read markers are automatically sent.
|
||||||
|
- Highlight legacy quotes in text messages.
|
||||||
|
- Fix Moxxy's app icon having a badge because of the foreground service.
|
||||||
|
- Make the notifications much prettier and compliant with Android 13.
|
||||||
|
- Prevent Moxxy from crashing on startup on a fresh device.
|
||||||
|
- Video thumbnails are now generated, if possible, after a video has been downloaded.
|
||||||
|
- The Moxxy APKs will now be signed by a different key stored on my YubiKey. You will have to uninstall and reinstall Moxxy. This will remove all your data.
|
||||||
|
- Moxxy now uses Android's direct share shortcuts.
|
||||||
|
- Moxxy now uses Android 13's new photo picker, whenever possible. This should allow Moxxy to require fewer permissions to work.
|
||||||
7
fastlane/metadata/android/en-US/changelogs/9.txt
Normal file
7
fastlane/metadata/android/en-US/changelogs/9.txt
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
* Expose the debug menu by tapping the Moxxy icon on the about page 10 times
|
||||||
|
* Maybe fix a connection race condition
|
||||||
|
* Allow sharing media with the app when it was closed
|
||||||
|
* Make quotes prettier
|
||||||
|
* Make the bottom part of the conversation page prettier
|
||||||
|
* Fix roster fetching
|
||||||
|
* Fix OMEMO key generation
|
||||||
@@ -10,12 +10,14 @@ Currently supported features include:
|
|||||||
<li>Typing indicators and message markers</li>
|
<li>Typing indicators and message markers</li>
|
||||||
<li>Chat backgrounds</li>
|
<li>Chat backgrounds</li>
|
||||||
<li>Runs in the background without Push Notifications</li>
|
<li>Runs in the background without Push Notifications</li>
|
||||||
|
<li>OMEMO (Currently not compatible with most apps)</li>
|
||||||
|
<li>Stickers</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
For the best experience, I recommend a server that:
|
For the best experience, I recommend a server that:
|
||||||
<ul>
|
<ul>
|
||||||
<li>Supports direct TLS/StartTLS on the same domain as in the Jid</li>
|
<li>Supports direct TLS/StartTLS on the same domain as in the Jid</li>
|
||||||
<li>Supports SCRAM-SHA-1 or SCRAM-SHA-256</li>
|
<li>Supports SCRAM-SHA-1, SCRAM-SHA-256 or SCRAM-SHA-512</li>
|
||||||
<li>Supports HTTP File Upload</li>
|
<li>Supports HTTP File Upload</li>
|
||||||
<li>Supports Stream Management</li>
|
<li>Supports Stream Management</li>
|
||||||
<li>Supports Client State Indication</li>
|
<li>Supports Client State Indication</li>
|
||||||
|
|||||||
1
fastlane/metadata/android/fr-FR/short_description.txt
Normal file
1
fastlane/metadata/android/fr-FR/short_description.txt
Normal file
@@ -0,0 +1 @@
|
|||||||
|
Moxxy est un client XMPP expérimental qui vise d’être moderne et facile.
|
||||||
1
fastlane/metadata/android/fr-FR/title.txt
Normal file
1
fastlane/metadata/android/fr-FR/title.txt
Normal file
@@ -0,0 +1 @@
|
|||||||
|
Moxxy
|
||||||
10
fastlane/metadata/android/nl-NL/changelogs/11.txt
Normal file
10
fastlane/metadata/android/nl-NL/changelogs/11.txt
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
- Offline-berichten worden verstuurd als de verbinding hersteld is;
|
||||||
|
- SFS-cache, waardoor downloaden alleen plaatsvindt indien niet lokaal beschikbaar;
|
||||||
|
- Berichten en mediabestanden worden op pagina's getoond;
|
||||||
|
- Diverse pagina's bijgewerkt;
|
||||||
|
- Reacties herontworpen;
|
||||||
|
- Zelfmemofunctie;
|
||||||
|
- Gespreksstatussen worden niet meer verstuurd indien ongefocust;
|
||||||
|
- Stickers als antwoord op citaten;
|
||||||
|
- Nieuw databankontwerp;
|
||||||
|
- Verbeterde emoji-/stickerkeuze i.c.m. toetsenbord.
|
||||||
5
fastlane/metadata/android/nl-NL/changelogs/12.txt
Normal file
5
fastlane/metadata/android/nl-NL/changelogs/12.txt
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
Dit is een oplossingsversie:
|
||||||
|
|
||||||
|
Het versturen van een bericht zonder bijlage zorgde voor een grijs vlak op de berichtenlijst. Dat is nu opgelost.
|
||||||
|
|
||||||
|
(Ook ben ik gestopt met de ontwikkeling van mijn afsplitsing van de Flutter-sdk.)
|
||||||
5
fastlane/metadata/android/nl-NL/changelogs/13.txt
Normal file
5
fastlane/metadata/android/nl-NL/changelogs/13.txt
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
-(Hopelijk) Oplossing voor OMEMO tussen twee Moxxy-clients;
|
||||||
|
-Oplossing voor het lang ingedrukt houden van een bericht om het keuzemenu te openen;
|
||||||
|
-Leesmarkeringen worden voortaan automatisch verzonden (indien ingeschakeld);
|
||||||
|
-Nieuw: (onvolledige) Nederlandse, Japanse en Russische vertalingen;
|
||||||
|
-Nieuw: bewerken van berichten ouder dan het recentste bericht. Onduidelijk of alle clients dit op de juiste manier tonen.
|
||||||
7
fastlane/metadata/android/nl-NL/changelogs/9.txt
Normal file
7
fastlane/metadata/android/nl-NL/changelogs/9.txt
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
* Expose the debug menu by tapping the Moxxy icon on the about page 10 times
|
||||||
|
* Maybe fix a connection race condition
|
||||||
|
* Allow sharing media with the app when it was closed
|
||||||
|
* Make quotes prettier
|
||||||
|
* Make the bottom part of the conversation page prettier
|
||||||
|
* Fix roster fetching
|
||||||
|
* Fix OMEMO key generation
|
||||||
24
fastlane/metadata/android/nl-NL/full_description.txt
Normal file
24
fastlane/metadata/android/nl-NL/full_description.txt
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
Moxxy is een experimentele xmpp-client met als doel modern gebruiksgemak.
|
||||||
|
|
||||||
|
Let op: Moxxy is momenteel in de alfafase. Dit houdt in dat er gegarandeerd bugs en
|
||||||
|
problemen zullen zijn. Gebruik Moxxy dus niet voor belangrijke zaken.
|
||||||
|
|
||||||
|
Huidige functies:
|
||||||
|
<ul>
|
||||||
|
<li>Verstuur bestanden en afbeeldingen;</li>
|
||||||
|
<li>Stel je profielfoto in;</li>
|
||||||
|
<li>Typmeldingen en berichtstatussen;</li>
|
||||||
|
<li>Gespreksachtergronden;</li>
|
||||||
|
<li>Draait op de achtergrond zónder pushmeldingen;</li>
|
||||||
|
<li>OMEMO (momenteel niet compatibel met de meeste apps);</li>
|
||||||
|
<li>Stickers.</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
Voor de beste gebruikservaring is het belangrijk om een server te gebruiken met:
|
||||||
|
<ul>
|
||||||
|
<li>Ondersteuning voor TLS/StartTLS op dezelfde domeinnaam als in de Jid;</li>
|
||||||
|
<li>Ondersteuning voor SCRAM-SHA-1, SCRAM-SHA-256 of SCRAM-SHA-512;</li>
|
||||||
|
<li>Ondersteuning voor HTTP-bestandsupload;</li>
|
||||||
|
<li>Ondersteuning voor streambeheer;</li>
|
||||||
|
<li>Ondersteuning voor Client State Indication.</li>
|
||||||
|
</ul>
|
||||||
1
fastlane/metadata/android/nl-NL/short_description.txt
Normal file
1
fastlane/metadata/android/nl-NL/short_description.txt
Normal file
@@ -0,0 +1 @@
|
|||||||
|
Moxxy is een experimentele xmpp-client met als doel modern gebruiksgemak.
|
||||||
1
fastlane/metadata/android/nl-NL/title.txt
Normal file
1
fastlane/metadata/android/nl-NL/title.txt
Normal file
@@ -0,0 +1 @@
|
|||||||
|
Moxxy
|
||||||
186
flake.lock
generated
186
flake.lock
generated
@@ -1,6 +1,103 @@
|
|||||||
{
|
{
|
||||||
"nodes": {
|
"nodes": {
|
||||||
|
"android-nixpkgs": {
|
||||||
|
"inputs": {
|
||||||
|
"devshell": "devshell",
|
||||||
|
"flake-utils": "flake-utils",
|
||||||
|
"nixpkgs": "nixpkgs"
|
||||||
|
},
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1689798050,
|
||||||
|
"narHash": "sha256-ZyFPra7N0MF803o55dYQQyX9b/BmXr6QTCyN7slRThY=",
|
||||||
|
"owner": "tadfisher",
|
||||||
|
"repo": "android-nixpkgs",
|
||||||
|
"rev": "9aa0e2990da86de8ca203af313668851dcb9ea6e",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "tadfisher",
|
||||||
|
"repo": "android-nixpkgs",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"bab": {
|
||||||
|
"inputs": {
|
||||||
|
"flake-utils": "flake-utils_2",
|
||||||
|
"nixpkgs": "nixpkgs_2"
|
||||||
|
},
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1694100130,
|
||||||
|
"narHash": "sha256-3xQgPgFNVtuftoYsUxtUTFu/P5ZzcIaRIrLwNs4xrBg=",
|
||||||
|
"ref": "refs/heads/master",
|
||||||
|
"rev": "8e98e366f7de0d8636a387ee857ead7cc8c1b646",
|
||||||
|
"revCount": 6,
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://codeberg.org/PapaTutuWawa/bits-and-bytes.git"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://codeberg.org/PapaTutuWawa/bits-and-bytes.git"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"devshell": {
|
||||||
|
"inputs": {
|
||||||
|
"nixpkgs": [
|
||||||
|
"android-nixpkgs",
|
||||||
|
"nixpkgs"
|
||||||
|
],
|
||||||
|
"systems": "systems"
|
||||||
|
},
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1688380630,
|
||||||
|
"narHash": "sha256-8ilApWVb1mAi4439zS3iFeIT0ODlbrifm/fegWwgHjA=",
|
||||||
|
"owner": "numtide",
|
||||||
|
"repo": "devshell",
|
||||||
|
"rev": "f9238ec3d75cefbb2b42a44948c4e8fb1ae9a205",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "numtide",
|
||||||
|
"repo": "devshell",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
"flake-utils": {
|
"flake-utils": {
|
||||||
|
"inputs": {
|
||||||
|
"systems": "systems_2"
|
||||||
|
},
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1689068808,
|
||||||
|
"narHash": "sha256-6ixXo3wt24N/melDWjq70UuHQLxGV8jZvooRanIHXw0=",
|
||||||
|
"owner": "numtide",
|
||||||
|
"repo": "flake-utils",
|
||||||
|
"rev": "919d646de7be200f3bf08cb76ae1f09402b6f9b4",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "numtide",
|
||||||
|
"repo": "flake-utils",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"flake-utils_2": {
|
||||||
|
"inputs": {
|
||||||
|
"systems": "systems_3"
|
||||||
|
},
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1689068808,
|
||||||
|
"narHash": "sha256-6ixXo3wt24N/melDWjq70UuHQLxGV8jZvooRanIHXw0=",
|
||||||
|
"owner": "numtide",
|
||||||
|
"repo": "flake-utils",
|
||||||
|
"rev": "919d646de7be200f3bf08cb76ae1f09402b6f9b4",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "numtide",
|
||||||
|
"repo": "flake-utils",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"flake-utils_3": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1667395993,
|
"lastModified": 1667395993,
|
||||||
"narHash": "sha256-nuEHfE/LcWyuSWnS8t12N1wc105Qtau+/OdUAjtQ0rA=",
|
"narHash": "sha256-nuEHfE/LcWyuSWnS8t12N1wc105Qtau+/OdUAjtQ0rA=",
|
||||||
@@ -17,11 +114,43 @@
|
|||||||
},
|
},
|
||||||
"nixpkgs": {
|
"nixpkgs": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1669165918,
|
"lastModified": 1689679375,
|
||||||
"narHash": "sha256-hIVruk2+0wmw/Kfzy11rG3q7ev3VTi/IKVODeHcVjFo=",
|
"narHash": "sha256-LHUC52WvyVDi9PwyL1QCpaxYWBqp4ir4iL6zgOkmcb8=",
|
||||||
"owner": "NixOS",
|
"owner": "NixOS",
|
||||||
"repo": "nixpkgs",
|
"repo": "nixpkgs",
|
||||||
"rev": "3b400a525d92e4085e46141ff48cbf89fd89739e",
|
"rev": "684c17c429c42515bafb3ad775d2a710947f3d67",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "NixOS",
|
||||||
|
"ref": "nixos-unstable",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"nixpkgs_2": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1689935543,
|
||||||
|
"narHash": "sha256-6GQ9ib4dA/r1leC5VUpsBo0BmDvNxLjKrX1iyL+h8mc=",
|
||||||
|
"owner": "NixOS",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"rev": "e43e2448161c0a2c4928abec4e16eae1516571bc",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "NixOS",
|
||||||
|
"ref": "nixpkgs-unstable",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"nixpkgs_3": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1689752456,
|
||||||
|
"narHash": "sha256-VOChdECcEI8ixz8QY+YC4JaNEFwQd1V8bA0G4B28Ki0=",
|
||||||
|
"owner": "NixOS",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"rev": "7f256d7da238cb627ef189d56ed590739f42f13b",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
@@ -33,8 +162,55 @@
|
|||||||
},
|
},
|
||||||
"root": {
|
"root": {
|
||||||
"inputs": {
|
"inputs": {
|
||||||
"flake-utils": "flake-utils",
|
"android-nixpkgs": "android-nixpkgs",
|
||||||
"nixpkgs": "nixpkgs"
|
"bab": "bab",
|
||||||
|
"flake-utils": "flake-utils_3",
|
||||||
|
"nixpkgs": "nixpkgs_3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"systems": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1681028828,
|
||||||
|
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
||||||
|
"owner": "nix-systems",
|
||||||
|
"repo": "default",
|
||||||
|
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "nix-systems",
|
||||||
|
"repo": "default",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"systems_2": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1681028828,
|
||||||
|
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
||||||
|
"owner": "nix-systems",
|
||||||
|
"repo": "default",
|
||||||
|
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "nix-systems",
|
||||||
|
"repo": "default",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"systems_3": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1681028828,
|
||||||
|
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
||||||
|
"owner": "nix-systems",
|
||||||
|
"repo": "default",
|
||||||
|
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "nix-systems",
|
||||||
|
"repo": "default",
|
||||||
|
"type": "github"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
107
flake.nix
107
flake.nix
@@ -3,33 +3,50 @@
|
|||||||
inputs = {
|
inputs = {
|
||||||
nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
|
nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
|
||||||
flake-utils.url = "github:numtide/flake-utils";
|
flake-utils.url = "github:numtide/flake-utils";
|
||||||
|
android-nixpkgs.url = "github:tadfisher/android-nixpkgs";
|
||||||
|
bab.url = "git+https://codeberg.org/PapaTutuWawa/bits-and-bytes.git";
|
||||||
};
|
};
|
||||||
|
|
||||||
outputs = { self, nixpkgs, flake-utils }: flake-utils.lib.eachDefaultSystem (system: let
|
outputs = { self, nixpkgs, flake-utils, android-nixpkgs, bab }: flake-utils.lib.eachDefaultSystem (system: let
|
||||||
pkgs = import nixpkgs {
|
pkgs = import nixpkgs {
|
||||||
inherit system;
|
inherit system;
|
||||||
config = {
|
config = {
|
||||||
android_sdk.accept_license = true;
|
android_sdk.accept_license = true;
|
||||||
allowUnfree = true;
|
allowUnfree = true;
|
||||||
|
|
||||||
|
# Fix to allow building the NDK package
|
||||||
|
# TODO: Remove once https://github.com/tadfisher/android-nixpkgs/issues/62 is resolved
|
||||||
|
permittedInsecurePackages = [
|
||||||
|
"python-2.7.18.6"
|
||||||
|
];
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
android = pkgs.androidenv.composeAndroidPackages {
|
# Everything to make Flutter happy
|
||||||
# TODO: Find a way to pin these
|
sdk = android-nixpkgs.sdk.${system} (sdkPkgs: with sdkPkgs; [
|
||||||
#toolsVersion = "26.1.1";
|
cmdline-tools-latest
|
||||||
#platformToolsVersion = "31.0.3";
|
build-tools-30-0-3
|
||||||
#buildToolsVersions = [ "31.0.0" ];
|
build-tools-33-0-2
|
||||||
#includeEmulator = true;
|
build-tools-34-0-0
|
||||||
#emulatorVersion = "30.6.3";
|
platform-tools
|
||||||
platformVersions = [ "28" ];
|
emulator
|
||||||
includeSources = false;
|
patcher-v4
|
||||||
includeSystemImages = true;
|
platforms-android-28
|
||||||
systemImageTypes = [ "default" ];
|
platforms-android-29
|
||||||
abiVersions = [ "x86_64" ];
|
platforms-android-30
|
||||||
includeNDK = false;
|
platforms-android-31
|
||||||
useGoogleAPIs = false;
|
platforms-android-33
|
||||||
useGoogleTVAddOns = false;
|
|
||||||
};
|
# For flutter_zxing
|
||||||
pinnedJDK = pkgs.jdk;
|
cmake-3-18-1
|
||||||
|
#ndk-21-4-7075529
|
||||||
|
(ndk-21-4-7075529.overrideAttrs (old: {
|
||||||
|
buildInputs = old.buildInputs ++ [ pkgs.python27 ];
|
||||||
|
}))
|
||||||
|
]);
|
||||||
|
lib = pkgs.lib;
|
||||||
|
babPkgs = bab.packages."${system}";
|
||||||
|
pinnedJDK = pkgs.jdk17;
|
||||||
|
flutterVersion = pkgs.flutter37;
|
||||||
|
|
||||||
pythonEnv = pkgs.python3.withPackages (ps: with ps; [
|
pythonEnv = pkgs.python3.withPackages (ps: with ps; [
|
||||||
requests pyyaml # For the build scripts
|
requests pyyaml # For the build scripts
|
||||||
@@ -38,13 +55,59 @@
|
|||||||
in {
|
in {
|
||||||
devShell = pkgs.mkShell {
|
devShell = pkgs.mkShell {
|
||||||
buildInputs = with pkgs; [
|
buildInputs = with pkgs; [
|
||||||
flutter pinnedJDK android.platform-tools dart scrcpy # Flutter/Android
|
# Android
|
||||||
pythonEnv gnumake # Build scripts
|
pinnedJDK sdk ktlint
|
||||||
gitlint jq # Code hygiene
|
scrcpy
|
||||||
ripgrep # General utilities
|
|
||||||
|
# Flutter
|
||||||
|
flutterVersion
|
||||||
|
|
||||||
|
# Build scripts
|
||||||
|
pythonEnv gnumake
|
||||||
|
|
||||||
|
# Code hygiene
|
||||||
|
gitlint jq
|
||||||
];
|
];
|
||||||
|
|
||||||
|
ANDROID_SDK_ROOT = "${sdk}/share/android-sdk";
|
||||||
|
ANDROID_HOME = "${sdk}/share/android-sdk";
|
||||||
JAVA_HOME = pinnedJDK;
|
JAVA_HOME = pinnedJDK;
|
||||||
|
|
||||||
|
# Fix an issue with Flutter using an older version of aapt2, which does not know
|
||||||
|
# an used parameter.
|
||||||
|
GRADLE_OPTS = "-Dorg.gradle.project.android.aapt2FromMavenOverride=${sdk}/share/android-sdk/build-tools/34.0.0/aapt2";
|
||||||
|
};
|
||||||
|
|
||||||
|
apps = let
|
||||||
|
providerArg = pkgs.writeText "provider-arg.cfg" ''
|
||||||
|
name = OpenSC-PKCS11
|
||||||
|
description = SunPKCS11 via OpenSC
|
||||||
|
library = ${pkgs.opensc}/lib/opensc-pkcs11.so
|
||||||
|
slotListIndex = 0
|
||||||
|
'';
|
||||||
|
mkBuildScript = skipBuild: pkgs.writeShellScript "build-moxxy.sh" ''
|
||||||
|
${babPkgs.flutter-build}/bin/flutter-build \
|
||||||
|
--name Moxxy \
|
||||||
|
--not-signed \
|
||||||
|
--zipalign ${sdk}/share/android-sdk/build-tools/34.0.0/zipalign \
|
||||||
|
--apksigner ${sdk}/share/android-sdk/build-tools/34.0.0/apksigner \
|
||||||
|
--pigeon ./pigeon/quirks.dart \
|
||||||
|
--flutter ${flutterVersion}/bin/flutter \
|
||||||
|
--dart ${flutterVersion}/bin/dart \
|
||||||
|
--provider-config ${providerArg} ${lib.optionalString skipBuild "--skip-build"}
|
||||||
|
'';
|
||||||
|
in {
|
||||||
|
# Skip the build and just sign
|
||||||
|
onlySign = {
|
||||||
|
type = "app";
|
||||||
|
program = "${mkBuildScript true}";
|
||||||
|
};
|
||||||
|
|
||||||
|
# Build everything and sign
|
||||||
|
build = {
|
||||||
|
type = "app";
|
||||||
|
program = "${mkBuildScript false}";
|
||||||
|
};
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,67 +0,0 @@
|
|||||||
import 'package:get_it/get_it.dart';
|
|
||||||
import 'package:logging/logging.dart';
|
|
||||||
import 'package:moxxmpp/moxxmpp.dart';
|
|
||||||
import 'package:moxxmpp_socket_tcp/moxxmpp_socket_tcp.dart';
|
|
||||||
import 'package:moxxyv2/service/connectivity.dart';
|
|
||||||
import 'package:moxxyv2/service/moxxmpp/reconnect.dart';
|
|
||||||
import 'package:connectivity_plus/connectivity_plus.dart';
|
|
||||||
import 'package:test/test.dart';
|
|
||||||
|
|
||||||
class StubConnectivityService extends ConnectivityService {
|
|
||||||
StubConnectivityService() : super();
|
|
||||||
|
|
||||||
@override
|
|
||||||
ConnectivityResult get currentState => ConnectivityResult.wifi;
|
|
||||||
}
|
|
||||||
|
|
||||||
void main() {
|
|
||||||
Logger.root.level = Level.ALL;
|
|
||||||
Logger.root.onRecord.listen((record) {
|
|
||||||
print('${record.level.name}: ${record.time}: ${record.message}');
|
|
||||||
});
|
|
||||||
final log = Logger('FailureReconnectionTest');
|
|
||||||
GetIt.I.registerSingleton<ConnectivityService>(StubConnectivityService());
|
|
||||||
|
|
||||||
test('Failing an awaited connection with MoxxyReconnectionPolicy', () async {
|
|
||||||
var errors = 0;
|
|
||||||
final connection = XmppConnection(
|
|
||||||
MoxxyReconnectionPolicy(maxBackoffTime: 1),
|
|
||||||
TCPSocketWrapper(false),
|
|
||||||
);
|
|
||||||
connection.registerFeatureNegotiators([
|
|
||||||
StartTlsNegotiator(),
|
|
||||||
]);
|
|
||||||
connection.registerManagers([
|
|
||||||
DiscoManager(),
|
|
||||||
RosterManager(),
|
|
||||||
PingManager(),
|
|
||||||
MessageManager(),
|
|
||||||
PresenceManager('http://moxxmpp.example'),
|
|
||||||
]);
|
|
||||||
connection.asBroadcastStream().listen((event) {
|
|
||||||
if (event is ConnectionStateChangedEvent) {
|
|
||||||
if (event.state == XmppConnectionState.error) {
|
|
||||||
errors++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
connection.setConnectionSettings(
|
|
||||||
ConnectionSettings(
|
|
||||||
jid: JID.fromString('testuser@no-sasl.badxmpp.eu'),
|
|
||||||
password: 'abc123',
|
|
||||||
useDirectTLS: true,
|
|
||||||
allowPlainAuth: true,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
final result = await connection.connectAwaitable();
|
|
||||||
log.info('Connection failed as expected');
|
|
||||||
expect(result.success, false);
|
|
||||||
expect(errors, 1);
|
|
||||||
|
|
||||||
log.info('Waiting 20 seconds for unexpected reconnections');
|
|
||||||
await Future.delayed(const Duration(seconds: 20));
|
|
||||||
expect(errors, 1);
|
|
||||||
}, timeout: Timeout.factor(2));
|
|
||||||
}
|
|
||||||
@@ -22,7 +22,8 @@ files:
|
|||||||
- JsonImplementation
|
- JsonImplementation
|
||||||
attributes:
|
attributes:
|
||||||
state: String
|
state: String
|
||||||
permissionsToRequest: List<int>
|
requestNotificationPermission: bool
|
||||||
|
excludeFromBatteryOptimisation: bool
|
||||||
preferences:
|
preferences:
|
||||||
type: PreferencesState
|
type: PreferencesState
|
||||||
deserialise: true
|
deserialise: true
|
||||||
@@ -36,15 +37,6 @@ files:
|
|||||||
roster:
|
roster:
|
||||||
type: List<RosterItem>?
|
type: List<RosterItem>?
|
||||||
deserialise: true
|
deserialise: true
|
||||||
# Returned by [GetMessagesForJidCommand]
|
|
||||||
- name: MessagesResultEvent
|
|
||||||
extends: BackgroundEvent
|
|
||||||
implements:
|
|
||||||
- JsonImplementation
|
|
||||||
attributes:
|
|
||||||
messages:
|
|
||||||
type: List<Message>
|
|
||||||
deserialise: true
|
|
||||||
# Triggered if a conversation has been added.
|
# Triggered if a conversation has been added.
|
||||||
# Also returned by [AddConversationCommand]
|
# Also returned by [AddConversationCommand]
|
||||||
- name: ConversationAddedEvent
|
- name: ConversationAddedEvent
|
||||||
@@ -71,7 +63,7 @@ files:
|
|||||||
extends: BackgroundEvent
|
extends: BackgroundEvent
|
||||||
implements:
|
implements:
|
||||||
- JsonImplementation
|
- JsonImplementation
|
||||||
# Send by the service if a message has been received or returned by # [SendMessageCommand].
|
# Send by the service if a message has been received or returned by [SendMessageCommand].
|
||||||
- name: MessageAddedEvent
|
- name: MessageAddedEvent
|
||||||
extends: BackgroundEvent
|
extends: BackgroundEvent
|
||||||
implements:
|
implements:
|
||||||
@@ -103,13 +95,20 @@ files:
|
|||||||
extends: BackgroundEvent
|
extends: BackgroundEvent
|
||||||
implements:
|
implements:
|
||||||
- JsonImplementation
|
- JsonImplementation
|
||||||
|
# Triggered in response to a [GetBlocklistCommand]
|
||||||
|
- name: GetBlocklistResultEvent
|
||||||
|
extends: BackgroundEvent
|
||||||
|
implements:
|
||||||
|
- JsonImplementation
|
||||||
|
attributes:
|
||||||
|
entries: List<String>
|
||||||
# Triggered by DownloadService or UploadService.
|
# Triggered by DownloadService or UploadService.
|
||||||
- name: ProgressEvent
|
- name: ProgressEvent
|
||||||
extends: BackgroundEvent
|
extends: BackgroundEvent
|
||||||
implements:
|
implements:
|
||||||
- JsonImplementation
|
- JsonImplementation
|
||||||
attributes:
|
attributes:
|
||||||
id: int
|
id: String
|
||||||
progress: double?
|
progress: double?
|
||||||
# Triggered by [RosterService] if we receive a roster push.
|
# Triggered by [RosterService] if we receive a roster push.
|
||||||
- name: RosterDiffEvent
|
- name: RosterDiffEvent
|
||||||
@@ -163,6 +162,7 @@ files:
|
|||||||
supportsCsi: bool
|
supportsCsi: bool
|
||||||
supportsUserBlocking: bool
|
supportsUserBlocking: bool
|
||||||
supportsHttpFileUpload: bool
|
supportsHttpFileUpload: bool
|
||||||
|
supportsCarbons: bool
|
||||||
# Returned by [SignOutCommand]
|
# Returned by [SignOutCommand]
|
||||||
- name: SignedOutEvent
|
- name: SignedOutEvent
|
||||||
extends: BackgroundEvent
|
extends: BackgroundEvent
|
||||||
@@ -199,6 +199,162 @@ files:
|
|||||||
device:
|
device:
|
||||||
type: OmemoDevice
|
type: OmemoDevice
|
||||||
deserialise: true
|
deserialise: true
|
||||||
|
- name: MessageNotificationTappedEvent
|
||||||
|
extends: BackgroundEvent
|
||||||
|
implements:
|
||||||
|
- JsonImplementation
|
||||||
|
attributes:
|
||||||
|
conversationJid: String
|
||||||
|
title: String
|
||||||
|
avatarPath: String
|
||||||
|
- name: StickerPackImportSuccessEvent
|
||||||
|
extends: BackgroundEvent
|
||||||
|
implements:
|
||||||
|
- JsonImplementation
|
||||||
|
attributes:
|
||||||
|
stickerPack:
|
||||||
|
type: StickerPack
|
||||||
|
deserialise: true
|
||||||
|
- name: StickerPackImportFailureEvent
|
||||||
|
extends: BackgroundEvent
|
||||||
|
implements:
|
||||||
|
- JsonImplementation
|
||||||
|
attributes:
|
||||||
|
- name: FetchStickerPackSuccessResult
|
||||||
|
extends: BackgroundEvent
|
||||||
|
implements:
|
||||||
|
- JsonImplementation
|
||||||
|
attributes:
|
||||||
|
stickerPack:
|
||||||
|
type: StickerPack
|
||||||
|
deserialise: true
|
||||||
|
- name: FetchStickerPackFailureResult
|
||||||
|
extends: BackgroundEvent
|
||||||
|
implements:
|
||||||
|
- JsonImplementation
|
||||||
|
attributes:
|
||||||
|
- name: StickerPackInstallSuccessEvent
|
||||||
|
extends: BackgroundEvent
|
||||||
|
implements:
|
||||||
|
- JsonImplementation
|
||||||
|
attributes:
|
||||||
|
stickerPack:
|
||||||
|
type: StickerPack
|
||||||
|
deserialise: true
|
||||||
|
- name: StickerPackInstallFailureEvent
|
||||||
|
extends: BackgroundEvent
|
||||||
|
implements:
|
||||||
|
- JsonImplementation
|
||||||
|
attributes:
|
||||||
|
- name: StickerPackAddedEvent
|
||||||
|
extends: BackgroundEvent
|
||||||
|
implements:
|
||||||
|
- JsonImplementation
|
||||||
|
attributes:
|
||||||
|
stickerPack:
|
||||||
|
type: StickerPack
|
||||||
|
deserialise: true
|
||||||
|
# Returned by [GetPagedMessagesCommand]
|
||||||
|
- name: PagedMessagesResultEvent
|
||||||
|
extends: BackgroundEvent
|
||||||
|
implements:
|
||||||
|
- JsonImplementation
|
||||||
|
attributes:
|
||||||
|
messages:
|
||||||
|
type: List<Message>
|
||||||
|
deserialise: true
|
||||||
|
# Returned by [GetReactionsForMessageCommand]
|
||||||
|
- name: ReactionsForMessageResult
|
||||||
|
extends: BackgroundEvent
|
||||||
|
implements:
|
||||||
|
- JsonImplementation
|
||||||
|
attributes:
|
||||||
|
reactions:
|
||||||
|
type: List<ReactionGroup>
|
||||||
|
deserialise: true
|
||||||
|
# Triggered when the stream negotiations have been completed
|
||||||
|
- name: StreamNegotiationsCompletedEvent
|
||||||
|
extends: BackgroundEvent
|
||||||
|
implements:
|
||||||
|
- JsonImplementation
|
||||||
|
attributes:
|
||||||
|
resumed: bool
|
||||||
|
- name: AvatarUpdatedEvent
|
||||||
|
extends: BackgroundEvent
|
||||||
|
implements:
|
||||||
|
- JsonImplementation
|
||||||
|
attributes:
|
||||||
|
jid: String
|
||||||
|
path: String
|
||||||
|
# Returned when attempting to start a chat with a groupchat
|
||||||
|
- name: JidIsGroupchatEvent
|
||||||
|
extends: BackgroundEvent
|
||||||
|
implements:
|
||||||
|
- JsonImplementation
|
||||||
|
attributes:
|
||||||
|
jid: String
|
||||||
|
# Returned when an error occured
|
||||||
|
- name: ErrorEvent
|
||||||
|
extends: BackgroundEvent
|
||||||
|
implements:
|
||||||
|
- JsonImplementation
|
||||||
|
attributes:
|
||||||
|
errorId: int
|
||||||
|
# Triggered by the service in response to an [JoinGroupchatCommand].
|
||||||
|
- name: JoinGroupchatResult
|
||||||
|
extends: BackgroundEvent
|
||||||
|
implements:
|
||||||
|
- JsonImplementation
|
||||||
|
attributes:
|
||||||
|
conversation:
|
||||||
|
type: Conversation
|
||||||
|
deserialise: true
|
||||||
|
# Returned after a [GetStorageUsageCommand]
|
||||||
|
- name: GetStorageUsageEvent
|
||||||
|
extends: BackgroundEvent
|
||||||
|
implements:
|
||||||
|
- JsonImplementation
|
||||||
|
attributes:
|
||||||
|
# The used storage in bytes for media files
|
||||||
|
mediaUsage: int
|
||||||
|
# The used storage in bytes for stickers
|
||||||
|
stickerUsage: int
|
||||||
|
# Returned after [DeleteOldMediaFilesCommand]
|
||||||
|
- name: DeleteOldMediaFilesDoneEvent
|
||||||
|
extends: BackgroundEvent
|
||||||
|
implements:
|
||||||
|
- JsonImplementation
|
||||||
|
attributes:
|
||||||
|
# The used storage in bytes after the deletion operation is done
|
||||||
|
newUsage: int
|
||||||
|
# The new list of Conversations
|
||||||
|
conversations:
|
||||||
|
type: List<Conversation>
|
||||||
|
deserialize: true
|
||||||
|
- name: PagedStickerPackResult
|
||||||
|
extends: BackgroundEvent
|
||||||
|
implements:
|
||||||
|
- JsonImplementation
|
||||||
|
attributes:
|
||||||
|
stickerPacks:
|
||||||
|
type: List<StickerPack>
|
||||||
|
deserialise: true
|
||||||
|
- name: GetStickerPackByIdResult
|
||||||
|
extends: BackgroundEvent
|
||||||
|
implements:
|
||||||
|
- JsonImplementation
|
||||||
|
attributes:
|
||||||
|
stickerPack:
|
||||||
|
type: StickerPack?
|
||||||
|
deserialise: true
|
||||||
|
- name: FetchRecipientInformationResult
|
||||||
|
extends: BackgroundEvent
|
||||||
|
implements:
|
||||||
|
- JsonImplementation
|
||||||
|
attributes:
|
||||||
|
items:
|
||||||
|
type: List<SendFilesRecipient>
|
||||||
|
deserialise: true
|
||||||
generate_builder: true
|
generate_builder: true
|
||||||
builder_name: "Event"
|
builder_name: "Event"
|
||||||
builder_baseclass: "BackgroundEvent"
|
builder_baseclass: "BackgroundEvent"
|
||||||
@@ -228,12 +384,7 @@ files:
|
|||||||
lastMessageBody: String
|
lastMessageBody: String
|
||||||
avatarUrl: String
|
avatarUrl: String
|
||||||
jid: String
|
jid: String
|
||||||
- name: GetMessagesForJidCommand
|
conversationType: String
|
||||||
extends: BackgroundCommand
|
|
||||||
implements:
|
|
||||||
- JsonImplementation
|
|
||||||
attributes:
|
|
||||||
jid: String
|
|
||||||
- name: SetOpenConversationCommand
|
- name: SetOpenConversationCommand
|
||||||
extends: BackgroundCommand
|
extends: BackgroundCommand
|
||||||
implements:
|
implements:
|
||||||
@@ -251,6 +402,8 @@ files:
|
|||||||
quotedMessage:
|
quotedMessage:
|
||||||
type: Message?
|
type: Message?
|
||||||
deserialise: true
|
deserialise: true
|
||||||
|
editSid: String?
|
||||||
|
currentConversationJid: String?
|
||||||
- name: SendFilesCommand
|
- name: SendFilesCommand
|
||||||
extends: BackgroundCommand
|
extends: BackgroundCommand
|
||||||
implements:
|
implements:
|
||||||
@@ -294,6 +447,12 @@ files:
|
|||||||
- JsonImplementation
|
- JsonImplementation
|
||||||
attributes:
|
attributes:
|
||||||
jid: String
|
jid: String
|
||||||
|
- name: RemoveContactCommand
|
||||||
|
extends: BackgroundCommand
|
||||||
|
implements:
|
||||||
|
- JsonImplementation
|
||||||
|
attributes:
|
||||||
|
jid: String
|
||||||
- name: RequestDownloadCommand
|
- name: RequestDownloadCommand
|
||||||
extends: BackgroundCommand
|
extends: BackgroundCommand
|
||||||
implements:
|
implements:
|
||||||
@@ -322,6 +481,12 @@ files:
|
|||||||
- JsonImplementation
|
- JsonImplementation
|
||||||
attributes:
|
attributes:
|
||||||
jid: String
|
jid: String
|
||||||
|
- name: ExitConversationCommand
|
||||||
|
extends: BackgroundCommand
|
||||||
|
implements:
|
||||||
|
- JsonImplementation
|
||||||
|
attributes:
|
||||||
|
conversationType: String
|
||||||
- name: SendChatStateCommand
|
- name: SendChatStateCommand
|
||||||
extends: BackgroundCommand
|
extends: BackgroundCommand
|
||||||
implements:
|
implements:
|
||||||
@@ -329,6 +494,7 @@ files:
|
|||||||
attributes:
|
attributes:
|
||||||
state: String
|
state: String
|
||||||
jid: String
|
jid: String
|
||||||
|
conversationType: String
|
||||||
- name: GetFeaturesCommand
|
- name: GetFeaturesCommand
|
||||||
extends: BackgroundCommand
|
extends: BackgroundCommand
|
||||||
implements:
|
implements:
|
||||||
@@ -387,13 +553,166 @@ files:
|
|||||||
implements:
|
implements:
|
||||||
- JsonImplementation
|
- JsonImplementation
|
||||||
attributes:
|
attributes:
|
||||||
- name: RetractMessageComment
|
- name: RetractMessageCommentCommand
|
||||||
extends: BackgroundCommand
|
extends: BackgroundCommand
|
||||||
implements:
|
implements:
|
||||||
- JsonImplementation
|
- JsonImplementation
|
||||||
attributes:
|
attributes:
|
||||||
originId: String
|
originId: String
|
||||||
conversationJid: String
|
conversationJid: String
|
||||||
|
- name: MarkConversationAsReadCommand
|
||||||
|
extends: BackgroundCommand
|
||||||
|
implements:
|
||||||
|
- JsonImplementation
|
||||||
|
attributes:
|
||||||
|
conversationJid: String
|
||||||
|
- name: MarkMessageAsReadCommand
|
||||||
|
extends: BackgroundCommand
|
||||||
|
implements:
|
||||||
|
- JsonImplementation
|
||||||
|
attributes:
|
||||||
|
id: String
|
||||||
|
sendMarker: bool
|
||||||
|
- name: AddReactionToMessageCommand
|
||||||
|
extends: BackgroundCommand
|
||||||
|
implements:
|
||||||
|
- JsonImplementation
|
||||||
|
attributes:
|
||||||
|
id: String
|
||||||
|
emoji: String
|
||||||
|
- name: RemoveReactionFromMessageCommand
|
||||||
|
extends: BackgroundCommand
|
||||||
|
implements:
|
||||||
|
- JsonImplementation
|
||||||
|
attributes:
|
||||||
|
id: String
|
||||||
|
emoji: String
|
||||||
|
- name: MarkOmemoDeviceAsVerifiedCommand
|
||||||
|
extends: BackgroundCommand
|
||||||
|
implements:
|
||||||
|
- JsonImplementation
|
||||||
|
attributes:
|
||||||
|
deviceId: int
|
||||||
|
jid: String
|
||||||
|
- name: ImportStickerPackCommand
|
||||||
|
extends: BackgroundCommand
|
||||||
|
implements:
|
||||||
|
- JsonImplementation
|
||||||
|
attributes:
|
||||||
|
path: String
|
||||||
|
- name: RemoveStickerPackCommand
|
||||||
|
extends: BackgroundCommand
|
||||||
|
implements:
|
||||||
|
- JsonImplementation
|
||||||
|
attributes:
|
||||||
|
stickerPackId: String
|
||||||
|
- name: SendStickerCommand
|
||||||
|
extends: BackgroundCommand
|
||||||
|
implements:
|
||||||
|
- JsonImplementation
|
||||||
|
attributes:
|
||||||
|
sticker:
|
||||||
|
type: Sticker
|
||||||
|
deserialise: true
|
||||||
|
recipient: String
|
||||||
|
quotes:
|
||||||
|
type: Message?
|
||||||
|
deserialise: true
|
||||||
|
- name: FetchStickerPackCommand
|
||||||
|
extends: BackgroundCommand
|
||||||
|
implements:
|
||||||
|
- JsonImplementation
|
||||||
|
attributes:
|
||||||
|
stickerPackId: String
|
||||||
|
jid: String
|
||||||
|
- name: InstallStickerPackCommand
|
||||||
|
extends: BackgroundCommand
|
||||||
|
implements:
|
||||||
|
- JsonImplementation
|
||||||
|
attributes:
|
||||||
|
stickerPack:
|
||||||
|
type: StickerPack
|
||||||
|
deserialise: true
|
||||||
|
- name: GetBlocklistCommand
|
||||||
|
extends: BackgroundCommand
|
||||||
|
implements:
|
||||||
|
- JsonImplementation
|
||||||
|
attributes:
|
||||||
|
- name: GetPagedMessagesCommand
|
||||||
|
extends: BackgroundCommand
|
||||||
|
implements:
|
||||||
|
- JsonImplementation
|
||||||
|
attributes:
|
||||||
|
conversationJid: String
|
||||||
|
olderThan: bool
|
||||||
|
timestamp: int?
|
||||||
|
- name: GetPagedSharedMediaCommand
|
||||||
|
extends: BackgroundCommand
|
||||||
|
implements:
|
||||||
|
- JsonImplementation
|
||||||
|
attributes:
|
||||||
|
conversationJid: String?
|
||||||
|
olderThan: bool
|
||||||
|
timestamp: int?
|
||||||
|
- name: GetReactionsForMessageCommand
|
||||||
|
extends: BackgroundCommand
|
||||||
|
implements:
|
||||||
|
- JsonImplementation
|
||||||
|
attributes:
|
||||||
|
id: String
|
||||||
|
- name: RequestAvatarForJidCommand
|
||||||
|
extends: BackgroundCommand
|
||||||
|
implements:
|
||||||
|
- JsonImplementation
|
||||||
|
attributes:
|
||||||
|
jid: String
|
||||||
|
hash: String?
|
||||||
|
ownAvatar: bool
|
||||||
|
- name: GetStorageUsageCommand
|
||||||
|
extends: BackgroundCommand
|
||||||
|
implements:
|
||||||
|
- JsonImplementation
|
||||||
|
- name: DeleteOldMediaFilesCommand
|
||||||
|
extends: BackgroundCommand
|
||||||
|
implements:
|
||||||
|
- JsonImplementation
|
||||||
|
attributes:
|
||||||
|
# Milliseconds from now in the past; The maximum age of a file to not
|
||||||
|
# get deleted.
|
||||||
|
timeOffset: int
|
||||||
|
- name: GetPagedStickerPackCommand
|
||||||
|
extends: BackgroundCommand
|
||||||
|
implements:
|
||||||
|
- JsonImplementation
|
||||||
|
attributes:
|
||||||
|
olderThan: bool
|
||||||
|
timestamp: int?
|
||||||
|
includeStickers: bool
|
||||||
|
- name: GetStickerPackByIdCommand
|
||||||
|
extends: BackgroundCommand
|
||||||
|
implements:
|
||||||
|
- JsonImplementation
|
||||||
|
attributes:
|
||||||
|
id: String
|
||||||
|
- name: DebugCommand
|
||||||
|
extends: BackgroundCommand
|
||||||
|
implements:
|
||||||
|
- JsonImplementation
|
||||||
|
attributes:
|
||||||
|
id: int
|
||||||
|
- name: JoinGroupchatCommand
|
||||||
|
extends: BackgroundCommand
|
||||||
|
implements:
|
||||||
|
- JsonImplementation
|
||||||
|
attributes:
|
||||||
|
jid: String
|
||||||
|
nick: String
|
||||||
|
- name: FetchRecipientInformationCommand
|
||||||
|
extends: BackgroundCommand
|
||||||
|
implements:
|
||||||
|
- JsonImplementation
|
||||||
|
attributes:
|
||||||
|
jids: List<String>
|
||||||
generate_builder: true
|
generate_builder: true
|
||||||
# get${builder_Name}FromJson
|
# get${builder_Name}FromJson
|
||||||
builder_name: "Command"
|
builder_name: "Command"
|
||||||
|
|||||||
247
lib/main.dart
247
lib/main.dart
@@ -9,30 +9,34 @@ import 'package:moxplatform/moxplatform.dart';
|
|||||||
import 'package:moxxyv2/i18n/strings.g.dart';
|
import 'package:moxxyv2/i18n/strings.g.dart';
|
||||||
import 'package:moxxyv2/service/service.dart';
|
import 'package:moxxyv2/service/service.dart';
|
||||||
import 'package:moxxyv2/shared/commands.dart';
|
import 'package:moxxyv2/shared/commands.dart';
|
||||||
import 'package:moxxyv2/ui/bloc/addcontact_bloc.dart';
|
import 'package:moxxyv2/shared/synchronized_queue.dart';
|
||||||
import 'package:moxxyv2/ui/bloc/blocklist_bloc.dart';
|
import 'package:moxxyv2/ui/bloc/blocklist_bloc.dart';
|
||||||
import 'package:moxxyv2/ui/bloc/conversation_bloc.dart';
|
import 'package:moxxyv2/ui/bloc/conversation_bloc.dart';
|
||||||
import 'package:moxxyv2/ui/bloc/conversations_bloc.dart';
|
import 'package:moxxyv2/ui/bloc/conversations_bloc.dart';
|
||||||
import 'package:moxxyv2/ui/bloc/crop_bloc.dart';
|
import 'package:moxxyv2/ui/bloc/crop_bloc.dart';
|
||||||
import 'package:moxxyv2/ui/bloc/cropbackground_bloc.dart';
|
import 'package:moxxyv2/ui/bloc/cropbackground_bloc.dart';
|
||||||
import 'package:moxxyv2/ui/bloc/devices_bloc.dart';
|
import 'package:moxxyv2/ui/bloc/devices_bloc.dart';
|
||||||
|
import 'package:moxxyv2/ui/bloc/groupchat/joingroupchat_bloc.dart';
|
||||||
import 'package:moxxyv2/ui/bloc/login_bloc.dart';
|
import 'package:moxxyv2/ui/bloc/login_bloc.dart';
|
||||||
import 'package:moxxyv2/ui/bloc/navigation_bloc.dart';
|
import 'package:moxxyv2/ui/bloc/navigation_bloc.dart';
|
||||||
import 'package:moxxyv2/ui/bloc/newconversation_bloc.dart';
|
import 'package:moxxyv2/ui/bloc/newconversation_bloc.dart';
|
||||||
import 'package:moxxyv2/ui/bloc/own_devices_bloc.dart';
|
import 'package:moxxyv2/ui/bloc/own_devices_bloc.dart';
|
||||||
import 'package:moxxyv2/ui/bloc/preferences_bloc.dart';
|
import 'package:moxxyv2/ui/bloc/preferences_bloc.dart';
|
||||||
import 'package:moxxyv2/ui/bloc/profile_bloc.dart';
|
import 'package:moxxyv2/ui/bloc/profile_bloc.dart';
|
||||||
|
import 'package:moxxyv2/ui/bloc/request_bloc.dart';
|
||||||
import 'package:moxxyv2/ui/bloc/sendfiles_bloc.dart';
|
import 'package:moxxyv2/ui/bloc/sendfiles_bloc.dart';
|
||||||
import 'package:moxxyv2/ui/bloc/server_info_bloc.dart';
|
import 'package:moxxyv2/ui/bloc/server_info_bloc.dart';
|
||||||
import 'package:moxxyv2/ui/bloc/share_selection_bloc.dart';
|
import 'package:moxxyv2/ui/bloc/share_selection_bloc.dart';
|
||||||
import 'package:moxxyv2/ui/bloc/sharedmedia_bloc.dart';
|
import 'package:moxxyv2/ui/bloc/startchat_bloc.dart';
|
||||||
|
import 'package:moxxyv2/ui/bloc/sticker_pack_bloc.dart';
|
||||||
|
import 'package:moxxyv2/ui/bloc/stickers_bloc.dart';
|
||||||
import 'package:moxxyv2/ui/constants.dart';
|
import 'package:moxxyv2/ui/constants.dart';
|
||||||
|
import 'package:moxxyv2/ui/controller/conversation_controller.dart';
|
||||||
import 'package:moxxyv2/ui/events.dart';
|
import 'package:moxxyv2/ui/events.dart';
|
||||||
/*
|
/*
|
||||||
import "package:moxxyv2/ui/pages/register/register.dart";
|
import "package:moxxyv2/ui/pages/register/register.dart";
|
||||||
import "package:moxxyv2/ui/pages/postregister/postregister.dart";
|
import "package:moxxyv2/ui/pages/postregister/postregister.dart";
|
||||||
*/
|
*/
|
||||||
import 'package:moxxyv2/ui/pages/addcontact.dart';
|
|
||||||
import 'package:moxxyv2/ui/pages/blocklist.dart';
|
import 'package:moxxyv2/ui/pages/blocklist.dart';
|
||||||
import 'package:moxxyv2/ui/pages/conversation/conversation.dart';
|
import 'package:moxxyv2/ui/pages/conversation/conversation.dart';
|
||||||
import 'package:moxxyv2/ui/pages/conversations.dart';
|
import 'package:moxxyv2/ui/pages/conversations.dart';
|
||||||
@@ -43,7 +47,7 @@ import 'package:moxxyv2/ui/pages/newconversation.dart';
|
|||||||
import 'package:moxxyv2/ui/pages/profile/devices.dart';
|
import 'package:moxxyv2/ui/pages/profile/devices.dart';
|
||||||
import 'package:moxxyv2/ui/pages/profile/own_devices.dart';
|
import 'package:moxxyv2/ui/pages/profile/own_devices.dart';
|
||||||
import 'package:moxxyv2/ui/pages/profile/profile.dart';
|
import 'package:moxxyv2/ui/pages/profile/profile.dart';
|
||||||
import 'package:moxxyv2/ui/pages/sendfiles.dart';
|
import 'package:moxxyv2/ui/pages/sendfiles/sendfiles.dart';
|
||||||
import 'package:moxxyv2/ui/pages/server_info.dart';
|
import 'package:moxxyv2/ui/pages/server_info.dart';
|
||||||
import 'package:moxxyv2/ui/pages/settings/about.dart';
|
import 'package:moxxyv2/ui/pages/settings/about.dart';
|
||||||
import 'package:moxxyv2/ui/pages/settings/appearance/appearance.dart';
|
import 'package:moxxyv2/ui/pages/settings/appearance/appearance.dart';
|
||||||
@@ -54,20 +58,33 @@ import 'package:moxxyv2/ui/pages/settings/licenses.dart';
|
|||||||
import 'package:moxxyv2/ui/pages/settings/network.dart';
|
import 'package:moxxyv2/ui/pages/settings/network.dart';
|
||||||
import 'package:moxxyv2/ui/pages/settings/privacy/privacy.dart';
|
import 'package:moxxyv2/ui/pages/settings/privacy/privacy.dart';
|
||||||
import 'package:moxxyv2/ui/pages/settings/settings.dart';
|
import 'package:moxxyv2/ui/pages/settings/settings.dart';
|
||||||
|
import 'package:moxxyv2/ui/pages/settings/sticker_packs.dart';
|
||||||
|
import 'package:moxxyv2/ui/pages/settings/stickers.dart';
|
||||||
|
import 'package:moxxyv2/ui/pages/settings/storage/shared_media.dart';
|
||||||
|
import 'package:moxxyv2/ui/pages/settings/storage/storage.dart';
|
||||||
import 'package:moxxyv2/ui/pages/share_selection.dart';
|
import 'package:moxxyv2/ui/pages/share_selection.dart';
|
||||||
import 'package:moxxyv2/ui/pages/sharedmedia.dart';
|
//import 'package:moxxyv2/ui/pages/sharedmedia.dart';
|
||||||
import 'package:moxxyv2/ui/pages/splashscreen/splashscreen.dart';
|
import 'package:moxxyv2/ui/pages/splashscreen/splashscreen.dart';
|
||||||
|
import 'package:moxxyv2/ui/pages/startchat.dart';
|
||||||
|
import 'package:moxxyv2/ui/pages/startgroupchat.dart';
|
||||||
|
import 'package:moxxyv2/ui/pages/sticker_pack.dart';
|
||||||
|
import 'package:moxxyv2/ui/pages/util/qrcode.dart';
|
||||||
|
import 'package:moxxyv2/ui/service/avatars.dart';
|
||||||
|
import 'package:moxxyv2/ui/service/connectivity.dart';
|
||||||
import 'package:moxxyv2/ui/service/data.dart';
|
import 'package:moxxyv2/ui/service/data.dart';
|
||||||
import 'package:moxxyv2/ui/service/progress.dart';
|
import 'package:moxxyv2/ui/service/progress.dart';
|
||||||
|
import 'package:moxxyv2/ui/service/read.dart';
|
||||||
|
import 'package:moxxyv2/ui/service/sharing.dart';
|
||||||
import 'package:moxxyv2/ui/theme.dart';
|
import 'package:moxxyv2/ui/theme.dart';
|
||||||
import 'package:page_transition/page_transition.dart';
|
import 'package:page_transition/page_transition.dart';
|
||||||
import 'package:share_handler/share_handler.dart';
|
|
||||||
|
|
||||||
void setupLogging() {
|
void setupLogging() {
|
||||||
Logger.root.level = kDebugMode ? Level.ALL : Level.INFO;
|
Logger.root.level = kDebugMode ? Level.ALL : Level.INFO;
|
||||||
Logger.root.onRecord.listen((record) {
|
Logger.root.onRecord.listen((record) {
|
||||||
// ignore: avoid_print
|
// ignore: avoid_print
|
||||||
print('[${record.level.name}] (${record.loggerName}) ${record.time}: ${record.message}');
|
print(
|
||||||
|
'[${record.level.name}] (${record.loggerName}) ${record.time}: ${record.message}',
|
||||||
|
);
|
||||||
});
|
});
|
||||||
GetIt.I.registerSingleton<Logger>(Logger('MoxxyMain'));
|
GetIt.I.registerSingleton<Logger>(Logger('MoxxyMain'));
|
||||||
}
|
}
|
||||||
@@ -75,17 +92,25 @@ void setupLogging() {
|
|||||||
Future<void> setupUIServices() async {
|
Future<void> setupUIServices() async {
|
||||||
GetIt.I.registerSingleton<UIProgressService>(UIProgressService());
|
GetIt.I.registerSingleton<UIProgressService>(UIProgressService());
|
||||||
GetIt.I.registerSingleton<UIDataService>(UIDataService());
|
GetIt.I.registerSingleton<UIDataService>(UIDataService());
|
||||||
|
GetIt.I.registerSingleton<UIAvatarsService>(UIAvatarsService());
|
||||||
|
GetIt.I.registerSingleton<UISharingService>(UISharingService());
|
||||||
|
GetIt.I.registerSingleton<UIConnectivityService>(UIConnectivityService());
|
||||||
|
GetIt.I.registerSingleton<UIReadMarkerService>(UIReadMarkerService());
|
||||||
|
|
||||||
|
/// Initialize services
|
||||||
|
await GetIt.I.get<UIConnectivityService>().initialize();
|
||||||
}
|
}
|
||||||
|
|
||||||
void setupBlocs(GlobalKey<NavigatorState> navKey) {
|
void setupBlocs(GlobalKey<NavigatorState> navKey) {
|
||||||
GetIt.I.registerSingleton<NavigationBloc>(NavigationBloc(navigationKey: navKey));
|
GetIt.I
|
||||||
|
.registerSingleton<NavigationBloc>(NavigationBloc(navigationKey: navKey));
|
||||||
GetIt.I.registerSingleton<ConversationsBloc>(ConversationsBloc());
|
GetIt.I.registerSingleton<ConversationsBloc>(ConversationsBloc());
|
||||||
GetIt.I.registerSingleton<NewConversationBloc>(NewConversationBloc());
|
GetIt.I.registerSingleton<NewConversationBloc>(NewConversationBloc());
|
||||||
GetIt.I.registerSingleton<ConversationBloc>(ConversationBloc());
|
GetIt.I.registerSingleton<ConversationBloc>(ConversationBloc());
|
||||||
GetIt.I.registerSingleton<BlocklistBloc>(BlocklistBloc()); GetIt.I.registerSingleton<ProfileBloc>(ProfileBloc());
|
GetIt.I.registerSingleton<BlocklistBloc>(BlocklistBloc());
|
||||||
|
GetIt.I.registerSingleton<ProfileBloc>(ProfileBloc());
|
||||||
GetIt.I.registerSingleton<PreferencesBloc>(PreferencesBloc());
|
GetIt.I.registerSingleton<PreferencesBloc>(PreferencesBloc());
|
||||||
GetIt.I.registerSingleton<AddContactBloc>(AddContactBloc());
|
GetIt.I.registerSingleton<StartChatBloc>(StartChatBloc());
|
||||||
GetIt.I.registerSingleton<SharedMediaBloc>(SharedMediaBloc());
|
|
||||||
GetIt.I.registerSingleton<CropBloc>(CropBloc());
|
GetIt.I.registerSingleton<CropBloc>(CropBloc());
|
||||||
GetIt.I.registerSingleton<SendFilesBloc>(SendFilesBloc());
|
GetIt.I.registerSingleton<SendFilesBloc>(SendFilesBloc());
|
||||||
GetIt.I.registerSingleton<CropBackgroundBloc>(CropBackgroundBloc());
|
GetIt.I.registerSingleton<CropBackgroundBloc>(CropBackgroundBloc());
|
||||||
@@ -93,14 +118,13 @@ void setupBlocs(GlobalKey<NavigatorState> navKey) {
|
|||||||
GetIt.I.registerSingleton<ServerInfoBloc>(ServerInfoBloc());
|
GetIt.I.registerSingleton<ServerInfoBloc>(ServerInfoBloc());
|
||||||
GetIt.I.registerSingleton<DevicesBloc>(DevicesBloc());
|
GetIt.I.registerSingleton<DevicesBloc>(DevicesBloc());
|
||||||
GetIt.I.registerSingleton<OwnDevicesBloc>(OwnDevicesBloc());
|
GetIt.I.registerSingleton<OwnDevicesBloc>(OwnDevicesBloc());
|
||||||
|
GetIt.I.registerSingleton<StickersBloc>(StickersBloc());
|
||||||
|
GetIt.I.registerSingleton<StickerPackBloc>(StickerPackBloc());
|
||||||
|
GetIt.I.registerSingleton<RequestBloc>(RequestBloc());
|
||||||
|
GetIt.I.registerSingleton<JoinGroupchatBloc>(JoinGroupchatBloc());
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO(Unknown): Replace all Column(children: [ Padding(), Padding, ...]) with a
|
|
||||||
// Padding(padding: ..., child: Column(children: [ ... ]))
|
|
||||||
// TODO(Unknown): Theme the switches
|
|
||||||
void main() async {
|
void main() async {
|
||||||
GetIt.I.registerSingleton<Completer<void>>(Completer());
|
|
||||||
|
|
||||||
setupLogging();
|
setupLogging();
|
||||||
await setupUIServices();
|
await setupUIServices();
|
||||||
|
|
||||||
@@ -111,6 +135,8 @@ void main() async {
|
|||||||
|
|
||||||
await initializeServiceIfNeeded();
|
await initializeServiceIfNeeded();
|
||||||
|
|
||||||
|
imageCache.maximumSizeBytes = 500 * 1024 * 1024;
|
||||||
|
|
||||||
runApp(
|
runApp(
|
||||||
MultiBlocProvider(
|
MultiBlocProvider(
|
||||||
providers: [
|
providers: [
|
||||||
@@ -138,11 +164,8 @@ void main() async {
|
|||||||
BlocProvider<PreferencesBloc>(
|
BlocProvider<PreferencesBloc>(
|
||||||
create: (_) => GetIt.I.get<PreferencesBloc>(),
|
create: (_) => GetIt.I.get<PreferencesBloc>(),
|
||||||
),
|
),
|
||||||
BlocProvider<AddContactBloc>(
|
BlocProvider<StartChatBloc>(
|
||||||
create: (_) => GetIt.I.get<AddContactBloc>(),
|
create: (_) => GetIt.I.get<StartChatBloc>(),
|
||||||
),
|
|
||||||
BlocProvider<SharedMediaBloc>(
|
|
||||||
create: (_) => GetIt.I.get<SharedMediaBloc>(),
|
|
||||||
),
|
),
|
||||||
BlocProvider<CropBloc>(
|
BlocProvider<CropBloc>(
|
||||||
create: (_) => GetIt.I.get<CropBloc>(),
|
create: (_) => GetIt.I.get<CropBloc>(),
|
||||||
@@ -165,6 +188,18 @@ void main() async {
|
|||||||
BlocProvider<OwnDevicesBloc>(
|
BlocProvider<OwnDevicesBloc>(
|
||||||
create: (_) => GetIt.I.get<OwnDevicesBloc>(),
|
create: (_) => GetIt.I.get<OwnDevicesBloc>(),
|
||||||
),
|
),
|
||||||
|
BlocProvider<StickersBloc>(
|
||||||
|
create: (_) => GetIt.I.get<StickersBloc>(),
|
||||||
|
),
|
||||||
|
BlocProvider<StickerPackBloc>(
|
||||||
|
create: (_) => GetIt.I.get<StickerPackBloc>(),
|
||||||
|
),
|
||||||
|
BlocProvider<RequestBloc>(
|
||||||
|
create: (_) => GetIt.I.get<RequestBloc>(),
|
||||||
|
),
|
||||||
|
BlocProvider<JoinGroupchatBloc>(
|
||||||
|
create: (_) => GetIt.I.get<JoinGroupchatBloc>(),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
child: TranslationProvider(
|
child: TranslationProvider(
|
||||||
child: MyApp(navKey),
|
child: MyApp(navKey),
|
||||||
@@ -174,8 +209,7 @@ void main() async {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class MyApp extends StatefulWidget {
|
class MyApp extends StatefulWidget {
|
||||||
|
const MyApp(this.navigationKey, {super.key});
|
||||||
const MyApp(this.navigationKey, { super.key });
|
|
||||||
final GlobalKey<NavigatorState> navigationKey;
|
final GlobalKey<NavigatorState> navigationKey;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -188,46 +222,20 @@ class MyAppState extends State<MyApp> with WidgetsBindingObserver {
|
|||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
|
_initState();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Async "version" of initState()
|
||||||
|
Future<void> _initState() async {
|
||||||
WidgetsBinding.instance.addObserver(this);
|
WidgetsBinding.instance.addObserver(this);
|
||||||
|
|
||||||
|
// Set up receiving share intents
|
||||||
|
await GetIt.I.get<UISharingService>().initialize();
|
||||||
|
|
||||||
// Lift the UI block
|
// Lift the UI block
|
||||||
GetIt.I.get<Completer<void>>().complete();
|
await GetIt.I
|
||||||
|
.get<SynchronizedQueue<Map<String, dynamic>?>>()
|
||||||
_setupSharingHandler();
|
.removeQueueLock();
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _handleSharedMedia(SharedMedia media) async {
|
|
||||||
final attachments = media.attachments ?? [];
|
|
||||||
GetIt.I.get<ShareSelectionBloc>().add(
|
|
||||||
ShareSelectionRequestedEvent(
|
|
||||||
attachments.map((a) => a!.path).toList(),
|
|
||||||
media.content,
|
|
||||||
media.content != null ? ShareSelectionType.text : ShareSelectionType.media,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _setupSharingHandler() async {
|
|
||||||
final handler = ShareHandlerPlatform.instance;
|
|
||||||
final media = await handler.getInitialSharedMedia();
|
|
||||||
|
|
||||||
// Shared while the app was closed
|
|
||||||
if (media != null) {
|
|
||||||
if (GetIt.I.get<UIDataService>().isLoggedIn) {
|
|
||||||
await _handleSharedMedia(media);
|
|
||||||
}
|
|
||||||
|
|
||||||
await handler.resetInitialSharedMedia();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Shared while the app is stil running
|
|
||||||
handler.sharedMediaStream.listen((SharedMedia media) async {
|
|
||||||
if (GetIt.I.get<UIDataService>().isLoggedIn) {
|
|
||||||
await _handleSharedMedia(media);
|
|
||||||
}
|
|
||||||
|
|
||||||
await handler.resetInitialSharedMedia();
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -246,17 +254,19 @@ class MyAppState extends State<MyApp> with WidgetsBindingObserver {
|
|||||||
sender.sendData(
|
sender.sendData(
|
||||||
SetCSIStateCommand(active: false),
|
SetCSIStateCommand(active: false),
|
||||||
);
|
);
|
||||||
GetIt.I.get<ConversationBloc>().add(AppStateChanged(false));
|
BidirectionalConversationController.currentController
|
||||||
break;
|
?.handleAppStateChange(false);
|
||||||
|
break;
|
||||||
case AppLifecycleState.resumed:
|
case AppLifecycleState.resumed:
|
||||||
sender.sendData(
|
sender.sendData(
|
||||||
SetCSIStateCommand(active: true),
|
SetCSIStateCommand(active: true),
|
||||||
);
|
);
|
||||||
GetIt.I.get<ConversationBloc>().add(AppStateChanged(true));
|
BidirectionalConversationController.currentController
|
||||||
break;
|
?.handleAppStateChange(true);
|
||||||
|
break;
|
||||||
case AppLifecycleState.detached:
|
case AppLifecycleState.detached:
|
||||||
case AppLifecycleState.inactive:
|
case AppLifecycleState.inactive:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -272,34 +282,85 @@ class MyAppState extends State<MyApp> with WidgetsBindingObserver {
|
|||||||
navigatorKey: widget.navigationKey,
|
navigatorKey: widget.navigationKey,
|
||||||
onGenerateRoute: (settings) {
|
onGenerateRoute: (settings) {
|
||||||
switch (settings.name) {
|
switch (settings.name) {
|
||||||
case introRoute: return Intro.route;
|
case introRoute:
|
||||||
case loginRoute: return Login.route;
|
return Intro.route;
|
||||||
case conversationsRoute: return ConversationsPage.route;
|
case loginRoute:
|
||||||
case newConversationRoute: return NewConversationPage.route;
|
return Login.route;
|
||||||
case conversationRoute: return PageTransition<dynamic>(
|
case conversationsRoute:
|
||||||
type: PageTransitionType.rightToLeft,
|
return ConversationsPage.route;
|
||||||
settings: settings,
|
case newConversationRoute:
|
||||||
child: const ConversationPage(),
|
return NewConversationPage.route;
|
||||||
);
|
case conversationRoute:
|
||||||
case sharedMediaRoute: return SharedMediaPage.route;
|
final args = settings.arguments! as ConversationPageArguments;
|
||||||
case blocklistRoute: return BlocklistPage.route;
|
return PageTransition<dynamic>(
|
||||||
case profileRoute: return ProfilePage.route;
|
type: PageTransitionType.rightToLeft,
|
||||||
case settingsRoute: return SettingsPage.route;
|
settings: settings,
|
||||||
case aboutRoute: return SettingsAboutPage.route;
|
child: ConversationPage(
|
||||||
case licensesRoute: return SettingsLicensesPage.route;
|
conversationJid: args.conversationJid,
|
||||||
case networkRoute: return NetworkPage.route;
|
initialText: args.initialText,
|
||||||
case privacyRoute: return PrivacyPage.route;
|
conversationType: args.type,
|
||||||
case debuggingRoute: return DebuggingPage.route;
|
),
|
||||||
case addContactRoute: return AddContactPage.route;
|
);
|
||||||
case cropRoute: return CropPage.route;
|
// case sharedMediaRoute:
|
||||||
case sendFilesRoute: return SendFilesPage.route;
|
// return SharedMediaPage.getRoute(
|
||||||
case backgroundCroppingRoute: return CropBackgroundPage.route;
|
// settings.arguments! as SharedMediaPageArguments,
|
||||||
case shareSelectionRoute: return ShareSelectionPage.route;
|
// );
|
||||||
case serverInfoRoute: return ServerInfoPage.route;
|
case blocklistRoute:
|
||||||
case conversationSettingsRoute: return ConversationSettingsPage.route;
|
return BlocklistPage.route;
|
||||||
case devicesRoute: return DevicesPage.route;
|
case profileRoute:
|
||||||
case ownDevicesRoute: return OwnDevicesPage.route;
|
return ProfilePage.getRoute(
|
||||||
case appearanceRoute: return AppearanceSettingsPage.route;
|
settings.arguments! as ProfileArguments,
|
||||||
|
);
|
||||||
|
case settingsRoute:
|
||||||
|
return SettingsPage.route;
|
||||||
|
case aboutRoute:
|
||||||
|
return SettingsAboutPage.route;
|
||||||
|
case licensesRoute:
|
||||||
|
return SettingsLicensesPage.route;
|
||||||
|
case networkRoute:
|
||||||
|
return NetworkPage.route;
|
||||||
|
case privacyRoute:
|
||||||
|
return PrivacyPage.route;
|
||||||
|
case debuggingRoute:
|
||||||
|
return DebuggingPage.route;
|
||||||
|
case addContactRoute:
|
||||||
|
return StartChatPage.route;
|
||||||
|
case joinGroupchatRoute:
|
||||||
|
return JoinGroupchatPage.getRoute(
|
||||||
|
settings.arguments! as JoinGroupchatArguments,
|
||||||
|
);
|
||||||
|
case cropRoute:
|
||||||
|
return CropPage.route;
|
||||||
|
case sendFilesRoute:
|
||||||
|
return SendFilesPage.route;
|
||||||
|
case backgroundCroppingRoute:
|
||||||
|
return CropBackgroundPage.route;
|
||||||
|
case shareSelectionRoute:
|
||||||
|
return ShareSelectionPage.route;
|
||||||
|
case serverInfoRoute:
|
||||||
|
return ServerInfoPage.route;
|
||||||
|
case conversationSettingsRoute:
|
||||||
|
return ConversationSettingsPage.route;
|
||||||
|
case devicesRoute:
|
||||||
|
return DevicesPage.route;
|
||||||
|
case ownDevicesRoute:
|
||||||
|
return OwnDevicesPage.route;
|
||||||
|
case appearanceRoute:
|
||||||
|
return AppearanceSettingsPage.route;
|
||||||
|
case qrCodeScannerRoute:
|
||||||
|
return QrCodeScanningPage.getRoute(
|
||||||
|
settings.arguments! as QrCodeScanningArguments,
|
||||||
|
);
|
||||||
|
case stickersRoute:
|
||||||
|
return StickersSettingsPage.route;
|
||||||
|
case stickerPacksRoute:
|
||||||
|
return StickerPacksSettingsPage.route;
|
||||||
|
case stickerPackRoute:
|
||||||
|
return StickerPackPage.route;
|
||||||
|
case storageSettingsRoute:
|
||||||
|
return StorageSettingsPage.route;
|
||||||
|
case storageSharedMediaSettingsRoute:
|
||||||
|
return StorageSharedMediaPage.route;
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@@ -1,150 +1,146 @@
|
|||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
import 'package:cryptography/cryptography.dart';
|
|
||||||
import 'package:get_it/get_it.dart';
|
import 'package:get_it/get_it.dart';
|
||||||
import 'package:hex/hex.dart';
|
|
||||||
import 'package:image_size_getter/image_size_getter.dart';
|
|
||||||
import 'package:logging/logging.dart';
|
import 'package:logging/logging.dart';
|
||||||
import 'package:moxlib/moxlib.dart';
|
|
||||||
import 'package:moxxmpp/moxxmpp.dart';
|
import 'package:moxxmpp/moxxmpp.dart';
|
||||||
import 'package:moxxyv2/service/conversation.dart';
|
import 'package:moxxyv2/service/conversation.dart';
|
||||||
|
import 'package:moxxyv2/service/notifications.dart';
|
||||||
import 'package:moxxyv2/service/preferences.dart';
|
import 'package:moxxyv2/service/preferences.dart';
|
||||||
import 'package:moxxyv2/service/roster.dart';
|
import 'package:moxxyv2/service/roster.dart';
|
||||||
import 'package:moxxyv2/service/service.dart';
|
import 'package:moxxyv2/service/service.dart';
|
||||||
import 'package:moxxyv2/service/xmpp.dart';
|
import 'package:moxxyv2/service/xmpp_state.dart';
|
||||||
import 'package:moxxyv2/shared/avatar.dart';
|
import 'package:moxxyv2/shared/avatar.dart';
|
||||||
import 'package:moxxyv2/shared/events.dart';
|
import 'package:moxxyv2/shared/events.dart';
|
||||||
|
import 'package:moxxyv2/shared/helpers.dart';
|
||||||
/// Removes line breaks and spaces from [original]. This might happen when we request the
|
|
||||||
/// avatar data. Returns the cleaned version.
|
|
||||||
String _cleanBase64String(String original) {
|
|
||||||
var ret = original;
|
|
||||||
for (final char in ['\n', ' ']) {
|
|
||||||
ret = ret.replaceAll(char, '');
|
|
||||||
}
|
|
||||||
|
|
||||||
return ret;
|
|
||||||
}
|
|
||||||
|
|
||||||
class AvatarService {
|
class AvatarService {
|
||||||
|
final Logger _log = Logger('AvatarService');
|
||||||
|
|
||||||
AvatarService() : _log = Logger('AvatarService');
|
/// List of JIDs for which we have already requested the avatar in the current stream.
|
||||||
final Logger _log;
|
final List<JID> _requestedInStream = [];
|
||||||
|
|
||||||
UserAvatarManager _getUserAvatarManager() => GetIt.I.get<XmppConnection>().getManagerById<UserAvatarManager>(userAvatarManager)!;
|
void resetCache() {
|
||||||
|
_requestedInStream.clear();
|
||||||
|
}
|
||||||
|
|
||||||
DiscoManager _getDiscoManager() => GetIt.I.get<XmppConnection>().getManagerById<DiscoManager>(discoManager)!;
|
Future<bool> _fetchAvatarForJid(JID jid, String hash) async {
|
||||||
|
final conn = GetIt.I.get<XmppConnection>();
|
||||||
Future<void> updateAvatarForJid(String jid, String hash, String base64) async {
|
final am = conn.getManagerById<UserAvatarManager>(userAvatarManager)!;
|
||||||
final cs = GetIt.I.get<ConversationService>();
|
final rawAvatar = await am.getUserAvatar(jid);
|
||||||
final rs = GetIt.I.get<RosterService>();
|
if (rawAvatar.isType<AvatarError>()) {
|
||||||
final originalConversation = await cs.getConversationByJid(jid);
|
_log.warning('Failed to request avatar for $jid');
|
||||||
var saved = false;
|
return false;
|
||||||
|
|
||||||
// Clean the raw data. Since this may arrive by chunks, those chunks may contain
|
|
||||||
// weird data pieces.
|
|
||||||
final base64Data = base64Decode(_cleanBase64String(base64));
|
|
||||||
if (originalConversation != null) {
|
|
||||||
final avatarPath = await saveAvatarInCache(
|
|
||||||
base64Data,
|
|
||||||
hash,
|
|
||||||
jid,
|
|
||||||
originalConversation.avatarUrl,
|
|
||||||
);
|
|
||||||
saved = true;
|
|
||||||
final conv = await cs.updateConversation(
|
|
||||||
originalConversation.id,
|
|
||||||
avatarUrl: avatarPath,
|
|
||||||
);
|
|
||||||
|
|
||||||
sendEvent(ConversationUpdatedEvent(conversation: conv));
|
|
||||||
} else {
|
|
||||||
_log.warning('Failed to get conversation');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
final originalRoster = await rs.getRosterItemByJid(jid);
|
final avatar = rawAvatar.get<UserAvatarData>();
|
||||||
if (originalRoster != null) {
|
await _updateAvatarForJid(
|
||||||
var avatarPath = '';
|
jid,
|
||||||
if (saved) {
|
avatar.hash,
|
||||||
avatarPath = await getAvatarPath(jid, hash);
|
avatar.data,
|
||||||
} else {
|
);
|
||||||
avatarPath = await saveAvatarInCache(
|
return true;
|
||||||
base64Data,
|
}
|
||||||
hash,
|
|
||||||
jid,
|
/// Requests the avatar for [jid]. [oldHash], if given, is the last SHA-1 hash of the known avatar.
|
||||||
originalRoster.avatarUrl,
|
/// If the avatar for [jid] has already been requested in this stream session, does nothing. Otherwise,
|
||||||
|
/// requests the XEP-0084 metadata and queries the new avatar only if the queried SHA-1 != [oldHash].
|
||||||
|
///
|
||||||
|
/// Returns true, if everything went okay. Returns false if an error occurred.
|
||||||
|
Future<bool> requestAvatar(JID jid, String? oldHash) async {
|
||||||
|
if (_requestedInStream.contains(jid)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
_requestedInStream.add(jid);
|
||||||
|
final conn = GetIt.I.get<XmppConnection>();
|
||||||
|
final am = conn.getManagerById<UserAvatarManager>(userAvatarManager)!;
|
||||||
|
final rawId = await am.getAvatarId(jid);
|
||||||
|
|
||||||
|
if (rawId.isType<AvatarError>()) {
|
||||||
|
_log.finest(
|
||||||
|
'Failed to get avatar metadata for $jid using XEP-0084: ${rawId.get<AvatarError>()}',
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
final id = rawId.get<String>();
|
||||||
|
if (id == oldHash) {
|
||||||
|
_log.finest('Not fetching avatar for $jid since the hashes are equal');
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return _fetchAvatarForJid(jid, id);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> handleAvatarUpdate(UserAvatarUpdatedEvent event) async {
|
||||||
|
if (event.metadata.isEmpty) return;
|
||||||
|
|
||||||
|
// TODO(Unknown): Maybe make a better decision?
|
||||||
|
await _fetchAvatarForJid(event.jid, event.metadata.first.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Updates the avatar path and hash for the conversation and/or roster item with jid [JID].
|
||||||
|
/// [hash] is the new hash of the avatar. [data] is the raw avatar data.
|
||||||
|
Future<void> _updateAvatarForJid(
|
||||||
|
JID jid,
|
||||||
|
String hash,
|
||||||
|
List<int> data,
|
||||||
|
) async {
|
||||||
|
final cs = GetIt.I.get<ConversationService>();
|
||||||
|
final rs = GetIt.I.get<RosterService>();
|
||||||
|
final accountJid = await GetIt.I.get<XmppStateService>().getAccountJid();
|
||||||
|
final originalConversation =
|
||||||
|
await cs.getConversationByJid(jid.toString(), accountJid!);
|
||||||
|
final originalRoster = await rs.getRosterItemByJid(
|
||||||
|
jid.toString(),
|
||||||
|
accountJid,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (originalConversation == null && originalRoster == null) return;
|
||||||
|
|
||||||
|
final avatarPath = await saveAvatarInCache(
|
||||||
|
data,
|
||||||
|
hash,
|
||||||
|
jid.toString(),
|
||||||
|
(originalConversation?.avatarPath ?? originalRoster?.avatarPath)!,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (originalConversation != null) {
|
||||||
|
final conversation = await cs.createOrUpdateConversation(
|
||||||
|
jid.toString(),
|
||||||
|
accountJid,
|
||||||
|
update: (c) async {
|
||||||
|
return cs.updateConversation(
|
||||||
|
jid.toString(),
|
||||||
|
accountJid,
|
||||||
|
avatarPath: avatarPath,
|
||||||
|
avatarHash: hash,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
if (conversation != null) {
|
||||||
|
sendEvent(
|
||||||
|
ConversationUpdatedEvent(conversation: conversation),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (originalRoster != null) {
|
||||||
final roster = await rs.updateRosterItem(
|
final roster = await rs.updateRosterItem(
|
||||||
originalRoster.id,
|
originalRoster.jid,
|
||||||
avatarUrl: avatarPath,
|
accountJid,
|
||||||
|
avatarPath: avatarPath,
|
||||||
avatarHash: hash,
|
avatarHash: hash,
|
||||||
);
|
);
|
||||||
|
|
||||||
sendEvent(RosterDiffEvent(modified: [roster]));
|
sendEvent(RosterDiffEvent(modified: [roster]));
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> fetchAndUpdateAvatarForJid(String jid, String oldHash) async {
|
sendEvent(
|
||||||
final response = await _getDiscoManager().discoItemsQuery(jid);
|
AvatarUpdatedEvent(
|
||||||
final items = response.isType<DiscoError>() ?
|
jid: jid.toString(),
|
||||||
<DiscoItem>[] :
|
path: avatarPath,
|
||||||
response.get<List<DiscoItem>>();
|
),
|
||||||
final itemNodes = items.map((i) => i.node);
|
);
|
||||||
|
|
||||||
_log.finest('Disco items for $jid:');
|
|
||||||
for (final item in itemNodes) {
|
|
||||||
_log.finest('- $item');
|
|
||||||
}
|
|
||||||
|
|
||||||
var base64 = '';
|
|
||||||
var hash = '';
|
|
||||||
if (listContains<DiscoItem>(items, (item) => item.node == userAvatarDataXmlns)) {
|
|
||||||
final avatar = _getUserAvatarManager();
|
|
||||||
final pubsubHash = await avatar.getAvatarId(jid);
|
|
||||||
|
|
||||||
// Don't request if we already have the newest avatar
|
|
||||||
if (pubsubHash == oldHash) return;
|
|
||||||
|
|
||||||
// Query via PubSub
|
|
||||||
final data = await avatar.getUserAvatar(jid);
|
|
||||||
if (data == null) return;
|
|
||||||
|
|
||||||
base64 = data.base64;
|
|
||||||
hash = data.hash;
|
|
||||||
} else {
|
|
||||||
// Query the vCard
|
|
||||||
final vm = GetIt.I.get<XmppConnection>().getManagerById<VCardManager>(vcardManager)!;
|
|
||||||
final vcard = await vm.requestVCard(jid);
|
|
||||||
if (vcard != null) {
|
|
||||||
final binval = vcard.photo?.binval;
|
|
||||||
if (binval != null) {
|
|
||||||
// Clean the raw data. Since this may arrive by chunks, those chunks may contain
|
|
||||||
// weird data pieces.
|
|
||||||
base64 = _cleanBase64String(binval);
|
|
||||||
|
|
||||||
final rawHash = await Sha1().hash(base64Decode(base64));
|
|
||||||
hash = HEX.encode(rawHash.bytes);
|
|
||||||
|
|
||||||
vm.setLastHash(jid, hash);
|
|
||||||
} else {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await updateAvatarForJid(jid, hash, base64);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<bool> subscribeJid(String jid) async {
|
|
||||||
return _getUserAvatarManager().subscribe(jid);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<bool> unsubscribeJid(String jid) async {
|
|
||||||
return _getUserAvatarManager().unsubscribe(jid);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Publishes the data at [path] as an avatar with PubSub ID
|
/// Publishes the data at [path] as an avatar with PubSub ID
|
||||||
@@ -158,59 +154,97 @@ class AvatarService {
|
|||||||
final public = prefs.isAvatarPublic;
|
final public = prefs.isAvatarPublic;
|
||||||
|
|
||||||
// Read the image metadata
|
// Read the image metadata
|
||||||
final imageSize = ImageSizeGetter.getSize(MemoryInput(bytes));
|
final imageSize = (await getImageSizeFromData(bytes))!;
|
||||||
|
|
||||||
// Publish data and metadata
|
// Publish data and metadata
|
||||||
final manager = _getUserAvatarManager();
|
final am = GetIt.I
|
||||||
await manager.publishUserAvatar(
|
.get<XmppConnection>()
|
||||||
|
.getManagerById<UserAvatarManager>(userAvatarManager)!;
|
||||||
|
|
||||||
|
_log.finest('Publishing avatar...');
|
||||||
|
final dataResult = await am.publishUserAvatar(
|
||||||
base64,
|
base64,
|
||||||
hash,
|
hash,
|
||||||
public,
|
public,
|
||||||
);
|
);
|
||||||
await manager.publishUserAvatarMetadata(
|
if (dataResult.isType<AvatarError>()) {
|
||||||
|
_log.finest('Avatar data publishing failed');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO(Unknown): Make sure that the image is not too large.
|
||||||
|
final metadataResult = await am.publishUserAvatarMetadata(
|
||||||
UserAvatarMetadata(
|
UserAvatarMetadata(
|
||||||
hash,
|
hash,
|
||||||
bytes.length,
|
bytes.length,
|
||||||
imageSize.width,
|
imageSize.width.toInt(),
|
||||||
imageSize.height,
|
imageSize.height.toInt(),
|
||||||
// TODO(PapaTutuWawa): Maybe do a check here
|
// TODO(PapaTutuWawa): Maybe do a check here
|
||||||
'image/png',
|
'image/png',
|
||||||
|
null,
|
||||||
),
|
),
|
||||||
public,
|
public,
|
||||||
);
|
);
|
||||||
|
if (metadataResult.isType<AvatarError>()) {
|
||||||
|
_log.finest('Avatar metadata publishing failed');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
_log.finest('Avatar publishing done');
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Like [requestAvatar], but fetches and processes the avatar for our own account.
|
||||||
Future<void> requestOwnAvatar() async {
|
Future<void> requestOwnAvatar() async {
|
||||||
final avatar = _getUserAvatarManager();
|
final xss = GetIt.I.get<XmppStateService>();
|
||||||
final xmpp = GetIt.I.get<XmppService>();
|
final accountJid = await xss.getAccountJid();
|
||||||
final state = await xmpp.getXmppState();
|
final state = await xss.state;
|
||||||
final jid = state.jid!;
|
final jid = JID.fromString(accountJid!);
|
||||||
final id = await avatar.getAvatarId(jid);
|
|
||||||
|
|
||||||
if (id == state.avatarHash) return;
|
if (_requestedInStream.contains(jid)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_requestedInStream.add(jid);
|
||||||
|
|
||||||
_log.info('Mismatch between saved avatar data and server-side avatar data about ourself');
|
final am = GetIt.I
|
||||||
final data = await avatar.getUserAvatar(jid);
|
.get<XmppConnection>()
|
||||||
if (data == null) {
|
.getManagerById<UserAvatarManager>(userAvatarManager)!;
|
||||||
_log.severe('Failed to fetch our avatar');
|
final rawId = await am.getAvatarId(jid);
|
||||||
|
if (rawId.isType<AvatarError>()) {
|
||||||
|
_log.finest(
|
||||||
|
'Failed to get avatar metadata for $jid using XEP-0084: ${rawId.get<AvatarError>()}',
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final id = rawId.get<String>();
|
||||||
|
|
||||||
|
if (id == state.avatarHash) {
|
||||||
|
_log.finest('Not fetching avatar for $jid since the hashes are equal');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
_log.info('Received data for our own avatar');
|
final rawAvatar = await am.getUserAvatar(jid);
|
||||||
|
if (rawAvatar.isType<AvatarError>()) {
|
||||||
|
_log.warning('Failed to request avatar for $jid');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final avatarData = rawAvatar.get<UserAvatarData>();
|
||||||
final avatarPath = await saveAvatarInCache(
|
final avatarPath = await saveAvatarInCache(
|
||||||
base64Decode(_cleanBase64String(data.base64)),
|
avatarData.data,
|
||||||
data.hash,
|
avatarData.hash,
|
||||||
jid,
|
jid.toString(),
|
||||||
state.avatarUrl,
|
state.avatarUrl,
|
||||||
);
|
);
|
||||||
await xmpp.modifyXmppState((state) => state.copyWith(
|
await xss.modifyXmppState(
|
||||||
avatarUrl: avatarPath,
|
(state) => state.copyWith(
|
||||||
avatarHash: data.hash,
|
avatarUrl: avatarPath,
|
||||||
),);
|
avatarHash: avatarData.hash,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
sendEvent(SelfAvatarChangedEvent(path: avatarPath, hash: data.hash));
|
// Update our notification avatar
|
||||||
|
await GetIt.I.get<NotificationsService>().maybeSetAvatarFromState();
|
||||||
|
|
||||||
|
sendEvent(SelfAvatarChangedEvent(path: avatarPath, hash: avatarData.hash));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,43 +1,131 @@
|
|||||||
|
import 'dart:async';
|
||||||
import 'package:get_it/get_it.dart';
|
import 'package:get_it/get_it.dart';
|
||||||
|
import 'package:logging/logging.dart';
|
||||||
import 'package:moxxmpp/moxxmpp.dart';
|
import 'package:moxxmpp/moxxmpp.dart';
|
||||||
|
import 'package:moxxyv2/service/database/constants.dart';
|
||||||
|
import 'package:moxxyv2/service/database/database.dart';
|
||||||
import 'package:moxxyv2/service/service.dart';
|
import 'package:moxxyv2/service/service.dart';
|
||||||
|
import 'package:moxxyv2/service/xmpp_state.dart';
|
||||||
import 'package:moxxyv2/shared/events.dart';
|
import 'package:moxxyv2/shared/events.dart';
|
||||||
|
|
||||||
enum BlockPushType {
|
enum BlockPushType { block, unblock }
|
||||||
block,
|
|
||||||
unblock
|
|
||||||
}
|
|
||||||
|
|
||||||
class BlocklistService {
|
class BlocklistService {
|
||||||
|
BlocklistService();
|
||||||
|
List<String>? _blocklist;
|
||||||
|
bool _requested = false;
|
||||||
|
bool? _supported;
|
||||||
|
final Logger _log = Logger('BlocklistService');
|
||||||
|
|
||||||
BlocklistService() :
|
Future<void> _removeBlocklistEntry(String jid, String accountJid) async {
|
||||||
_blocklistCache = List.empty(growable: true),
|
await GetIt.I.get<DatabaseService>().database.delete(
|
||||||
_requestedBlocklist = false;
|
blocklistTable,
|
||||||
final List<String> _blocklistCache;
|
where: 'jid = ? AND accountJid = ?',
|
||||||
bool _requestedBlocklist;
|
whereArgs: [jid, accountJid],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
Future<List<String>> _requestBlocklist() async {
|
Future<void> _addBlocklistEntry(String jid, String accountJid) async {
|
||||||
final manager = GetIt.I.get<XmppConnection>().getManagerById<BlockingManager>(blockingManager)!;
|
await GetIt.I.get<DatabaseService>().database.insert(
|
||||||
_blocklistCache
|
blocklistTable,
|
||||||
..clear()
|
{
|
||||||
..addAll(await manager.getBlocklist());
|
'jid': jid,
|
||||||
_requestedBlocklist = true;
|
'accountJid': accountJid,
|
||||||
return _blocklistCache;
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void onNewConnection() {
|
||||||
|
// Invalidate the caches
|
||||||
|
_blocklist = null;
|
||||||
|
_requested = false;
|
||||||
|
_supported = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<bool> _checkSupport() async {
|
||||||
|
return _supported ??= await GetIt.I
|
||||||
|
.get<XmppConnection>()
|
||||||
|
.getManagerById<BlockingManager>(blockingManager)!
|
||||||
|
.isSupported();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _requestBlocklist() async {
|
||||||
|
assert(
|
||||||
|
_blocklist != null,
|
||||||
|
'The blocklist must be loaded from the database before requesting',
|
||||||
|
);
|
||||||
|
|
||||||
|
// Check if blocking is supported
|
||||||
|
if (!(await _checkSupport())) {
|
||||||
|
_log.warning('Blocklist requested but server does not support it.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final accountJid = await GetIt.I.get<XmppStateService>().getAccountJid();
|
||||||
|
final blocklist = await GetIt.I
|
||||||
|
.get<XmppConnection>()
|
||||||
|
.getManagerById<BlockingManager>(blockingManager)!
|
||||||
|
.getBlocklist();
|
||||||
|
|
||||||
|
// Diff the received blocklist with the cache
|
||||||
|
final newItems = List<String>.empty(growable: true);
|
||||||
|
final removedItems = List<String>.empty(growable: true);
|
||||||
|
for (final item in blocklist) {
|
||||||
|
if (!_blocklist!.contains(item)) {
|
||||||
|
await _addBlocklistEntry(item, accountJid!);
|
||||||
|
_blocklist!.add(item);
|
||||||
|
newItems.add(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Diff the cache with the received blocklist
|
||||||
|
for (final item in _blocklist!) {
|
||||||
|
if (!blocklist.contains(item)) {
|
||||||
|
await _removeBlocklistEntry(item, accountJid!);
|
||||||
|
_blocklist!.remove(item);
|
||||||
|
removedItems.add(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_requested = true;
|
||||||
|
|
||||||
|
// Trigger an UI event if we have anything to tell the UI
|
||||||
|
if (newItems.isNotEmpty || removedItems.isNotEmpty) {
|
||||||
|
sendEvent(
|
||||||
|
BlocklistPushEvent(
|
||||||
|
added: newItems,
|
||||||
|
removed: removedItems,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the blocklist from the database
|
/// Returns the blocklist from the database
|
||||||
Future<List<String>> getBlocklist() async {
|
Future<List<String>> getBlocklist(String accountJid) async {
|
||||||
if (!_requestedBlocklist) {
|
if (_blocklist == null) {
|
||||||
_blocklistCache
|
final blocklistRaw = await GetIt.I.get<DatabaseService>().database.query(
|
||||||
..clear()
|
blocklistTable,
|
||||||
..addAll(await _requestBlocklist());
|
where: 'accountJid = ?',
|
||||||
|
whereArgs: [accountJid],
|
||||||
|
);
|
||||||
|
_blocklist = blocklistRaw.map((m) => m['jid']! as String).toList();
|
||||||
|
|
||||||
|
if (!_requested) {
|
||||||
|
unawaited(_requestBlocklist());
|
||||||
|
}
|
||||||
|
|
||||||
|
return _blocklist!;
|
||||||
}
|
}
|
||||||
|
|
||||||
return _blocklistCache;
|
if (!_requested) {
|
||||||
|
unawaited(_requestBlocklist());
|
||||||
|
}
|
||||||
|
|
||||||
|
return _blocklist!;
|
||||||
}
|
}
|
||||||
|
|
||||||
void onUnblockAllPush() {
|
void onUnblockAllPush() {
|
||||||
_blocklistCache.clear();
|
_blocklist = List<String>.empty(growable: true);
|
||||||
sendEvent(
|
sendEvent(
|
||||||
BlocklistUnblockAllEvent(),
|
BlocklistUnblockAllEvent(),
|
||||||
);
|
);
|
||||||
@@ -45,23 +133,30 @@ class BlocklistService {
|
|||||||
|
|
||||||
Future<void> onBlocklistPush(BlockPushType type, List<String> items) async {
|
Future<void> onBlocklistPush(BlockPushType type, List<String> items) async {
|
||||||
// We will fetch it later when getBlocklist is called
|
// We will fetch it later when getBlocklist is called
|
||||||
if (!_requestedBlocklist) return;
|
if (!_requested) return;
|
||||||
|
|
||||||
|
final accountJid = await GetIt.I.get<XmppStateService>().getAccountJid();
|
||||||
final newBlocks = List<String>.empty(growable: true);
|
final newBlocks = List<String>.empty(growable: true);
|
||||||
final removedBlocks = List<String>.empty(growable: true);
|
final removedBlocks = List<String>.empty(growable: true);
|
||||||
for (final item in items) {
|
for (final item in items) {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case BlockPushType.block: {
|
case BlockPushType.block:
|
||||||
if (_blocklistCache.contains(item)) continue;
|
{
|
||||||
_blocklistCache.add(item);
|
if (_blocklist!.contains(item)) continue;
|
||||||
newBlocks.add(item);
|
_blocklist!.add(item);
|
||||||
}
|
newBlocks.add(item);
|
||||||
break;
|
|
||||||
case BlockPushType.unblock: {
|
await _addBlocklistEntry(item, accountJid!);
|
||||||
_blocklistCache.removeWhere((i) => i == item);
|
}
|
||||||
removedBlocks.add(item);
|
break;
|
||||||
}
|
case BlockPushType.unblock:
|
||||||
break;
|
{
|
||||||
|
_blocklist!.removeWhere((i) => i == item);
|
||||||
|
removedBlocks.add(item);
|
||||||
|
|
||||||
|
await _removeBlocklistEntry(item, accountJid!);
|
||||||
|
}
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -74,17 +169,60 @@ class BlocklistService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<bool> blockJid(String jid) async {
|
Future<bool> blockJid(String jid) async {
|
||||||
final manager = GetIt.I.get<XmppConnection>().getManagerById<BlockingManager>(blockingManager)!;
|
// Check if blocking is supported
|
||||||
return manager.block([ jid ]);
|
if (!(await _checkSupport())) {
|
||||||
|
_log.warning('Blocking $jid requested but server does not support it.');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
_blocklist!.add(jid);
|
||||||
|
await _addBlocklistEntry(
|
||||||
|
jid,
|
||||||
|
(await GetIt.I.get<XmppStateService>().getAccountJid())!,
|
||||||
|
);
|
||||||
|
return GetIt.I
|
||||||
|
.get<XmppConnection>()
|
||||||
|
.getManagerById<BlockingManager>(blockingManager)!
|
||||||
|
.block([jid]);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<bool> unblockJid(String jid) async {
|
Future<bool> unblockJid(String jid) async {
|
||||||
final manager = GetIt.I.get<XmppConnection>().getManagerById<BlockingManager>(blockingManager)!;
|
// Check if blocking is supported
|
||||||
return manager.unblock([ jid ]);
|
if (!(await _checkSupport())) {
|
||||||
|
_log.warning('Unblocking $jid requested but server does not support it.');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
_blocklist!.remove(jid);
|
||||||
|
await _removeBlocklistEntry(
|
||||||
|
jid,
|
||||||
|
(await GetIt.I.get<XmppStateService>().getAccountJid())!,
|
||||||
|
);
|
||||||
|
return GetIt.I
|
||||||
|
.get<XmppConnection>()
|
||||||
|
.getManagerById<BlockingManager>(blockingManager)!
|
||||||
|
.unblock([jid]);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<bool> unblockAll() async {
|
Future<bool> unblockAll() async {
|
||||||
final manager = GetIt.I.get<XmppConnection>().getManagerById<BlockingManager>(blockingManager)!;
|
// Check if blocking is supported
|
||||||
return manager.unblockAll();
|
if (!(await _checkSupport())) {
|
||||||
|
_log.warning(
|
||||||
|
'Unblocking all JIDs requested but server does not support it.',
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
_blocklist!.clear();
|
||||||
|
await GetIt.I.get<DatabaseService>().database.delete(
|
||||||
|
blocklistTable,
|
||||||
|
where: 'accountJid = ?',
|
||||||
|
whereArgs: [await GetIt.I.get<XmppStateService>().getAccountJid()],
|
||||||
|
);
|
||||||
|
|
||||||
|
return GetIt.I
|
||||||
|
.get<XmppConnection>()
|
||||||
|
.getManagerById<BlockingManager>(blockingManager)!
|
||||||
|
.unblockAll();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,22 +1,32 @@
|
|||||||
import 'dart:io' show Platform;
|
import 'dart:async';
|
||||||
import 'package:connectivity_plus/connectivity_plus.dart';
|
import 'package:connectivity_plus/connectivity_plus.dart';
|
||||||
import 'package:get_it/get_it.dart';
|
|
||||||
import 'package:logging/logging.dart';
|
import 'package:logging/logging.dart';
|
||||||
import 'package:meta/meta.dart';
|
import 'package:meta/meta.dart';
|
||||||
import 'package:moxxmpp/moxxmpp.dart';
|
|
||||||
import 'package:moxxyv2/service/httpfiletransfer/httpfiletransfer.dart';
|
class ConnectivityEvent {
|
||||||
import 'package:moxxyv2/service/moxxmpp/reconnect.dart';
|
const ConnectivityEvent(this.regained, this.lost);
|
||||||
|
final bool regained;
|
||||||
|
final bool lost;
|
||||||
|
}
|
||||||
|
|
||||||
class ConnectivityService {
|
class ConnectivityService {
|
||||||
ConnectivityService() : _log = Logger('ConnectivityService');
|
/// The internal stream controller
|
||||||
final Logger _log;
|
final StreamController<ConnectivityEvent> _controller =
|
||||||
|
StreamController<ConnectivityEvent>.broadcast();
|
||||||
|
|
||||||
|
/// The logger
|
||||||
|
final Logger _log = Logger('ConnectivityService');
|
||||||
|
|
||||||
/// Caches the current connectivity state
|
/// Caches the current connectivity state
|
||||||
late ConnectivityResult _connectivity;
|
late ConnectivityResult _connectivity;
|
||||||
|
|
||||||
|
Stream<ConnectivityEvent> get stream => _controller.stream;
|
||||||
|
|
||||||
@visibleForTesting
|
@visibleForTesting
|
||||||
void setConnectivity(ConnectivityResult result) {
|
void setConnectivity(ConnectivityResult result) {
|
||||||
_log.warning('Internal connectivity state changed by request originating from outside ConnectivityService');
|
_log.warning(
|
||||||
|
'Internal connectivity state changed by request originating from outside ConnectivityService',
|
||||||
|
);
|
||||||
_connectivity = result;
|
_connectivity = result;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -24,23 +34,24 @@ class ConnectivityService {
|
|||||||
final conn = Connectivity();
|
final conn = Connectivity();
|
||||||
_connectivity = await conn.checkConnectivity();
|
_connectivity = await conn.checkConnectivity();
|
||||||
|
|
||||||
// TODO(Unknown): At least on Android, the stream fires directly after listening although the
|
conn.onConnectivityChanged.listen((ConnectivityResult result) {
|
||||||
// network does not change. So just skip it.
|
final regained = _connectivity == ConnectivityResult.none &&
|
||||||
// See https://github.com/fluttercommunity/plus_plugins/issues/567
|
result != ConnectivityResult.none;
|
||||||
final skipAmount = Platform.isAndroid ? 1 : 0;
|
|
||||||
conn.onConnectivityChanged.skip(skipAmount).listen((ConnectivityResult result) {
|
|
||||||
final regained = _connectivity == ConnectivityResult.none && result != ConnectivityResult.none;
|
|
||||||
final lost = result == ConnectivityResult.none;
|
final lost = result == ConnectivityResult.none;
|
||||||
_connectivity = result;
|
_connectivity = result;
|
||||||
|
|
||||||
// TODO(PapaTutuWawa): Should we use Streams?
|
_controller.add(
|
||||||
// Notify other services
|
ConnectivityEvent(
|
||||||
(GetIt.I.get<XmppConnection>().reconnectionPolicy as MoxxyReconnectionPolicy)
|
regained,
|
||||||
.onConnectivityChanged(regained, lost);
|
lost,
|
||||||
|
),
|
||||||
GetIt.I.get<HttpFileTransferService>().onConnectivityChanged(regained);
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
ConnectivityResult get currentState => _connectivity;
|
ConnectivityResult get currentState => _connectivity;
|
||||||
|
|
||||||
|
Future<bool> hasConnection() async {
|
||||||
|
return _connectivity != ConnectivityResult.none;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,56 +1,75 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'package:connectivity_plus/connectivity_plus.dart';
|
|
||||||
import 'package:get_it/get_it.dart';
|
import 'package:get_it/get_it.dart';
|
||||||
import 'package:logging/logging.dart';
|
import 'package:logging/logging.dart';
|
||||||
import 'package:moxxmpp/moxxmpp.dart';
|
import 'package:moxxmpp/moxxmpp.dart';
|
||||||
import 'package:moxxyv2/i18n/strings.g.dart';
|
import 'package:moxxyv2/i18n/strings.g.dart';
|
||||||
import 'package:moxxyv2/service/connectivity.dart';
|
import 'package:moxxyv2/service/connectivity.dart';
|
||||||
import 'package:moxxyv2/service/notifications.dart';
|
import 'package:moxxyv2/service/notifications.dart';
|
||||||
|
import 'package:synchronized/synchronized.dart';
|
||||||
|
|
||||||
class ConnectivityWatcherService {
|
class ConnectivityWatcherService {
|
||||||
|
/// Logger.
|
||||||
|
final Logger _log = Logger('ConnectivityWatcherService');
|
||||||
|
|
||||||
ConnectivityWatcherService() : _log = Logger('ConnectivityWatcherService');
|
/// Timer counting how much time has passed since we were last connected.
|
||||||
final Logger _log;
|
|
||||||
|
|
||||||
// Timer counting how much time has passed since we were last connected
|
|
||||||
Timer? _timer;
|
Timer? _timer;
|
||||||
|
|
||||||
|
/// Lock for accessing _timer
|
||||||
|
final Lock _lock = Lock();
|
||||||
|
|
||||||
|
Future<void> initialize() async {
|
||||||
|
GetIt.I.get<ConnectivityService>().stream.listen(_onConnectivityEvent);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _onConnectivityEvent(ConnectivityEvent event) async {
|
||||||
|
if (event.lost) {
|
||||||
|
_log.finest('Network connection lost. Stopping timer');
|
||||||
|
await _stopTimer();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> _onTimerElapsed() async {
|
Future<void> _onTimerElapsed() async {
|
||||||
|
await _stopTimer();
|
||||||
await GetIt.I.get<NotificationsService>().showWarningNotification(
|
await GetIt.I.get<NotificationsService>().showWarningNotification(
|
||||||
'Moxxy',
|
'Moxxy',
|
||||||
t.errors.connection.connectionTimeout,
|
t.errors.connection.connectionTimeout,
|
||||||
);
|
);
|
||||||
_stopTimer();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Stops the currently running timer, if there is one.
|
/// Stops the currently running timer, if there is one.
|
||||||
void _stopTimer() {
|
Future<void> _stopTimer() async {
|
||||||
if (_timer != null) {
|
await _lock.synchronized(() {
|
||||||
_timer!.cancel();
|
_timer?.cancel();
|
||||||
_timer = null;
|
_timer = null;
|
||||||
}
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Starts the timer. If it is already running, it stops the currently running one before
|
/// Starts the timer. If it is already running, it stops the currently running one before
|
||||||
/// starting the new one.
|
/// starting the new one.
|
||||||
void _startTimer() {
|
Future<void> _startTimer() async {
|
||||||
_stopTimer();
|
await _stopTimer();
|
||||||
_timer = Timer(const Duration(minutes: 30), _onTimerElapsed);
|
_timer = Timer(const Duration(minutes: 30), _onTimerElapsed);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Called when the XMPP connection state changed
|
/// Called when the XMPP connection state changed
|
||||||
Future<void> onConnectionStateChanged(XmppConnectionState before, XmppConnectionState current) async {
|
Future<void> onConnectionStateChanged(
|
||||||
if (before == XmppConnectionState.connected && current != XmppConnectionState.connected) {
|
XmppConnectionState before,
|
||||||
|
XmppConnectionState current,
|
||||||
|
) async {
|
||||||
|
if (before == XmppConnectionState.connected &&
|
||||||
|
current != XmppConnectionState.connected) {
|
||||||
// We somehow lost connection
|
// We somehow lost connection
|
||||||
if (GetIt.I.get<ConnectivityService>().currentState != ConnectivityResult.none) {
|
if (await GetIt.I.get<ConnectivityService>().hasConnection()) {
|
||||||
_log.finest('Lost connection to server. Starting warning timer...');
|
_log.finest('Lost connection to server. Starting warning timer...');
|
||||||
_startTimer();
|
await _startTimer();
|
||||||
} else {
|
} else {
|
||||||
_log.finest('Lost connection to server but no network connectivity available. Stopping warning timer...');
|
_log.finest(
|
||||||
_stopTimer();
|
'Lost connection to server but no network connectivity available. Stopping warning timer...',
|
||||||
|
);
|
||||||
|
await _stopTimer();
|
||||||
}
|
}
|
||||||
} else if (current == XmppConnectionState.connected) {
|
} else if (current == XmppConnectionState.connected) {
|
||||||
_stopTimer();
|
await _stopTimer();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
348
lib/service/contacts.dart
Normal file
348
lib/service/contacts.dart
Normal file
@@ -0,0 +1,348 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
import 'dart:io';
|
||||||
|
import 'dart:typed_data';
|
||||||
|
import 'package:flutter_contacts/flutter_contacts.dart';
|
||||||
|
import 'package:get_it/get_it.dart';
|
||||||
|
import 'package:logging/logging.dart';
|
||||||
|
import 'package:moxxyv2/service/conversation.dart';
|
||||||
|
import 'package:moxxyv2/service/database/constants.dart';
|
||||||
|
import 'package:moxxyv2/service/database/database.dart';
|
||||||
|
import 'package:moxxyv2/service/preferences.dart';
|
||||||
|
import 'package:moxxyv2/service/roster.dart';
|
||||||
|
import 'package:moxxyv2/service/service.dart';
|
||||||
|
import 'package:moxxyv2/service/xmpp_state.dart';
|
||||||
|
import 'package:moxxyv2/shared/events.dart';
|
||||||
|
import 'package:moxxyv2/shared/helpers.dart';
|
||||||
|
import 'package:moxxyv2/shared/models/roster.dart';
|
||||||
|
import 'package:permission_handler/permission_handler.dart';
|
||||||
|
|
||||||
|
class ContactWrapper {
|
||||||
|
const ContactWrapper(this.id, this.jid, this.displayName, this.thumbnail);
|
||||||
|
final String id;
|
||||||
|
final String jid;
|
||||||
|
final String displayName;
|
||||||
|
final Uint8List? thumbnail;
|
||||||
|
}
|
||||||
|
|
||||||
|
class ContactsService {
|
||||||
|
ContactsService() {
|
||||||
|
// NOTE: Apparently, this means that if false, contacts that are in 0 groups
|
||||||
|
// are not returned.
|
||||||
|
FlutterContacts.config.includeNonVisibleOnAndroid = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Logger.
|
||||||
|
final Logger _log = Logger('ContactsService');
|
||||||
|
|
||||||
|
/// JID -> Id.
|
||||||
|
Map<String, String>? _contactIds;
|
||||||
|
|
||||||
|
/// Contact ID -> Display name from the contact or null if we cached that there is
|
||||||
|
/// none
|
||||||
|
final Map<String, String?> _contactDisplayNames = {};
|
||||||
|
|
||||||
|
Future<void> initialize() async {
|
||||||
|
await enable(shouldScan: false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Enable listening to contact database events. If [shouldScan] is true, also
|
||||||
|
/// performs a scan of the contacts database, if we're allowed.
|
||||||
|
Future<void> enable({bool shouldScan = true}) async {
|
||||||
|
FlutterContacts.addListener(_onContactsDatabaseUpdate);
|
||||||
|
|
||||||
|
if (shouldScan && await _canUseContactIntegration()) {
|
||||||
|
unawaited(scanContacts());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Disable listening to contact database events. Also removes all roster items
|
||||||
|
/// that are pseudo roster items.
|
||||||
|
Future<void> disable() async {
|
||||||
|
FlutterContacts.removeListener(_onContactsDatabaseUpdate);
|
||||||
|
|
||||||
|
await GetIt.I.get<RosterService>().removePseudoRosterItems(
|
||||||
|
(await GetIt.I.get<XmppStateService>().getAccountJid())!,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _onContactsDatabaseUpdate() async {
|
||||||
|
_log.finest('Got contacts database update');
|
||||||
|
await scanContacts();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Queries the contact list for contacts that include a XMPP URI.
|
||||||
|
Future<List<ContactWrapper>> _fetchContactsWithJabber() async {
|
||||||
|
final contacts = await FlutterContacts.getContacts(
|
||||||
|
withProperties: true,
|
||||||
|
withThumbnail: true,
|
||||||
|
);
|
||||||
|
_log.finest('Got ${contacts.length} contacts');
|
||||||
|
|
||||||
|
final jabberContacts = List<ContactWrapper>.empty(growable: true);
|
||||||
|
for (final c in contacts) {
|
||||||
|
final index =
|
||||||
|
c.socialMedias.indexWhere((s) => s.label == SocialMediaLabel.jabber);
|
||||||
|
if (index == -1) continue;
|
||||||
|
|
||||||
|
jabberContacts.add(
|
||||||
|
ContactWrapper(
|
||||||
|
c.id,
|
||||||
|
c.socialMedias[index].userName,
|
||||||
|
c.displayName,
|
||||||
|
c.thumbnail,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
_log.finest('${jabberContacts.length} contacts have an XMPP address');
|
||||||
|
|
||||||
|
return jabberContacts;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Checks whether the contact integration is enabled by the user in the preferences.
|
||||||
|
/// Returns true if that is the case. If not, returns false.
|
||||||
|
Future<bool> isContactIntegrationEnabled() async {
|
||||||
|
final prefs = await GetIt.I.get<PreferencesService>().getPreferences();
|
||||||
|
return prefs.enableContactIntegration;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Checks if we a) have the permission to access the contact list and b) if the
|
||||||
|
/// user wants to use this integration.
|
||||||
|
/// Returns true if we can proceed with accessing the contact list. False, if not.
|
||||||
|
Future<bool> _canUseContactIntegration() async {
|
||||||
|
if (!(await isContactIntegrationEnabled())) {
|
||||||
|
_log.finest(
|
||||||
|
'_canUseContactIntegration: Returning false since enableContactIntegration is false',
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
final permission = await Permission.contacts.status;
|
||||||
|
if (permission == PermissionStatus.denied) {
|
||||||
|
_log.finest(
|
||||||
|
"_canUseContactIntegration: Returning false since we don't have the contacts permission",
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Queries the database for the mapping of JID -> Contact ID. The result is
|
||||||
|
/// cached after the first call.
|
||||||
|
Future<Map<String, String>> _getContactIds() async {
|
||||||
|
if (_contactIds != null) return _contactIds!;
|
||||||
|
|
||||||
|
_contactIds = Map<String, String>.fromEntries(
|
||||||
|
(await GetIt.I.get<DatabaseService>().database.query(contactsTable)).map(
|
||||||
|
(item) => MapEntry(
|
||||||
|
item['jid']! as String,
|
||||||
|
item['id']! as String,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return _contactIds!;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Queries the contact list, if enabled and allowed, and returns the contact's
|
||||||
|
/// display name.
|
||||||
|
///
|
||||||
|
/// [id] is the id of the contact. A null value indicates that there is no
|
||||||
|
/// contact and null will be returned immediately.
|
||||||
|
Future<String?> getContactDisplayName(String? id) async {
|
||||||
|
if (id == null || !(await _canUseContactIntegration())) return null;
|
||||||
|
if (_contactDisplayNames.containsKey(id)) return _contactDisplayNames[id];
|
||||||
|
|
||||||
|
final result = await FlutterContacts.getContact(
|
||||||
|
id,
|
||||||
|
withThumbnail: false,
|
||||||
|
);
|
||||||
|
_contactDisplayNames[id] = result?.displayName;
|
||||||
|
return result?.displayName;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the contact Id for the JID [jid]. If either the contact integration is
|
||||||
|
/// disabled, not possible (due to missing permissions) or there is no contact with
|
||||||
|
/// [jid] as their Jabber attribute, returns null.
|
||||||
|
Future<String?> getContactIdForJid(String jid) async {
|
||||||
|
if (!(await _canUseContactIntegration())) return null;
|
||||||
|
|
||||||
|
return (await _getContactIds())[jid];
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the path to the avatar file for the contact with JID [jid] as their
|
||||||
|
/// Jabber attribute. If either the contact integration is disabled, not possible
|
||||||
|
/// (due to missing permissions) or there is no contact with [jid] as their Jabber
|
||||||
|
/// attribute, returns null.
|
||||||
|
Future<String?> getProfilePicturePathForJid(String jid) async {
|
||||||
|
final id = await getContactIdForJid(jid);
|
||||||
|
if (id == null) return null;
|
||||||
|
|
||||||
|
final avatarPath = await getContactProfilePicturePath(id);
|
||||||
|
return File(avatarPath).existsSync() ? avatarPath : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> scanContacts() async {
|
||||||
|
final cs = GetIt.I.get<ConversationService>();
|
||||||
|
final rs = GetIt.I.get<RosterService>();
|
||||||
|
final contacts = await _fetchContactsWithJabber();
|
||||||
|
// JID -> Id
|
||||||
|
final knownContactIds = await _getContactIds();
|
||||||
|
// Id -> JID
|
||||||
|
final knownContactIdsReverse =
|
||||||
|
knownContactIds.map((key, value) => MapEntry(value, key));
|
||||||
|
final modifiedRosterItems = List<RosterItem>.empty(growable: true);
|
||||||
|
final addedRosterItems = List<RosterItem>.empty(growable: true);
|
||||||
|
final removedRosterItems = List<String>.empty(growable: true);
|
||||||
|
final accountJid = await GetIt.I.get<XmppStateService>().getAccountJid();
|
||||||
|
|
||||||
|
for (final id in List<String>.from(knownContactIds.values)) {
|
||||||
|
final index = contacts.indexWhere((c) => c.id == id);
|
||||||
|
if (index != -1) continue;
|
||||||
|
|
||||||
|
final jid = knownContactIdsReverse[id]!;
|
||||||
|
await GetIt.I.get<DatabaseService>().database.delete(
|
||||||
|
contactsTable,
|
||||||
|
where: 'id = ?',
|
||||||
|
whereArgs: [id],
|
||||||
|
);
|
||||||
|
|
||||||
|
_contactIds!.remove(knownContactIdsReverse[id]);
|
||||||
|
|
||||||
|
// Remove the avatar file, if it existed
|
||||||
|
final avatarPath = await getContactProfilePicturePath(id);
|
||||||
|
final avatarFile = File(avatarPath);
|
||||||
|
if (avatarFile.existsSync()) {
|
||||||
|
unawaited(avatarFile.delete());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove the contact attributes from the conversation, if it existed
|
||||||
|
final conversation = await cs.createOrUpdateConversation(
|
||||||
|
jid,
|
||||||
|
accountJid!,
|
||||||
|
update: (c) async {
|
||||||
|
return cs.updateConversation(
|
||||||
|
jid,
|
||||||
|
accountJid,
|
||||||
|
contactId: null,
|
||||||
|
contactAvatarPath: null,
|
||||||
|
contactDisplayName: null,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
if (conversation != null) {
|
||||||
|
sendEvent(
|
||||||
|
ConversationUpdatedEvent(
|
||||||
|
conversation: conversation,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove the contact attributes from the roster item, if it existed
|
||||||
|
final r = await rs.getRosterItemByJid(jid, accountJid);
|
||||||
|
if (r != null) {
|
||||||
|
if (r.pseudoRosterItem) {
|
||||||
|
_log.finest('Removing pseudo roster item $jid');
|
||||||
|
await rs.removeRosterItem(r.jid, accountJid);
|
||||||
|
removedRosterItems.add(jid);
|
||||||
|
} else {
|
||||||
|
final newRosterItem = await rs.updateRosterItem(
|
||||||
|
r.jid,
|
||||||
|
accountJid,
|
||||||
|
contactId: null,
|
||||||
|
contactAvatarPath: null,
|
||||||
|
contactDisplayName: null,
|
||||||
|
);
|
||||||
|
modifiedRosterItems.add(newRosterItem);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (final contact in contacts) {
|
||||||
|
// Add the ID to the cache and the database if it does not already exist
|
||||||
|
if (!knownContactIds.containsKey(contact.jid)) {
|
||||||
|
await GetIt.I.get<DatabaseService>().database.insert(
|
||||||
|
contactsTable,
|
||||||
|
<String, String>{
|
||||||
|
'id': contact.id,
|
||||||
|
'jid': contact.jid,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
_contactIds![contact.jid] = contact.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store the avatar image
|
||||||
|
// NOTE: We do not check if the file already exists since this function may also
|
||||||
|
// be triggered by the contact database listener. That listener fires when
|
||||||
|
// a change happened, without telling us exactly what happened. So, we
|
||||||
|
// just overwrite it.
|
||||||
|
final contactAvatarPath = await getContactProfilePicturePath(contact.id);
|
||||||
|
if (contact.thumbnail != null) {
|
||||||
|
final file = File(contactAvatarPath);
|
||||||
|
await file.writeAsBytes(contact.thumbnail!);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update a possibly existing conversation
|
||||||
|
final conversation = await cs.createOrUpdateConversation(
|
||||||
|
contact.jid,
|
||||||
|
accountJid!,
|
||||||
|
update: (c) async {
|
||||||
|
return cs.updateConversation(
|
||||||
|
contact.jid,
|
||||||
|
accountJid,
|
||||||
|
contactId: contact.id,
|
||||||
|
contactAvatarPath:
|
||||||
|
contact.thumbnail != null ? contactAvatarPath : null,
|
||||||
|
contactDisplayName: contact.displayName,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
if (conversation != null) {
|
||||||
|
sendEvent(
|
||||||
|
ConversationUpdatedEvent(
|
||||||
|
conversation: conversation,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update a possibly existing roster item
|
||||||
|
final r = await rs.getRosterItemByJid(contact.jid, accountJid);
|
||||||
|
if (r != null) {
|
||||||
|
final newRosterItem = await rs.updateRosterItem(
|
||||||
|
r.jid,
|
||||||
|
accountJid,
|
||||||
|
contactId: contact.id,
|
||||||
|
contactAvatarPath: contactAvatarPath,
|
||||||
|
contactDisplayName: contact.displayName,
|
||||||
|
);
|
||||||
|
modifiedRosterItems.add(newRosterItem);
|
||||||
|
} else {
|
||||||
|
final newRosterItem = await rs.addRosterItemFromData(
|
||||||
|
accountJid,
|
||||||
|
'',
|
||||||
|
'',
|
||||||
|
contact.jid,
|
||||||
|
contact.jid.split('@').first,
|
||||||
|
'none',
|
||||||
|
'none',
|
||||||
|
true,
|
||||||
|
contact.id,
|
||||||
|
contactAvatarPath,
|
||||||
|
contact.displayName,
|
||||||
|
);
|
||||||
|
addedRosterItems.add(newRosterItem);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (addedRosterItems.isNotEmpty ||
|
||||||
|
modifiedRosterItems.isNotEmpty ||
|
||||||
|
removedRosterItems.isNotEmpty) {
|
||||||
|
sendEvent(
|
||||||
|
RosterDiffEvent(
|
||||||
|
added: addedRosterItems,
|
||||||
|
modified: modifiedRosterItems,
|
||||||
|
removed: removedRosterItems,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,119 +1,314 @@
|
|||||||
import 'package:get_it/get_it.dart';
|
import 'package:get_it/get_it.dart';
|
||||||
import 'package:moxlib/moxlib.dart';
|
import 'package:logging/logging.dart';
|
||||||
import 'package:moxxmpp/moxxmpp.dart';
|
import 'package:moxxmpp/moxxmpp.dart';
|
||||||
|
import 'package:moxxyv2/service/connectivity.dart';
|
||||||
|
import 'package:moxxyv2/service/database/constants.dart';
|
||||||
import 'package:moxxyv2/service/database/database.dart';
|
import 'package:moxxyv2/service/database/database.dart';
|
||||||
|
import 'package:moxxyv2/service/database/helpers.dart';
|
||||||
|
import 'package:moxxyv2/service/groupchat.dart';
|
||||||
|
import 'package:moxxyv2/service/message.dart';
|
||||||
|
import 'package:moxxyv2/service/not_specified.dart';
|
||||||
import 'package:moxxyv2/service/preferences.dart';
|
import 'package:moxxyv2/service/preferences.dart';
|
||||||
import 'package:moxxyv2/shared/cache.dart';
|
import 'package:moxxyv2/service/roster.dart';
|
||||||
import 'package:moxxyv2/shared/models/conversation.dart';
|
import 'package:moxxyv2/shared/models/conversation.dart';
|
||||||
|
import 'package:moxxyv2/shared/models/groupchat.dart';
|
||||||
|
import 'package:moxxyv2/shared/models/message.dart';
|
||||||
|
import 'package:synchronized/synchronized.dart';
|
||||||
|
|
||||||
|
typedef CreateConversationCallback = Future<Conversation> Function();
|
||||||
|
|
||||||
|
typedef UpdateConversationCallback = Future<Conversation> Function(
|
||||||
|
Conversation,
|
||||||
|
);
|
||||||
|
|
||||||
|
typedef PreRunConversationCallback = Future<void> Function(Conversation?);
|
||||||
|
|
||||||
class ConversationService {
|
class ConversationService {
|
||||||
ConversationService()
|
/// The list of known conversations.
|
||||||
: _conversationCache = LRUCache(100),
|
Map<String, Conversation>? _conversationCache;
|
||||||
_loadedConversations = false;
|
|
||||||
|
|
||||||
final LRUCache<int, Conversation> _conversationCache;
|
/// The lock for accessing _conversationCache
|
||||||
bool _loadedConversations;
|
final Lock _lock = Lock();
|
||||||
|
|
||||||
|
final Logger _log = Logger('ConversationService');
|
||||||
|
|
||||||
|
String? _activeConversationJid;
|
||||||
|
|
||||||
|
String? get activeConversationJid => _activeConversationJid;
|
||||||
|
|
||||||
|
set activeConversationJid(String? jid) {
|
||||||
|
_log.finest('Setting activeConversationJid to $jid');
|
||||||
|
_activeConversationJid = jid;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// When called with a JID [jid], then first, if non-null, [preRun] is
|
||||||
|
/// executed.
|
||||||
|
/// Next, if a conversation with JID [jid] exists, [update] is called with
|
||||||
|
/// the conversation as its argument. If not, then [create] is executed.
|
||||||
|
/// Returns either the result of [create], [update] or null.
|
||||||
|
Future<Conversation?> createOrUpdateConversation(
|
||||||
|
String jid,
|
||||||
|
String accountJid, {
|
||||||
|
CreateConversationCallback? create,
|
||||||
|
UpdateConversationCallback? update,
|
||||||
|
PreRunConversationCallback? preRun,
|
||||||
|
}) async {
|
||||||
|
return _lock.synchronized(() async {
|
||||||
|
final conversation = await _getConversationByJid(jid, accountJid);
|
||||||
|
|
||||||
|
// Pre run
|
||||||
|
if (preRun != null) {
|
||||||
|
await preRun(conversation);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (conversation != null) {
|
||||||
|
// Conversation exists
|
||||||
|
if (update != null) {
|
||||||
|
return update(conversation);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Conversation does not exist
|
||||||
|
if (create != null) {
|
||||||
|
return create();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Loads all conversations from the database and adds them to the state and cache.
|
||||||
|
Future<List<Conversation>> loadConversations(String accountJid) async {
|
||||||
|
final db = GetIt.I.get<DatabaseService>().database;
|
||||||
|
final gs = GetIt.I.get<GroupchatService>();
|
||||||
|
final conversationsRaw = await db.query(
|
||||||
|
conversationsTable,
|
||||||
|
where: 'accountJid = ?',
|
||||||
|
whereArgs: [accountJid],
|
||||||
|
orderBy: 'lastChangeTimestamp DESC',
|
||||||
|
);
|
||||||
|
|
||||||
|
final tmp = List<Conversation>.empty(growable: true);
|
||||||
|
for (final c in conversationsRaw) {
|
||||||
|
final jid = c['jid']! as String;
|
||||||
|
final rosterItem = await GetIt.I
|
||||||
|
.get<RosterService>()
|
||||||
|
.getRosterItemByJid(jid, accountJid);
|
||||||
|
|
||||||
|
Message? lastMessage;
|
||||||
|
if (c['lastMessageId'] != null) {
|
||||||
|
lastMessage = await GetIt.I.get<MessageService>().getMessageById(
|
||||||
|
c['lastMessageId']! as String,
|
||||||
|
accountJid,
|
||||||
|
queryReactionPreview: false,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
GroupchatDetails? groupchatDetails;
|
||||||
|
if (c['type'] == ConversationType.groupchat.value) {
|
||||||
|
groupchatDetails = await gs.getGroupchatDetailsByJid(
|
||||||
|
c['jid']! as String,
|
||||||
|
accountJid,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
tmp.add(
|
||||||
|
Conversation.fromDatabaseJson(
|
||||||
|
c,
|
||||||
|
rosterItem?.showAddToRosterButton ?? true,
|
||||||
|
lastMessage,
|
||||||
|
groupchatDetails,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return tmp;
|
||||||
|
}
|
||||||
|
|
||||||
/// Wrapper around DatabaseService's loadConversations that adds the loaded
|
/// Wrapper around DatabaseService's loadConversations that adds the loaded
|
||||||
/// to the cache.
|
/// to the cache.
|
||||||
Future<void> _loadConversations() async {
|
Future<void> _loadConversationsIfNeeded(String accountJid) async {
|
||||||
final conversations = await GetIt.I.get<DatabaseService>().loadConversations();
|
if (_conversationCache != null) return;
|
||||||
for (final c in conversations) {
|
|
||||||
_conversationCache.cache(c.id, c);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns the conversation with jid [jid] or null if not found.
|
final conversations = await loadConversations(accountJid);
|
||||||
Future<Conversation?> getConversationByJid(String jid) async {
|
_conversationCache = Map<String, Conversation>.fromEntries(
|
||||||
if (!_loadedConversations) {
|
conversations.map((c) => MapEntry(c.jid, c)),
|
||||||
await _loadConversations();
|
|
||||||
_loadedConversations = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return firstWhereOrNull(
|
|
||||||
// TODO(Unknown): Maybe have it accept an iterable
|
|
||||||
_conversationCache.getValues(),
|
|
||||||
(Conversation c) => c.jid == jid,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the conversation by its database id or null if it does not exist.
|
/// Returns the conversation with jid [jid] or null if not found.
|
||||||
Future<Conversation?> _getConversationById(int id) async {
|
Future<Conversation?> _getConversationByJid(
|
||||||
if (!_loadedConversations) {
|
String jid,
|
||||||
await _loadConversations();
|
String accountJid,
|
||||||
_loadedConversations = true;
|
) async {
|
||||||
}
|
await _loadConversationsIfNeeded(accountJid);
|
||||||
|
return _conversationCache![jid];
|
||||||
|
}
|
||||||
|
|
||||||
return _conversationCache.getValue(id);
|
/// Wrapper around [ConversationService._getConversationByJid] that aquires
|
||||||
|
/// the lock for the cache.
|
||||||
|
Future<Conversation?> getConversationByJid(
|
||||||
|
String jid,
|
||||||
|
String accountJid,
|
||||||
|
) async {
|
||||||
|
return _lock
|
||||||
|
.synchronized(() async => _getConversationByJid(jid, accountJid));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// For modifying the cache without writing it to disk. Useful, for example, when
|
/// For modifying the cache without writing it to disk. Useful, for example, when
|
||||||
/// changing the chat state.
|
/// changing the chat state.
|
||||||
void setConversation(Conversation conversation) {
|
void setConversation(Conversation conversation) {
|
||||||
_conversationCache.cache(conversation.id, conversation);
|
_conversationCache![conversation.jid] = conversation;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Wrapper around [DatabaseService]'s [updateConversation] that modifies the cache.
|
/// Updates the conversation with JID [jid] inside the database.
|
||||||
Future<Conversation> updateConversation(int id, {
|
///
|
||||||
String? lastMessageBody,
|
/// To prevent issues with the cache, only call from within
|
||||||
|
/// [ConversationService.createOrUpdateConversation].
|
||||||
|
Future<Conversation> updateConversation(
|
||||||
|
String jid,
|
||||||
|
String accountJid, {
|
||||||
int? lastChangeTimestamp,
|
int? lastChangeTimestamp,
|
||||||
bool? lastMessageRetracted,
|
Message? lastMessage,
|
||||||
int? lastMessageId,
|
|
||||||
bool? open,
|
bool? open,
|
||||||
int? unreadCounter,
|
int? unreadCounter,
|
||||||
String? avatarUrl,
|
String? avatarPath,
|
||||||
|
Object? avatarHash = notSpecified,
|
||||||
ChatState? chatState,
|
ChatState? chatState,
|
||||||
bool? muted,
|
bool? muted,
|
||||||
bool? encrypted,
|
bool? encrypted,
|
||||||
|
Object? contactId = notSpecified,
|
||||||
|
Object? contactAvatarPath = notSpecified,
|
||||||
|
Object? contactDisplayName = notSpecified,
|
||||||
|
GroupchatDetails? groupchatDetails,
|
||||||
}) async {
|
}) async {
|
||||||
final conversation = await _getConversationById(id);
|
final conversation = (await _getConversationByJid(jid, accountJid))!;
|
||||||
final newConversation = await GetIt.I.get<DatabaseService>().updateConversation(
|
|
||||||
id,
|
final c = <String, dynamic>{};
|
||||||
lastMessageBody: lastMessageBody,
|
|
||||||
lastMessageRetracted: lastMessageRetracted,
|
if (lastMessage != null) {
|
||||||
lastMessageId: lastMessageId,
|
c['lastMessageId'] = lastMessage.id;
|
||||||
lastChangeTimestamp: lastChangeTimestamp,
|
}
|
||||||
open: open,
|
if (lastChangeTimestamp != null) {
|
||||||
unreadCounter: unreadCounter,
|
c['lastChangeTimestamp'] = lastChangeTimestamp;
|
||||||
avatarUrl: avatarUrl,
|
}
|
||||||
chatState: conversation?.chatState ?? ChatState.gone,
|
if (open != null) {
|
||||||
muted: muted,
|
c['open'] = boolToInt(open);
|
||||||
encrypted: encrypted,
|
}
|
||||||
|
if (unreadCounter != null) {
|
||||||
|
c['unreadCounter'] = unreadCounter;
|
||||||
|
}
|
||||||
|
if (avatarPath != null) {
|
||||||
|
c['avatarPath'] = avatarPath;
|
||||||
|
}
|
||||||
|
if (avatarHash != notSpecified) {
|
||||||
|
c['avatarHash'] = avatarHash as String?;
|
||||||
|
}
|
||||||
|
if (muted != null) {
|
||||||
|
c['muted'] = boolToInt(muted);
|
||||||
|
}
|
||||||
|
if (encrypted != null) {
|
||||||
|
c['encrypted'] = boolToInt(encrypted);
|
||||||
|
}
|
||||||
|
if (contactId != notSpecified) {
|
||||||
|
c['contactId'] = contactId as String?;
|
||||||
|
}
|
||||||
|
if (contactAvatarPath != notSpecified) {
|
||||||
|
c['contactAvatarPath'] = contactAvatarPath as String?;
|
||||||
|
}
|
||||||
|
if (contactDisplayName != notSpecified) {
|
||||||
|
c['contactDisplayName'] = contactDisplayName as String?;
|
||||||
|
}
|
||||||
|
|
||||||
|
final result =
|
||||||
|
await GetIt.I.get<DatabaseService>().database.updateAndReturn(
|
||||||
|
conversationsTable,
|
||||||
|
c,
|
||||||
|
where: 'jid = ? AND accountJid = ?',
|
||||||
|
whereArgs: [jid, accountJid],
|
||||||
);
|
);
|
||||||
|
|
||||||
_conversationCache.cache(id, newConversation);
|
final rosterItem =
|
||||||
|
await GetIt.I.get<RosterService>().getRosterItemByJid(jid, accountJid);
|
||||||
|
var newConversation = Conversation.fromDatabaseJson(
|
||||||
|
result,
|
||||||
|
rosterItem?.showAddToRosterButton ?? true,
|
||||||
|
lastMessage,
|
||||||
|
groupchatDetails,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Copy over the old lastMessage if a new one was not set
|
||||||
|
if (conversation.lastMessage != null && lastMessage == null) {
|
||||||
|
newConversation =
|
||||||
|
newConversation.copyWith(lastMessage: conversation.lastMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
_conversationCache![jid] = newConversation;
|
||||||
return newConversation;
|
return newConversation;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Wrapper around [DatabaseService]'s [addConversationFromData] that updates the cache.
|
/// Creates a [Conversation] inside the database given the data. This is so that the
|
||||||
|
/// [Conversation] object can carry its database id.
|
||||||
|
///
|
||||||
|
/// To prevent issues with the cache, only call from within
|
||||||
|
/// [ConversationService.createOrUpdateConversation].
|
||||||
Future<Conversation> addConversationFromData(
|
Future<Conversation> addConversationFromData(
|
||||||
|
String accountJid,
|
||||||
String title,
|
String title,
|
||||||
int lastMessageId,
|
Message? lastMessage,
|
||||||
bool lastMessageRetracted,
|
ConversationType type,
|
||||||
String lastMessageBody,
|
String avatarPath,
|
||||||
String avatarUrl,
|
|
||||||
String jid,
|
String jid,
|
||||||
int unreadCounter,
|
int unreadCounter,
|
||||||
int lastChangeTimestamp,
|
int lastChangeTimestamp,
|
||||||
bool open,
|
bool open,
|
||||||
bool muted,
|
bool muted,
|
||||||
bool encrypted,
|
bool encrypted,
|
||||||
|
String? contactId,
|
||||||
|
String? contactAvatarPath,
|
||||||
|
String? contactDisplayName,
|
||||||
|
GroupchatDetails? groupchatDetails,
|
||||||
) async {
|
) async {
|
||||||
final newConversation = await GetIt.I.get<DatabaseService>().addConversationFromData(
|
final rosterItem =
|
||||||
|
await GetIt.I.get<RosterService>().getRosterItemByJid(jid, accountJid);
|
||||||
|
final gs = GetIt.I.get<GroupchatService>();
|
||||||
|
final newConversation = Conversation(
|
||||||
|
accountJid,
|
||||||
title,
|
title,
|
||||||
lastMessageId,
|
lastMessage,
|
||||||
lastMessageRetracted,
|
avatarPath,
|
||||||
lastMessageBody,
|
null,
|
||||||
avatarUrl,
|
|
||||||
jid,
|
jid,
|
||||||
|
groupchatDetails,
|
||||||
unreadCounter,
|
unreadCounter,
|
||||||
|
type,
|
||||||
lastChangeTimestamp,
|
lastChangeTimestamp,
|
||||||
open,
|
open,
|
||||||
|
rosterItem?.showAddToRosterButton ?? true,
|
||||||
muted,
|
muted,
|
||||||
encrypted,
|
encrypted,
|
||||||
|
ChatState.gone,
|
||||||
|
contactId: contactId,
|
||||||
|
contactAvatarPath: contactAvatarPath,
|
||||||
|
contactDisplayName: contactDisplayName,
|
||||||
);
|
);
|
||||||
|
await GetIt.I.get<DatabaseService>().database.insert(
|
||||||
|
conversationsTable,
|
||||||
|
newConversation.toDatabaseJson(),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (_conversationCache != null) {
|
||||||
|
_conversationCache![newConversation.jid] = newConversation;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type == ConversationType.groupchat && groupchatDetails != null) {
|
||||||
|
await gs.addGroupchatDetailsFromData(
|
||||||
|
jid,
|
||||||
|
accountJid,
|
||||||
|
groupchatDetails.nick,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
_conversationCache.cache(newConversation.id, newConversation);
|
|
||||||
return newConversation;
|
return newConversation;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -122,9 +317,41 @@ class ConversationService {
|
|||||||
///
|
///
|
||||||
/// If the conversation does not exist, then the value of the preference for
|
/// If the conversation does not exist, then the value of the preference for
|
||||||
/// enableOmemoByDefault is used.
|
/// enableOmemoByDefault is used.
|
||||||
Future<bool> shouldEncryptForConversation(JID jid) async {
|
Future<bool> shouldEncryptForConversation(JID jid, String accountJid) async {
|
||||||
final prefs = await GetIt.I.get<PreferencesService>().getPreferences();
|
final prefs = await GetIt.I.get<PreferencesService>().getPreferences();
|
||||||
final conversation = await getConversationByJid(jid.toString());
|
final conversation = await getConversationByJid(jid.toString(), accountJid);
|
||||||
return conversation?.encrypted ?? prefs.enableOmemoByDefault;
|
return conversation?.encrypted ?? prefs.enableOmemoByDefault;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Send a chat state [state] to [jid], if certain pre-conditions are met:
|
||||||
|
/// - We have a network connection
|
||||||
|
/// - Sending chat markers/states are enabled
|
||||||
|
/// - [jid] != '' (not the self-chat)
|
||||||
|
/// [type] is the type of chat the chat state should be sent within.
|
||||||
|
Future<void> sendChatState(
|
||||||
|
ConversationType type,
|
||||||
|
String jid,
|
||||||
|
ChatState state,
|
||||||
|
) async {
|
||||||
|
final prefs = await GetIt.I.get<PreferencesService>().getPreferences();
|
||||||
|
|
||||||
|
// Only send chat states if the users wants to send them
|
||||||
|
if (!prefs.sendChatMarkers) return;
|
||||||
|
|
||||||
|
// Only send chat states when we're connected
|
||||||
|
// TODO(Unknown): Maybe queue it up intelligently
|
||||||
|
if (!(await GetIt.I.get<ConnectivityService>().hasConnection())) return;
|
||||||
|
|
||||||
|
final conn = GetIt.I.get<XmppConnection>();
|
||||||
|
|
||||||
|
if (jid != '') {
|
||||||
|
await conn
|
||||||
|
.getManagerById<ChatStateManager>(chatStateManager)!
|
||||||
|
.sendChatState(
|
||||||
|
state,
|
||||||
|
jid,
|
||||||
|
messageType: type.toMessageType(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import 'dart:math';
|
|||||||
import 'dart:typed_data';
|
import 'dart:typed_data';
|
||||||
import 'package:logging/logging.dart';
|
import 'package:logging/logging.dart';
|
||||||
import 'package:moxplatform/moxplatform.dart';
|
import 'package:moxplatform/moxplatform.dart';
|
||||||
import 'package:moxplatform_platform_interface/moxplatform_platform_interface.dart';
|
|
||||||
import 'package:moxxmpp/moxxmpp.dart';
|
import 'package:moxxmpp/moxxmpp.dart';
|
||||||
import 'package:moxxyv2/service/cryptography/types.dart';
|
import 'package:moxxyv2/service/cryptography/types.dart';
|
||||||
|
|
||||||
@@ -20,24 +19,30 @@ List<int> _randomBuffer(int length) {
|
|||||||
|
|
||||||
CipherAlgorithm _sfsToCipher(SFSEncryptionType type) {
|
CipherAlgorithm _sfsToCipher(SFSEncryptionType type) {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case SFSEncryptionType.aes128GcmNoPadding: return CipherAlgorithm.aes128GcmNoPadding;
|
case SFSEncryptionType.aes128GcmNoPadding:
|
||||||
case SFSEncryptionType.aes256GcmNoPadding: return CipherAlgorithm.aes256GcmNoPadding;
|
return CipherAlgorithm.aes128GcmNoPadding;
|
||||||
case SFSEncryptionType.aes256CbcPkcs7: return CipherAlgorithm.aes256CbcPkcs7;
|
case SFSEncryptionType.aes256GcmNoPadding:
|
||||||
|
return CipherAlgorithm.aes256GcmNoPadding;
|
||||||
|
case SFSEncryptionType.aes256CbcPkcs7:
|
||||||
|
return CipherAlgorithm.aes256CbcPkcs7;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class CryptographyService {
|
class CryptographyService {
|
||||||
|
|
||||||
CryptographyService() : _log = Logger('CryptographyService');
|
CryptographyService() : _log = Logger('CryptographyService');
|
||||||
final Logger _log;
|
final Logger _log;
|
||||||
|
|
||||||
/// Encrypt the file at path [source] and write the encrypted data to [dest]. For the
|
/// Encrypt the file at path [source] and write the encrypted data to [dest]. For the
|
||||||
/// encryption, use the algorithm indicated by [encryption].
|
/// encryption, use the algorithm indicated by [encryption].
|
||||||
Future<EncryptionResult> encryptFile(String source, String dest, SFSEncryptionType encryption) async {
|
Future<EncryptionResult> encryptFile(
|
||||||
|
String source,
|
||||||
|
String dest,
|
||||||
|
SFSEncryptionType encryption,
|
||||||
|
) async {
|
||||||
_log.finest('Beginning encryption routine for $source');
|
_log.finest('Beginning encryption routine for $source');
|
||||||
final key = encryption == SFSEncryptionType.aes128GcmNoPadding ?
|
final key = encryption == SFSEncryptionType.aes128GcmNoPadding
|
||||||
_randomBuffer(16) :
|
? _randomBuffer(16)
|
||||||
_randomBuffer(32);
|
: _randomBuffer(32);
|
||||||
final iv = _randomBuffer(12);
|
final iv = _randomBuffer(12);
|
||||||
final result = (await MoxplatformPlugin.crypto.encryptFile(
|
final result = (await MoxplatformPlugin.crypto.encryptFile(
|
||||||
source,
|
source,
|
||||||
@@ -52,11 +57,11 @@ class CryptographyService {
|
|||||||
return EncryptionResult(
|
return EncryptionResult(
|
||||||
key,
|
key,
|
||||||
iv,
|
iv,
|
||||||
<String, String>{
|
<HashFunction, String>{
|
||||||
hashSha256: base64Encode(result.plaintextHash),
|
HashFunction.sha256: base64Encode(result.plaintextHash),
|
||||||
},
|
},
|
||||||
<String, String>{
|
<HashFunction, String>{
|
||||||
hashSha256: base64Encode(result.ciphertextHash),
|
HashFunction.sha256: base64Encode(result.ciphertextHash),
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -70,8 +75,8 @@ class CryptographyService {
|
|||||||
SFSEncryptionType encryption,
|
SFSEncryptionType encryption,
|
||||||
List<int> key,
|
List<int> key,
|
||||||
List<int> iv,
|
List<int> iv,
|
||||||
Map<String, String> plaintextHashes,
|
Map<HashFunction, String> plaintextHashes,
|
||||||
Map<String, String> ciphertextHashes,
|
Map<HashFunction, String> ciphertextHashes,
|
||||||
) async {
|
) async {
|
||||||
_log.finest('Beginning decryption for $source');
|
_log.finest('Beginning decryption for $source');
|
||||||
final result = await MoxplatformPlugin.crypto.encryptFile(
|
final result = await MoxplatformPlugin.crypto.encryptFile(
|
||||||
@@ -88,7 +93,7 @@ class CryptographyService {
|
|||||||
var passedPlaintextIntegrityCheck = true;
|
var passedPlaintextIntegrityCheck = true;
|
||||||
var passedCiphertextIntegrityCheck = true;
|
var passedCiphertextIntegrityCheck = true;
|
||||||
for (final entry in plaintextHashes.entries) {
|
for (final entry in plaintextHashes.entries) {
|
||||||
if (entry.key == hashSha256) {
|
if (entry.key == HashFunction.sha256) {
|
||||||
if (base64Encode(result!.plaintextHash) != entry.value) {
|
if (base64Encode(result!.plaintextHash) != entry.value) {
|
||||||
passedPlaintextIntegrityCheck = false;
|
passedPlaintextIntegrityCheck = false;
|
||||||
} else {
|
} else {
|
||||||
@@ -98,8 +103,8 @@ class CryptographyService {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for (final entry in ciphertextHashes.entries) {
|
for (final entry in ciphertextHashes.entries) {
|
||||||
if (entry.key == hashSha256) {
|
if (entry.key == HashFunction.sha256) {
|
||||||
if (base64Encode(result!.ciphertextHash) != entry.value) {
|
if (base64Encode(result!.ciphertextHash) != entry.value) {
|
||||||
passedCiphertextIntegrityCheck = false;
|
passedCiphertextIntegrityCheck = false;
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -1,150 +0,0 @@
|
|||||||
import 'dart:convert';
|
|
||||||
import 'dart:io';
|
|
||||||
import 'package:cryptography/cryptography.dart';
|
|
||||||
import 'package:moxxmpp/moxxmpp.dart';
|
|
||||||
import 'package:moxxyv2/service/cryptography/types.dart';
|
|
||||||
|
|
||||||
Future<List<int>> hashFileImpl(HashRequest request) async {
|
|
||||||
final data = await File(request.path).readAsBytes();
|
|
||||||
|
|
||||||
return CryptographicHashManager.hashFromData(data, request.hash);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<EncryptionResult> encryptFileImpl(EncryptionRequest request) async {
|
|
||||||
Cipher algorithm;
|
|
||||||
switch (request.encryption) {
|
|
||||||
case SFSEncryptionType.aes128GcmNoPadding:
|
|
||||||
algorithm = AesGcm.with128bits();
|
|
||||||
break;
|
|
||||||
case SFSEncryptionType.aes256GcmNoPadding:
|
|
||||||
algorithm = AesGcm.with256bits();
|
|
||||||
break;
|
|
||||||
case SFSEncryptionType.aes256CbcPkcs7:
|
|
||||||
// TODO(Unknown): Implement
|
|
||||||
throw Exception();
|
|
||||||
// ignore: dead_code
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Generate a key and an IV for the file
|
|
||||||
final key = await algorithm.newSecretKey();
|
|
||||||
final iv = algorithm.newNonce();
|
|
||||||
final plaintext = await File(request.source).readAsBytes();
|
|
||||||
final secretBox = await algorithm.encrypt(
|
|
||||||
plaintext,
|
|
||||||
secretKey: key,
|
|
||||||
nonce: iv,
|
|
||||||
);
|
|
||||||
final ciphertext = [
|
|
||||||
...secretBox.cipherText,
|
|
||||||
...secretBox.mac.bytes,
|
|
||||||
];
|
|
||||||
|
|
||||||
// Write the file
|
|
||||||
await File(request.dest).writeAsBytes(ciphertext);
|
|
||||||
|
|
||||||
return EncryptionResult(
|
|
||||||
await key.extractBytes(),
|
|
||||||
iv,
|
|
||||||
{
|
|
||||||
hashSha256: base64Encode(
|
|
||||||
await CryptographicHashManager.hashFromData(plaintext, HashFunction.sha256),
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
hashSha256: base64Encode(
|
|
||||||
await CryptographicHashManager.hashFromData(ciphertext, HashFunction.sha256),
|
|
||||||
),
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO(PapaTutuWawa): Somehow fail when the ciphertext hash is not matching the provided data
|
|
||||||
Future<DecryptionResult> decryptFileImpl(DecryptionRequest request) async {
|
|
||||||
Cipher algorithm;
|
|
||||||
switch (request.encryption) {
|
|
||||||
case SFSEncryptionType.aes128GcmNoPadding:
|
|
||||||
algorithm = AesGcm.with128bits();
|
|
||||||
break;
|
|
||||||
case SFSEncryptionType.aes256GcmNoPadding:
|
|
||||||
algorithm = AesGcm.with256bits();
|
|
||||||
break;
|
|
||||||
case SFSEncryptionType.aes256CbcPkcs7:
|
|
||||||
// TODO(Unknown): Implement
|
|
||||||
throw Exception();
|
|
||||||
// ignore: dead_code
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
final ciphertextRaw = await File(request.source).readAsBytes();
|
|
||||||
final mac = List<int>.empty(growable: true);
|
|
||||||
final ciphertext = List<int>.empty(growable: true);
|
|
||||||
// TODO(PapaTutuWawa): Somehow handle aes256CbcPkcs7
|
|
||||||
if (request.encryption == SFSEncryptionType.aes128GcmNoPadding ||
|
|
||||||
request.encryption == SFSEncryptionType.aes256GcmNoPadding) {
|
|
||||||
mac.addAll(ciphertextRaw.sublist(ciphertextRaw.length - 16));
|
|
||||||
ciphertext.addAll(ciphertextRaw.sublist(0, ciphertextRaw.length - 16));
|
|
||||||
}
|
|
||||||
|
|
||||||
var passedCiphertextIntegrityCheck = true;
|
|
||||||
var passedPlaintextIntegrityCheck = true;
|
|
||||||
// Try to find one hash we can verify
|
|
||||||
for (final entry in request.ciphertextHashes.entries) {
|
|
||||||
if ([hashSha256, hashSha512, hashBlake2b512].contains(entry.key)) {
|
|
||||||
final hash = await CryptographicHashManager.hashFromData(
|
|
||||||
ciphertext,
|
|
||||||
hashFunctionFromName(entry.key),
|
|
||||||
);
|
|
||||||
|
|
||||||
if (base64Encode(hash) == entry.value) {
|
|
||||||
passedCiphertextIntegrityCheck = true;
|
|
||||||
} else {
|
|
||||||
passedCiphertextIntegrityCheck = false;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
final secretBox = SecretBox(
|
|
||||||
ciphertext,
|
|
||||||
nonce: request.iv,
|
|
||||||
mac: Mac(mac),
|
|
||||||
);
|
|
||||||
|
|
||||||
try {
|
|
||||||
final data = await algorithm.decrypt(
|
|
||||||
secretBox,
|
|
||||||
secretKey: SecretKey(request.key),
|
|
||||||
);
|
|
||||||
|
|
||||||
for (final entry in request.plaintextHashes.entries) {
|
|
||||||
if ([hashSha256, hashSha512, hashBlake2b512].contains(entry.key)) {
|
|
||||||
final hash = await CryptographicHashManager.hashFromData(
|
|
||||||
data,
|
|
||||||
hashFunctionFromName(entry.key),
|
|
||||||
);
|
|
||||||
|
|
||||||
if (base64Encode(hash) == entry.value) {
|
|
||||||
passedPlaintextIntegrityCheck = true;
|
|
||||||
} else {
|
|
||||||
passedPlaintextIntegrityCheck = false;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await File(request.dest).writeAsBytes(data);
|
|
||||||
} catch (_) {
|
|
||||||
return DecryptionResult(
|
|
||||||
false,
|
|
||||||
passedPlaintextIntegrityCheck,
|
|
||||||
passedCiphertextIntegrityCheck,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return DecryptionResult(
|
|
||||||
true,
|
|
||||||
passedPlaintextIntegrityCheck,
|
|
||||||
passedCiphertextIntegrityCheck,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -3,18 +3,21 @@ import 'package:moxxmpp/moxxmpp.dart';
|
|||||||
|
|
||||||
@immutable
|
@immutable
|
||||||
class EncryptionResult {
|
class EncryptionResult {
|
||||||
|
const EncryptionResult(
|
||||||
const EncryptionResult(this.key, this.iv, this.plaintextHashes, this.ciphertextHashes);
|
this.key,
|
||||||
|
this.iv,
|
||||||
|
this.plaintextHashes,
|
||||||
|
this.ciphertextHashes,
|
||||||
|
);
|
||||||
final List<int> key;
|
final List<int> key;
|
||||||
final List<int> iv;
|
final List<int> iv;
|
||||||
|
|
||||||
final Map<String, String> plaintextHashes;
|
final Map<HashFunction, String> plaintextHashes;
|
||||||
final Map<String, String> ciphertextHashes;
|
final Map<HashFunction, String> ciphertextHashes;
|
||||||
}
|
}
|
||||||
|
|
||||||
@immutable
|
@immutable
|
||||||
class EncryptionRequest {
|
class EncryptionRequest {
|
||||||
|
|
||||||
const EncryptionRequest(this.source, this.dest, this.encryption);
|
const EncryptionRequest(this.source, this.dest, this.encryption);
|
||||||
final String source;
|
final String source;
|
||||||
final String dest;
|
final String dest;
|
||||||
@@ -23,7 +26,6 @@ class EncryptionRequest {
|
|||||||
|
|
||||||
@immutable
|
@immutable
|
||||||
class DecryptionResult {
|
class DecryptionResult {
|
||||||
|
|
||||||
const DecryptionResult(
|
const DecryptionResult(
|
||||||
this.decryptionOkay,
|
this.decryptionOkay,
|
||||||
this.plaintextOkay,
|
this.plaintextOkay,
|
||||||
@@ -36,7 +38,6 @@ class DecryptionResult {
|
|||||||
|
|
||||||
@immutable
|
@immutable
|
||||||
class DecryptionRequest {
|
class DecryptionRequest {
|
||||||
|
|
||||||
const DecryptionRequest(
|
const DecryptionRequest(
|
||||||
this.source,
|
this.source,
|
||||||
this.dest,
|
this.dest,
|
||||||
@@ -51,14 +52,6 @@ class DecryptionRequest {
|
|||||||
final SFSEncryptionType encryption;
|
final SFSEncryptionType encryption;
|
||||||
final List<int> key;
|
final List<int> key;
|
||||||
final List<int> iv;
|
final List<int> iv;
|
||||||
final Map<String, String> plaintextHashes;
|
final Map<HashFunction, String> plaintextHashes;
|
||||||
final Map<String, String> ciphertextHashes;
|
final Map<HashFunction, String> ciphertextHashes;
|
||||||
}
|
|
||||||
|
|
||||||
@immutable
|
|
||||||
class HashRequest {
|
|
||||||
|
|
||||||
const HashRequest(this.path, this.hash);
|
|
||||||
final String path;
|
|
||||||
final HashFunction hash;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,23 @@
|
|||||||
const conversationsTable = 'Conversations';
|
const conversationsTable = 'Conversations';
|
||||||
const messsagesTable = 'Messages';
|
const messagesTable = 'Messages';
|
||||||
const rosterTable = 'RosterItems';
|
const rosterTable = 'RosterItems';
|
||||||
const mediaTable = 'SharedMedia';
|
const mediaTable = 'SharedMedia';
|
||||||
const preferenceTable = 'Preferences';
|
const preferenceTable = 'Preferences';
|
||||||
const omemoDeviceTable = 'OmemoDevices';
|
|
||||||
const omemoDeviceListTable = 'OmemoDeviceList';
|
|
||||||
const omemoRatchetsTable = 'OmemoSessions';
|
|
||||||
const omemoTrustCacheTable = 'OmemoTrustCacheList';
|
|
||||||
const omemoTrustDeviceListTable = 'OmemoTrustDeviceList';
|
|
||||||
const omemoTrustEnableListTable = 'OmemoTrustEnableList';
|
|
||||||
const xmppStateTable = 'XmppState';
|
const xmppStateTable = 'XmppState';
|
||||||
|
const contactsTable = 'Contacts';
|
||||||
|
const stickersTable = 'Stickers';
|
||||||
|
const stickerPacksTable = 'StickerPacks';
|
||||||
|
const blocklistTable = 'Blocklist';
|
||||||
|
const subscriptionsTable = 'SubscriptionRequests';
|
||||||
|
const fileMetadataTable = 'FileMetadata';
|
||||||
|
const fileMetadataHashesTable = 'FileMetadataHashes';
|
||||||
|
const reactionsTable = 'Reactions';
|
||||||
|
const omemoDevicesTable = 'OmemoDevices';
|
||||||
|
const omemoDeviceListTable = 'OmemoDeviceList';
|
||||||
|
const omemoRatchetsTable = 'OmemoRatchets';
|
||||||
|
const omemoTrustTable = 'OmemoTrustTable';
|
||||||
|
const notificationsTable = 'Notifications';
|
||||||
|
const groupchatTable = 'Groupchat';
|
||||||
|
|
||||||
const typeString = 0;
|
const typeString = 0;
|
||||||
const typeInt = 1;
|
const typeInt = 1;
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import 'package:moxxyv2/service/database/constants.dart';
|
import 'package:moxxyv2/service/database/constants.dart';
|
||||||
|
import 'package:moxxyv2/service/database/helpers.dart';
|
||||||
import 'package:moxxyv2/shared/models/preference.dart';
|
import 'package:moxxyv2/shared/models/preference.dart';
|
||||||
import 'package:sqflite_sqlcipher/sqflite.dart';
|
import 'package:sqflite_sqlcipher/sqflite.dart';
|
||||||
|
|
||||||
Future<void> configureDatabase(Database db) async {
|
Future<void> configureDatabase(Database db) async {
|
||||||
await db.execute('PRAGMA foreign_keys = ON');
|
await db.execute('PRAGMA foreign_keys = OFF');
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> createDatabase(Database db, int version) async {
|
Future<void> createDatabase(Database db, int version) async {
|
||||||
@@ -11,82 +12,170 @@ Future<void> createDatabase(Database db, int version) async {
|
|||||||
await db.execute(
|
await db.execute(
|
||||||
'''
|
'''
|
||||||
CREATE TABLE $xmppStateTable (
|
CREATE TABLE $xmppStateTable (
|
||||||
key TEXT PRIMARY KEY,
|
key TEXT NOT NULL,
|
||||||
value TEXT
|
accountJid TEXT NOT NULL,
|
||||||
|
value TEXT,
|
||||||
|
PRIMARY KEY (key, accountJid)
|
||||||
)''',
|
)''',
|
||||||
);
|
);
|
||||||
|
|
||||||
// Messages
|
// Messages
|
||||||
await db.execute(
|
await db.execute(
|
||||||
'''
|
'''
|
||||||
CREATE TABLE $messsagesTable (
|
CREATE TABLE $messagesTable (
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
id TEXT NOT NULL PRIMARY KEY,
|
||||||
sender TEXT NOT NULL,
|
accountJid TEXT NOT NULL,
|
||||||
body TEXT,
|
sender TEXT NOT NULL,
|
||||||
timestamp INTEGER NOT NULL,
|
body TEXT,
|
||||||
sid TEXT NOT NULL,
|
timestamp INTEGER NOT NULL,
|
||||||
conversationJid TEXT NOT NULL,
|
sid TEXT NOT NULL,
|
||||||
isMedia INTEGER NOT NULL,
|
conversationJid TEXT NOT NULL,
|
||||||
isFileUploadNotification INTEGER NOT NULL,
|
isFileUploadNotification INTEGER NOT NULL,
|
||||||
encrypted INTEGER NOT NULL,
|
encrypted INTEGER NOT NULL,
|
||||||
errorType INTEGER,
|
errorType INTEGER,
|
||||||
warningType INTEGER,
|
warningType INTEGER,
|
||||||
mediaUrl TEXT,
|
received INTEGER,
|
||||||
mediaType TEXT,
|
displayed INTEGER,
|
||||||
thumbnailData TEXT,
|
acked INTEGER,
|
||||||
mediaWidth INTEGER,
|
originId TEXT,
|
||||||
mediaHeight INTEGER,
|
quote_id TEXT,
|
||||||
srcUrl TEXT,
|
file_metadata_id TEXT,
|
||||||
key TEXT,
|
isDownloading INTEGER NOT NULL,
|
||||||
iv TEXT,
|
isUploading INTEGER NOT NULL,
|
||||||
encryptionScheme TEXT,
|
isRetracted INTEGER,
|
||||||
received INTEGER,
|
isEdited INTEGER NOT NULL,
|
||||||
displayed INTEGER,
|
containsNoStore INTEGER NOT NULL,
|
||||||
acked INTEGER,
|
stickerPackId TEXT,
|
||||||
originId TEXT,
|
occupantId TEXT,
|
||||||
quote_id INTEGER,
|
pseudoMessageType INTEGER,
|
||||||
filename TEXT,
|
pseudoMessageData TEXT,
|
||||||
plaintextHashes TEXT,
|
CONSTRAINT fk_quote
|
||||||
ciphertextHashes TEXT,
|
FOREIGN KEY (quote_id)
|
||||||
isDownloading INTEGER NOT NULL,
|
REFERENCES $messagesTable (id)
|
||||||
isUploading INTEGER NOT NULL,
|
CONSTRAINT fk_file_metadata
|
||||||
mediaSize INTEGER,
|
FOREIGN KEY (file_metadata_id)
|
||||||
isRetracted INTEGER,
|
REFERENCES $fileMetadataTable (id)
|
||||||
CONSTRAINT fk_quote FOREIGN KEY (quote_id) REFERENCES $messsagesTable (id)
|
|
||||||
)''',
|
)''',
|
||||||
);
|
);
|
||||||
|
await db.execute(
|
||||||
|
'CREATE INDEX idx_messages_id ON $messagesTable (id, sid, originId)',
|
||||||
|
);
|
||||||
|
|
||||||
|
// Reactions
|
||||||
|
await db.execute(
|
||||||
|
'''
|
||||||
|
CREATE TABLE $reactionsTable (
|
||||||
|
accountJid TEXT NOT NULL,
|
||||||
|
message_id TEXT NOT NULL,
|
||||||
|
senderJid TEXT NOT NULL,
|
||||||
|
emoji TEXT NOT NULL,
|
||||||
|
PRIMARY KEY (accountJid, senderJid, emoji, message_id),
|
||||||
|
CONSTRAINT fk_message
|
||||||
|
FOREIGN KEY (message_id)
|
||||||
|
REFERENCES $messagesTable (id)
|
||||||
|
ON DELETE CASCADE
|
||||||
|
)''',
|
||||||
|
);
|
||||||
|
await db.execute(
|
||||||
|
'CREATE INDEX idx_reactions_message_id ON $reactionsTable (message_id, accountJid, senderJid)',
|
||||||
|
);
|
||||||
|
|
||||||
|
// Notifications
|
||||||
|
await db.execute(
|
||||||
|
'''
|
||||||
|
CREATE TABLE $notificationsTable (
|
||||||
|
id INTEGER NOT NULL,
|
||||||
|
conversationJid TEXT NOT NULL,
|
||||||
|
accountJid TEXT NOT NULL,
|
||||||
|
sender TEXT,
|
||||||
|
senderJid TEXT,
|
||||||
|
avatarPath TEXT,
|
||||||
|
body TEXT NOT NULL,
|
||||||
|
mime TEXT,
|
||||||
|
path TEXT,
|
||||||
|
timestamp INTEGER NOT NULL,
|
||||||
|
PRIMARY KEY (id, conversationJid, senderJid, timestamp, accountJid)
|
||||||
|
)''',
|
||||||
|
);
|
||||||
|
await db.execute(
|
||||||
|
'CREATE INDEX idx_notifications ON $notificationsTable (conversationJid, accountJid)',
|
||||||
|
);
|
||||||
|
|
||||||
|
// File metadata
|
||||||
|
await db.execute(
|
||||||
|
'''
|
||||||
|
CREATE TABLE $fileMetadataTable (
|
||||||
|
id TEXT NOT NULL PRIMARY KEY,
|
||||||
|
path TEXT,
|
||||||
|
sourceUrls TEXT,
|
||||||
|
mimeType TEXT,
|
||||||
|
thumbnailType TEXT,
|
||||||
|
thumbnailData TEXT,
|
||||||
|
width INTEGER,
|
||||||
|
height INTEGER,
|
||||||
|
plaintextHashes TEXT,
|
||||||
|
encryptionKey TEXT,
|
||||||
|
encryptionIv TEXT,
|
||||||
|
encryptionScheme TEXT,
|
||||||
|
cipherTextHashes TEXT,
|
||||||
|
filename TEXT NOT NULL,
|
||||||
|
size INTEGER
|
||||||
|
)''',
|
||||||
|
);
|
||||||
|
await db.execute(
|
||||||
|
'''
|
||||||
|
CREATE TABLE $fileMetadataHashesTable (
|
||||||
|
algorithm TEXT NOT NULL,
|
||||||
|
value TEXT NOT NULL,
|
||||||
|
id TEXT NOT NULL,
|
||||||
|
CONSTRAINT f_primarykey PRIMARY KEY (algorithm, value),
|
||||||
|
CONSTRAINT fk_id FOREIGN KEY (id) REFERENCES $fileMetadataTable (id)
|
||||||
|
ON DELETE CASCADE
|
||||||
|
)''',
|
||||||
|
);
|
||||||
|
await db.execute(
|
||||||
|
'CREATE INDEX idx_file_metadata_message_id ON $fileMetadataTable (id)',
|
||||||
|
);
|
||||||
|
|
||||||
// Conversations
|
// Conversations
|
||||||
await db.execute(
|
await db.execute(
|
||||||
'''
|
'''
|
||||||
CREATE TABLE $conversationsTable (
|
CREATE TABLE $conversationsTable (
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
jid TEXT NOT NULL,
|
||||||
jid TEXT NOT NULL,
|
accountJid TEXT NOT NULL,
|
||||||
title TEXT NOT NULL,
|
title TEXT NOT NULL,
|
||||||
avatarUrl TEXT NOT NULL,
|
avatarPath TEXT NOT NULL,
|
||||||
|
avatarHash TEXT,
|
||||||
|
type TEXT NOT NULL,
|
||||||
lastChangeTimestamp INTEGER NOT NULL,
|
lastChangeTimestamp INTEGER NOT NULL,
|
||||||
unreadCounter INTEGER NOT NULL,
|
unreadCounter INTEGER NOT NULL,
|
||||||
lastMessageBody TEXT NOT NULL,
|
open INTEGER NOT NULL,
|
||||||
open INTEGER NOT NULL,
|
muted INTEGER NOT NULL,
|
||||||
muted INTEGER NOT NULL,
|
encrypted INTEGER NOT NULL,
|
||||||
encrypted INTEGER NOT NULL,
|
lastMessageId TEXT,
|
||||||
lastMessageId INTEGER NOT NULL,
|
contactId TEXT,
|
||||||
lastMessageRetracted INTEGER NOT NULL,
|
contactAvatarPath TEXT,
|
||||||
|
contactDisplayName TEXT,
|
||||||
|
PRIMARY KEY (jid, accountJid),
|
||||||
|
CONSTRAINT fk_last_message
|
||||||
|
FOREIGN KEY (lastMessageId)
|
||||||
|
REFERENCES $messagesTable (id),
|
||||||
|
CONSTRAINT fk_contact_id
|
||||||
|
FOREIGN KEY (contactId)
|
||||||
|
REFERENCES $contactsTable (id)
|
||||||
|
ON DELETE SET NULL
|
||||||
)''',
|
)''',
|
||||||
);
|
);
|
||||||
|
await db.execute(
|
||||||
|
'CREATE INDEX idx_conversation_id ON $conversationsTable (jid, accountJid)',
|
||||||
|
);
|
||||||
|
|
||||||
// Shared media
|
// Contacts
|
||||||
await db.execute(
|
await db.execute(
|
||||||
'''
|
'''
|
||||||
CREATE TABLE $mediaTable (
|
CREATE TABLE $contactsTable (
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
id TEXT PRIMARY KEY,
|
||||||
path TEXT NOT NULL,
|
jid TEXT NOT NULL
|
||||||
mime TEXT,
|
|
||||||
timestamp INTEGER NOT NULL,
|
|
||||||
conversation_id INTEGER NOT NULL,
|
|
||||||
message_id INTEGER,
|
|
||||||
FOREIGN KEY (conversation_id) REFERENCES $conversationsTable (id),
|
|
||||||
FOREIGN KEY (message_id) REFERENCES $messsagesTable (id)
|
|
||||||
)''',
|
)''',
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -94,76 +183,121 @@ Future<void> createDatabase(Database db, int version) async {
|
|||||||
await db.execute(
|
await db.execute(
|
||||||
'''
|
'''
|
||||||
CREATE TABLE $rosterTable (
|
CREATE TABLE $rosterTable (
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
jid TEXT NOT NULL,
|
||||||
jid TEXT NOT NULL,
|
accountJid TEXT NOT NULL,
|
||||||
title TEXT NOT NULL,
|
title TEXT NOT NULL,
|
||||||
avatarUrl TEXT NOT NULL,
|
avatarPath TEXT NOT NULL,
|
||||||
avatarHash TEXT NOT NULL,
|
avatarHash TEXT NOT NULL,
|
||||||
subscription TEXT NOT NULL,
|
subscription TEXT NOT NULL,
|
||||||
ask TEXT NOT NULL
|
ask TEXT NOT NULL,
|
||||||
|
contactId TEXT,
|
||||||
|
contactAvatarPath TEXT,
|
||||||
|
contactDisplayName TEXT,
|
||||||
|
pseudoRosterItem INTEGER NOT NULL,
|
||||||
|
CONSTRAINT fk_contact_id
|
||||||
|
FOREIGN KEY (contactId)
|
||||||
|
REFERENCES $contactsTable (id)
|
||||||
|
ON DELETE SET NULL
|
||||||
)''',
|
)''',
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Stickers
|
||||||
|
await db.execute(
|
||||||
|
'''
|
||||||
|
CREATE TABLE $stickersTable (
|
||||||
|
id TEXT PRIMARY KEY,
|
||||||
|
desc TEXT NOT NULL,
|
||||||
|
suggests TEXT NOT NULL,
|
||||||
|
file_metadata_id TEXT NOT NULL,
|
||||||
|
stickerPackId TEXT NOT NULL,
|
||||||
|
CONSTRAINT fk_sticker_pack FOREIGN KEY (stickerPackId) REFERENCES $stickerPacksTable (id)
|
||||||
|
ON DELETE CASCADE,
|
||||||
|
CONSTRAINT fk_file_metadata FOREIGN KEY (file_metadata_id) REFERENCES $fileMetadataTable (id)
|
||||||
|
)''',
|
||||||
|
);
|
||||||
|
await db.execute(
|
||||||
|
'''
|
||||||
|
CREATE TABLE $stickerPacksTable (
|
||||||
|
id TEXT PRIMARY KEY,
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
description TEXT NOT NULL,
|
||||||
|
hashAlgorithm TEXT NOT NULL,
|
||||||
|
hashValue TEXT NOT NULL,
|
||||||
|
restricted INTEGER NOT NULL,
|
||||||
|
addedTimestamp INTEGER NOT NULL
|
||||||
|
)''',
|
||||||
|
);
|
||||||
|
|
||||||
|
// Blocklist
|
||||||
|
await db.execute(
|
||||||
|
'''
|
||||||
|
CREATE TABLE $blocklistTable (
|
||||||
|
jid TEXT NOT NULL,
|
||||||
|
accountJid TEXT NOT NULL,
|
||||||
|
PRIMARY KEY (accountJid, jid)
|
||||||
|
);
|
||||||
|
''',
|
||||||
|
);
|
||||||
|
|
||||||
// OMEMO
|
// OMEMO
|
||||||
await db.execute(
|
await db.execute(
|
||||||
'''
|
'''
|
||||||
CREATE TABLE $omemoRatchetsTable (
|
CREATE TABLE $omemoDevicesTable (
|
||||||
id INTEGER NOT NULL,
|
jid TEXT NOT NULL PRIMARY KEY,
|
||||||
|
id INTEGER NOT NULL,
|
||||||
|
ikPub TEXT NOT NULL,
|
||||||
|
ik TEXT NOT NULL,
|
||||||
|
spkPub TEXT NOT NULL,
|
||||||
|
spk TEXT NOT NULL,
|
||||||
|
spkId INTEGER NOT NULL,
|
||||||
|
spkSig TEXT NOT NULL,
|
||||||
|
oldSpkPub TEXT,
|
||||||
|
oldSpk TEXT,
|
||||||
|
oldSpkId INTEGER,
|
||||||
|
opks TEXT NOT NULL
|
||||||
|
)''',
|
||||||
|
);
|
||||||
|
await db.execute(
|
||||||
|
'''
|
||||||
|
CREATE TABLE $omemoDeviceListTable (
|
||||||
jid TEXT NOT NULL,
|
jid TEXT NOT NULL,
|
||||||
|
accountJid TEXT NOT NULL,
|
||||||
|
devices TEXT NOT NULL,
|
||||||
|
PRIMARY KEY (accountJid, jid)
|
||||||
|
)''',
|
||||||
|
);
|
||||||
|
await db.execute(
|
||||||
|
'''
|
||||||
|
CREATE TABLE $omemoRatchetsTable (
|
||||||
|
jid TEXT NOT NULL,
|
||||||
|
accountJid TEXT NOT NULL,
|
||||||
|
device INTEGER NOT NULL,
|
||||||
|
dhsPub TEXT NOT NULL,
|
||||||
dhs TEXT NOT NULL,
|
dhs TEXT NOT NULL,
|
||||||
dhs_pub TEXT NOT NULL,
|
dhrPub TEXT,
|
||||||
dhr TEXT,
|
|
||||||
rk TEXT NOT NULL,
|
rk TEXT NOT NULL,
|
||||||
cks TEXT,
|
cks TEXT,
|
||||||
ckr TEXT,
|
ckr TEXT,
|
||||||
ns INTEGER NOT NULL,
|
ns INTEGER NOT NULL,
|
||||||
nr INTEGER NOT NULL,
|
nr INTEGER NOT NULL,
|
||||||
pn INTEGER NOT NULL,
|
pn INTEGER NOT NULL,
|
||||||
ik_pub TEXT NOT NULL,
|
ik TEXT NOT NULL,
|
||||||
session_ad TEXT NOT NULL,
|
ad TEXT NOT NULL,
|
||||||
acknowledged INTEGER NOT NULL,
|
skipped TEXT NOT NULL,
|
||||||
mkskipped TEXT NOT NULL,
|
kex TEXT NOT NULL,
|
||||||
kex_timestamp INTEGER NOT NULL,
|
acked INTEGER NOT NULL,
|
||||||
kex TEXT,
|
PRIMARY KEY (accountJid, jid, device)
|
||||||
PRIMARY KEY (jid, id)
|
|
||||||
)''',
|
)''',
|
||||||
);
|
);
|
||||||
await db.execute(
|
await db.execute(
|
||||||
'''
|
'''
|
||||||
CREATE TABLE $omemoTrustCacheTable (
|
CREATE TABLE $omemoTrustTable (
|
||||||
key TEXT PRIMARY KEY NOT NULL,
|
jid TEXT NOT NULL,
|
||||||
trust INTEGER NOT NULL
|
accountJid TEXT NOT NULL,
|
||||||
)''',
|
device INTEGER NOT NULL,
|
||||||
);
|
trust INTEGER NOT NULL,
|
||||||
await db.execute(
|
enabled INTEGER NOT NULL,
|
||||||
'''
|
PRIMARY KEY (accountJid, jid, device)
|
||||||
CREATE TABLE $omemoTrustDeviceListTable (
|
|
||||||
jid TEXT NOT NULL,
|
|
||||||
device INTEGER NOT NULL
|
|
||||||
)''',
|
|
||||||
);
|
|
||||||
await db.execute(
|
|
||||||
'''
|
|
||||||
CREATE TABLE $omemoTrustEnableListTable (
|
|
||||||
key TEXT PRIMARY KEY NOT NULL,
|
|
||||||
enabled INTEGER NOT NULL
|
|
||||||
)''',
|
|
||||||
);
|
|
||||||
await db.execute(
|
|
||||||
'''
|
|
||||||
CREATE TABLE $omemoDeviceTable (
|
|
||||||
jid TEXT NOT NULL,
|
|
||||||
id INTEGER NOT NULL,
|
|
||||||
data TEXT NOT NULL,
|
|
||||||
PRIMARY KEY (jid, id)
|
|
||||||
)''',
|
|
||||||
);
|
|
||||||
await db.execute(
|
|
||||||
'''
|
|
||||||
CREATE TABLE $omemoDeviceListTable (
|
|
||||||
jid TEXT NOT NULL,
|
|
||||||
id INTEGER NOT NULL,
|
|
||||||
PRIMARY KEY (jid, id)
|
|
||||||
)''',
|
)''',
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -176,6 +310,22 @@ Future<void> createDatabase(Database db, int version) async {
|
|||||||
value TEXT NOT NULL
|
value TEXT NOT NULL
|
||||||
)''',
|
)''',
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Groupchat
|
||||||
|
await db.execute(
|
||||||
|
'''
|
||||||
|
CREATE TABLE $groupchatTable (
|
||||||
|
jid TEXT NOT NULL,
|
||||||
|
accountJid TEXT NOT NULL,
|
||||||
|
nick TEXT NOT NULL,
|
||||||
|
PRIMARY KEY (jid, accountJid),
|
||||||
|
CONSTRAINT fk_groupchat
|
||||||
|
FOREIGN KEY (jid, accountJid)
|
||||||
|
REFERENCES $conversationsTable (jid, accountJid)
|
||||||
|
ON DELETE CASCADE
|
||||||
|
)''',
|
||||||
|
);
|
||||||
|
|
||||||
await db.insert(
|
await db.insert(
|
||||||
preferenceTable,
|
preferenceTable,
|
||||||
Preference(
|
Preference(
|
||||||
@@ -240,14 +390,6 @@ Future<void> createDatabase(Database db, int version) async {
|
|||||||
'true',
|
'true',
|
||||||
).toDatabaseJson(),
|
).toDatabaseJson(),
|
||||||
);
|
);
|
||||||
await db.insert(
|
|
||||||
preferenceTable,
|
|
||||||
Preference(
|
|
||||||
'autoAcceptSubscriptionRequests',
|
|
||||||
typeBool,
|
|
||||||
'false',
|
|
||||||
).toDatabaseJson(),
|
|
||||||
);
|
|
||||||
await db.insert(
|
await db.insert(
|
||||||
preferenceTable,
|
preferenceTable,
|
||||||
Preference(
|
Preference(
|
||||||
@@ -336,4 +478,28 @@ Future<void> createDatabase(Database db, int version) async {
|
|||||||
'default',
|
'default',
|
||||||
).toDatabaseJson(),
|
).toDatabaseJson(),
|
||||||
);
|
);
|
||||||
|
await db.insert(
|
||||||
|
preferenceTable,
|
||||||
|
Preference(
|
||||||
|
'enableContactIntegration',
|
||||||
|
typeBool,
|
||||||
|
'false',
|
||||||
|
).toDatabaseJson(),
|
||||||
|
);
|
||||||
|
await db.insert(
|
||||||
|
preferenceTable,
|
||||||
|
Preference(
|
||||||
|
'isStickersNodePublic',
|
||||||
|
typeBool,
|
||||||
|
'true',
|
||||||
|
).toDatabaseJson(),
|
||||||
|
);
|
||||||
|
await db.insert(
|
||||||
|
preferenceTable,
|
||||||
|
Preference(
|
||||||
|
'showDebugMenu',
|
||||||
|
typeBool,
|
||||||
|
boolToString(false),
|
||||||
|
).toDatabaseJson(),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -7,3 +7,17 @@ bool stringToBool(String s) => s == 'true' ? true : false;
|
|||||||
|
|
||||||
String intToString(int i) => '$i';
|
String intToString(int i) => '$i';
|
||||||
int stringToInt(String s) => int.parse(s);
|
int stringToInt(String s) => int.parse(s);
|
||||||
|
|
||||||
|
/// Given a map [map], extract all key-value pairs from [map] where the key starts with
|
||||||
|
/// [prefix]. Combine those key-value pairs into a new map, where the leading [prefix]
|
||||||
|
/// is removed from all key names.
|
||||||
|
Map<String, T> getPrefixedSubMap<T>(Map<String, T> map, String prefix) {
|
||||||
|
return Map<String, T>.fromEntries(
|
||||||
|
map.entries.where((entry) => entry.key.startsWith(prefix)).map(
|
||||||
|
(entry) => MapEntry<String, T>(
|
||||||
|
entry.key.substring(prefix.length),
|
||||||
|
entry.value,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
55
lib/service/database/migration.dart
Normal file
55
lib/service/database/migration.dart
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
import 'package:logging/logging.dart';
|
||||||
|
|
||||||
|
/// A function to be called when a migration should be performed.
|
||||||
|
typedef MigrationCallback<T> = Future<void> Function(T);
|
||||||
|
|
||||||
|
/// This class represents a single database migration.
|
||||||
|
class Migration<T> {
|
||||||
|
const Migration(this.version, this.migration);
|
||||||
|
|
||||||
|
/// The version this migration upgrades the database to.
|
||||||
|
final int version;
|
||||||
|
|
||||||
|
/// The migration callback. Called the the database version is less than [version].
|
||||||
|
final MigrationCallback<T> migration;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Given the migration [param], which is passed to every migration, with the current version
|
||||||
|
/// [version], goes through the list of
|
||||||
|
/// migrations [migrations] and applies all migrations with a version greater than
|
||||||
|
/// [version]. [migrations] is sorted before usage.
|
||||||
|
///
|
||||||
|
/// NOTE: This entire setup is written as a generic to make testing easier. We cannot easily
|
||||||
|
/// mock, or better "instantiate", a Database object. Thus, to avoid having nullable
|
||||||
|
/// database argument, just pass in whatever (the tests use an integer).
|
||||||
|
Future<void> runMigrations<T>(
|
||||||
|
Logger log,
|
||||||
|
T param,
|
||||||
|
List<Migration<T>> migrations,
|
||||||
|
int version,
|
||||||
|
String typeName, {
|
||||||
|
Future<void> Function(int)? commitVersion,
|
||||||
|
}) async {
|
||||||
|
final sortedMigrations = List<Migration<T>>.from(migrations)
|
||||||
|
..sort(
|
||||||
|
(a, b) => a.version.compareTo(b.version),
|
||||||
|
);
|
||||||
|
var currentVersion = version;
|
||||||
|
var hasRunMigration = false;
|
||||||
|
for (final migration in sortedMigrations) {
|
||||||
|
if (version < migration.version) {
|
||||||
|
log.info(
|
||||||
|
'Running $typeName migration $currentVersion -> ${migration.version}',
|
||||||
|
);
|
||||||
|
await migration.migration(param);
|
||||||
|
currentVersion = migration.version;
|
||||||
|
hasRunMigration = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Commit the version, if specified.
|
||||||
|
if (commitVersion != null && hasRunMigration) {
|
||||||
|
log.info('Committing migration version $currentVersion');
|
||||||
|
await commitVersion(currentVersion);
|
||||||
|
}
|
||||||
|
}
|
||||||
12
lib/service/database/migrations/0000_blocklist.dart
Normal file
12
lib/service/database/migrations/0000_blocklist.dart
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import 'package:moxxyv2/service/database/constants.dart';
|
||||||
|
import 'package:sqflite_sqlcipher/sqflite.dart';
|
||||||
|
|
||||||
|
Future<void> upgradeFromV22ToV23(Database db) async {
|
||||||
|
await db.execute(
|
||||||
|
'''
|
||||||
|
CREATE TABLE $blocklistTable (
|
||||||
|
jid TEXT PRIMARY KEY
|
||||||
|
);
|
||||||
|
''',
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
import 'package:moxxyv2/service/database/constants.dart';
|
||||||
|
import 'package:moxxyv2/shared/models/preference.dart';
|
||||||
|
import 'package:sqflite_sqlcipher/sqflite.dart';
|
||||||
|
|
||||||
|
Future<void> upgradeFromV13ToV14(Database db) async {
|
||||||
|
// Create the new table
|
||||||
|
await db.execute('''
|
||||||
|
CREATE TABLE $contactsTable (
|
||||||
|
id TEXT PRIMARY KEY,
|
||||||
|
jid TEXT NOT NULL
|
||||||
|
)''');
|
||||||
|
|
||||||
|
// Migrate the conversations
|
||||||
|
await db.execute(
|
||||||
|
'''
|
||||||
|
CREATE TABLE ${conversationsTable}_new (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
jid TEXT NOT NULL,
|
||||||
|
title TEXT NOT NULL,
|
||||||
|
avatarUrl TEXT NOT NULL,
|
||||||
|
lastChangeTimestamp INTEGER NOT NULL,
|
||||||
|
unreadCounter INTEGER NOT NULL,
|
||||||
|
open INTEGER NOT NULL,
|
||||||
|
muted INTEGER NOT NULL,
|
||||||
|
encrypted INTEGER NOT NULL,
|
||||||
|
lastMessageId INTEGER,
|
||||||
|
contactId TEXT,
|
||||||
|
CONSTRAINT fk_last_message FOREIGN KEY (lastMessageId) REFERENCES $messagesTable (id),
|
||||||
|
CONSTRAINT fk_contact_id FOREIGN KEY (contactId) REFERENCES $contactsTable (id)
|
||||||
|
ON DELETE SET NULL
|
||||||
|
)''',
|
||||||
|
);
|
||||||
|
await db.execute(
|
||||||
|
'INSERT INTO ${conversationsTable}_new SELECT *, NULL from $conversationsTable',
|
||||||
|
);
|
||||||
|
await db.execute('DROP TABLE $conversationsTable;');
|
||||||
|
await db.execute(
|
||||||
|
'ALTER TABLE ${conversationsTable}_new RENAME TO $conversationsTable;',
|
||||||
|
);
|
||||||
|
|
||||||
|
// Migrate the roster items
|
||||||
|
await db.execute(
|
||||||
|
'''
|
||||||
|
CREATE TABLE ${rosterTable}_new (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
jid TEXT NOT NULL,
|
||||||
|
title TEXT NOT NULL,
|
||||||
|
avatarUrl TEXT NOT NULL,
|
||||||
|
avatarHash TEXT NOT NULL,
|
||||||
|
subscription TEXT NOT NULL,
|
||||||
|
ask TEXT NOT NULL,
|
||||||
|
contactId TEXT,
|
||||||
|
CONSTRAINT fk_contact_id FOREIGN KEY (contactId) REFERENCES $contactsTable (id)
|
||||||
|
ON DELETE SET NULL
|
||||||
|
)''',
|
||||||
|
);
|
||||||
|
await db.execute(
|
||||||
|
'INSERT INTO ${rosterTable}_new SELECT *, NULL from $rosterTable',
|
||||||
|
);
|
||||||
|
await db.execute('DROP TABLE $rosterTable;');
|
||||||
|
await db.execute('ALTER TABLE ${rosterTable}_new RENAME TO $rosterTable;');
|
||||||
|
|
||||||
|
// Introduce the new preference key
|
||||||
|
await db.insert(
|
||||||
|
preferenceTable,
|
||||||
|
Preference(
|
||||||
|
'enableContactIntegration',
|
||||||
|
typeBool,
|
||||||
|
'false',
|
||||||
|
).toDatabaseJson(),
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import 'package:moxxyv2/service/database/constants.dart';
|
||||||
|
import 'package:sqflite_sqlcipher/sqflite.dart';
|
||||||
|
|
||||||
|
Future<void> upgradeFromV14ToV15(Database db) async {
|
||||||
|
await db.execute(
|
||||||
|
'ALTER TABLE $conversationsTable ADD COLUMN contactAvatarPath TEXT DEFAULT NULL;',
|
||||||
|
);
|
||||||
|
await db.execute(
|
||||||
|
'ALTER TABLE $rosterTable ADD COLUMN contactAvatarPath TEXT DEFAULT NULL;',
|
||||||
|
);
|
||||||
|
await db.execute(
|
||||||
|
'ALTER TABLE $conversationsTable ADD COLUMN contactDisplayName TEXT DEFAULT NULL;',
|
||||||
|
);
|
||||||
|
await db.execute(
|
||||||
|
'ALTER TABLE $rosterTable ADD COLUMN contactDisplayName TEXT DEFAULT NULL;',
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import 'package:moxxyv2/service/database/constants.dart';
|
||||||
|
import 'package:moxxyv2/service/database/helpers.dart';
|
||||||
|
import 'package:sqflite_sqlcipher/sqflite.dart';
|
||||||
|
|
||||||
|
Future<void> upgradeFromV15ToV16(Database db) async {
|
||||||
|
await db.execute(
|
||||||
|
'ALTER TABLE $rosterTable ADD COLUMN pseudoRosterItem INTEGER NOT NULL DEFAULT ${boolToInt(false)};',
|
||||||
|
);
|
||||||
|
}
|
||||||
11
lib/service/database/migrations/0000_conversations.dart
Normal file
11
lib/service/database/migrations/0000_conversations.dart
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import 'package:moxxyv2/service/database/constants.dart';
|
||||||
|
import 'package:sqflite_sqlcipher/sqflite.dart';
|
||||||
|
|
||||||
|
Future<void> upgradeFromV6ToV7(Database db) async {
|
||||||
|
await db.execute(
|
||||||
|
'ALTER TABLE $conversationsTable ADD COLUMN lastMessageState INTEGER NOT NULL DEFAULT 0;',
|
||||||
|
);
|
||||||
|
await db.execute(
|
||||||
|
"ALTER TABLE $conversationsTable ADD COLUMN lastMessageSender TEXT NOT NULL DEFAULT '';",
|
||||||
|
);
|
||||||
|
}
|
||||||
17
lib/service/database/migrations/0000_conversations2.dart
Normal file
17
lib/service/database/migrations/0000_conversations2.dart
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import 'package:moxxyv2/service/database/constants.dart';
|
||||||
|
import 'package:sqflite_sqlcipher/sqflite.dart';
|
||||||
|
|
||||||
|
Future<void> upgradeFromV7ToV8(Database db) async {
|
||||||
|
await db.execute(
|
||||||
|
'ALTER TABLE $conversationsTable DROP COLUMN lastMessageState;',
|
||||||
|
);
|
||||||
|
await db.execute(
|
||||||
|
'ALTER TABLE $conversationsTable DROP COLUMN lastMessageSender;',
|
||||||
|
);
|
||||||
|
await db.execute(
|
||||||
|
'ALTER TABLE $conversationsTable DROP COLUMN lastMessageBody;',
|
||||||
|
);
|
||||||
|
await db.execute(
|
||||||
|
'ALTER TABLE $conversationsTable DROP COLUMN lastMessageRetracted;',
|
||||||
|
);
|
||||||
|
}
|
||||||
47
lib/service/database/migrations/0000_conversations3.dart
Normal file
47
lib/service/database/migrations/0000_conversations3.dart
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
import 'package:moxxyv2/service/database/constants.dart';
|
||||||
|
import 'package:sqflite_sqlcipher/sqflite.dart';
|
||||||
|
|
||||||
|
Future<void> upgradeFromV8ToV9(Database db) async {
|
||||||
|
// Step 1
|
||||||
|
//await db.execute('PRAGMA foreign_keys = 0;');
|
||||||
|
|
||||||
|
// Step 2
|
||||||
|
// Step 4
|
||||||
|
await db.execute(
|
||||||
|
'''
|
||||||
|
CREATE TABLE ${conversationsTable}_new (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
jid TEXT NOT NULL,
|
||||||
|
title TEXT NOT NULL,
|
||||||
|
avatarUrl TEXT NOT NULL,
|
||||||
|
lastChangeTimestamp INTEGER NOT NULL,
|
||||||
|
unreadCounter INTEGER NOT NULL,
|
||||||
|
open INTEGER NOT NULL,
|
||||||
|
muted INTEGER NOT NULL,
|
||||||
|
encrypted INTEGER NOT NULL,
|
||||||
|
lastMessageId INTEGER,
|
||||||
|
CONSTRAINT fk_last_message FOREIGN KEY (lastMessageId) REFERENCES $messagesTable (id)
|
||||||
|
)''',
|
||||||
|
);
|
||||||
|
|
||||||
|
// Step 5
|
||||||
|
await db.execute(
|
||||||
|
'INSERT INTO ${conversationsTable}_new SELECT * from $conversationsTable',
|
||||||
|
);
|
||||||
|
|
||||||
|
// Step 6
|
||||||
|
await db.execute('DROP TABLE $conversationsTable;');
|
||||||
|
|
||||||
|
// Step 7
|
||||||
|
await db.execute(
|
||||||
|
'ALTER TABLE ${conversationsTable}_new RENAME TO $conversationsTable;',
|
||||||
|
);
|
||||||
|
|
||||||
|
// Step 10
|
||||||
|
//await db.execute('PRAGMA foreign_key_check;');
|
||||||
|
|
||||||
|
// Step 11
|
||||||
|
|
||||||
|
// Step 12
|
||||||
|
//await db.execute('PRAGMA foreign_keys=ON;');
|
||||||
|
}
|
||||||
10
lib/service/database/migrations/0000_lmc.dart
Normal file
10
lib/service/database/migrations/0000_lmc.dart
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import 'package:moxxyv2/service/database/constants.dart';
|
||||||
|
import 'package:moxxyv2/service/database/helpers.dart';
|
||||||
|
import 'package:sqflite_sqlcipher/sqflite.dart';
|
||||||
|
|
||||||
|
Future<void> upgradeFromV9ToV10(Database db) async {
|
||||||
|
// Mark all messages as not edited
|
||||||
|
await db.execute(
|
||||||
|
'ALTER TABLE $messagesTable ADD COLUMN isEdited INTEGER NOT NULL DEFAULT ${boolToInt(false)};',
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import 'package:sqflite_sqlcipher/sqflite.dart';
|
||||||
|
|
||||||
|
Future<void> upgradeFromV12ToV13(Database db) async {
|
||||||
|
await db.execute(
|
||||||
|
'''
|
||||||
|
CREATE TABLE OmemoFingerprintCache (
|
||||||
|
jid TEXT NOT NULL,
|
||||||
|
id INTEGER NOT NULL,
|
||||||
|
fingerprint TEXT NOT NULL,
|
||||||
|
PRIMARY KEY (jid, id)
|
||||||
|
)''',
|
||||||
|
);
|
||||||
|
}
|
||||||
11
lib/service/database/migrations/0000_pseudo_messages.dart
Normal file
11
lib/service/database/migrations/0000_pseudo_messages.dart
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import 'package:moxxyv2/service/database/constants.dart';
|
||||||
|
import 'package:sqflite_sqlcipher/sqflite.dart';
|
||||||
|
|
||||||
|
Future<void> upgradeFromV23ToV24(Database db) async {
|
||||||
|
await db.execute(
|
||||||
|
'ALTER TABLE $messagesTable ADD COLUMN pseudoMessageType INTEGER;',
|
||||||
|
);
|
||||||
|
await db.execute(
|
||||||
|
'ALTER TABLE $messagesTable ADD COLUMN pseudoMessageData TEXT;',
|
||||||
|
);
|
||||||
|
}
|
||||||
8
lib/service/database/migrations/0000_reactions.dart
Normal file
8
lib/service/database/migrations/0000_reactions.dart
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
import 'package:moxxyv2/service/database/constants.dart';
|
||||||
|
import 'package:sqflite_sqlcipher/sqflite.dart';
|
||||||
|
|
||||||
|
Future<void> upgradeFromV10ToV11(Database db) async {
|
||||||
|
await db.execute(
|
||||||
|
"ALTER TABLE $messagesTable ADD COLUMN reactions TEXT NOT NULL DEFAULT '[]';",
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import 'package:moxxyv2/service/database/constants.dart';
|
||||||
|
import 'package:moxxyv2/service/database/helpers.dart';
|
||||||
|
import 'package:sqflite_sqlcipher/sqflite.dart';
|
||||||
|
|
||||||
|
Future<void> upgradeFromV11ToV12(Database db) async {
|
||||||
|
await db.execute(
|
||||||
|
'ALTER TABLE $messagesTable ADD COLUMN containsNoStore INTEGER NOT NULL DEFAULT ${boolToInt(false)};',
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,11 +1,10 @@
|
|||||||
import 'package:moxxyv2/service/database/constants.dart';
|
import 'package:moxxyv2/service/database/constants.dart';
|
||||||
import 'package:moxxyv2/service/database/helpers.dart';
|
import 'package:moxxyv2/service/database/helpers.dart';
|
||||||
import 'package:moxxyv2/shared/models/preference.dart';
|
|
||||||
import 'package:sqflite_sqlcipher/sqflite.dart';
|
import 'package:sqflite_sqlcipher/sqflite.dart';
|
||||||
|
|
||||||
Future<void> upgradeFromV3ToV4(Database db) async {
|
Future<void> upgradeFromV3ToV4(Database db) async {
|
||||||
// Mark all messages as not retracted
|
// Mark all messages as not retracted
|
||||||
await db.execute(
|
await db.execute(
|
||||||
'ALTER TABLE $messsagesTable ADD COLUMN isRetracted INTEGER DEFAULT ${boolToInt(false)};',
|
'ALTER TABLE $messagesTable ADD COLUMN isRetracted INTEGER DEFAULT ${boolToInt(false)};',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import 'package:moxxyv2/service/database/constants.dart';
|
import 'package:moxxyv2/service/database/constants.dart';
|
||||||
import 'package:moxxyv2/service/database/helpers.dart';
|
import 'package:moxxyv2/service/database/helpers.dart';
|
||||||
import 'package:moxxyv2/shared/models/preference.dart';
|
|
||||||
import 'package:sqflite_sqlcipher/sqflite.dart';
|
import 'package:sqflite_sqlcipher/sqflite.dart';
|
||||||
|
|
||||||
Future<void> upgradeFromV4ToV5(Database db) async {
|
Future<void> upgradeFromV4ToV5(Database db) async {
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
import 'package:moxxyv2/service/database/constants.dart';
|
import 'package:moxxyv2/service/database/constants.dart';
|
||||||
import 'package:moxxyv2/shared/models/preference.dart';
|
|
||||||
import 'package:sqflite_sqlcipher/sqflite.dart';
|
import 'package:sqflite_sqlcipher/sqflite.dart';
|
||||||
|
|
||||||
Future<void> upgradeFromV5ToV6(Database db) async {
|
Future<void> upgradeFromV5ToV6(Database db) async {
|
||||||
// Allow shared media to reference a message
|
// Allow shared media to reference a message
|
||||||
await db.execute(
|
await db.execute(
|
||||||
'ALTER TABLE $mediaTable ADD COLUMN message_id INTEGER REFERENCES $messsagesTable (id);',
|
'ALTER TABLE $mediaTable ADD COLUMN message_id INTEGER REFERENCES $messagesTable (id);',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
59
lib/service/database/migrations/0000_stickers.dart
Normal file
59
lib/service/database/migrations/0000_stickers.dart
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
import 'package:moxxyv2/service/database/constants.dart';
|
||||||
|
import 'package:moxxyv2/shared/models/preference.dart';
|
||||||
|
import 'package:sqflite_sqlcipher/sqflite.dart';
|
||||||
|
|
||||||
|
Future<void> upgradeFromV16ToV17(Database db) async {
|
||||||
|
await db.execute(
|
||||||
|
'''
|
||||||
|
CREATE TABLE $stickersTable (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
mediaType TEXT NOT NULL,
|
||||||
|
desc TEXT NOT NULL,
|
||||||
|
size INTEGER NOT NULL,
|
||||||
|
width INTEGER,
|
||||||
|
height INTEGER,
|
||||||
|
hashes TEXT NOT NULL,
|
||||||
|
urlSources TEXT NOT NULL,
|
||||||
|
path TEXT NOT NULL,
|
||||||
|
stickerPackId TEXT NOT NULL,
|
||||||
|
CONSTRAINT fk_sticker_pack FOREIGN KEY (stickerPackId) REFERENCES $stickerPacksTable (id)
|
||||||
|
ON DELETE CASCADE
|
||||||
|
)''',
|
||||||
|
);
|
||||||
|
await db.execute(
|
||||||
|
'''
|
||||||
|
CREATE TABLE $stickerPacksTable (
|
||||||
|
id TEXT PRIMARY KEY,
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
description TEXT NOT NULL,
|
||||||
|
hashAlgorithm TEXT NOT NULL,
|
||||||
|
hashValue TEXT NOT NULL
|
||||||
|
)''',
|
||||||
|
);
|
||||||
|
|
||||||
|
// Add the sticker attributes to Messages
|
||||||
|
await db.execute(
|
||||||
|
'ALTER TABLE $messagesTable ADD COLUMN stickerPackId TEXT;',
|
||||||
|
);
|
||||||
|
await db.execute(
|
||||||
|
'ALTER TABLE $messagesTable ADD COLUMN stickerId INTEGER;',
|
||||||
|
);
|
||||||
|
|
||||||
|
// Add the new preferences
|
||||||
|
await db.insert(
|
||||||
|
preferenceTable,
|
||||||
|
Preference(
|
||||||
|
'enableStickers',
|
||||||
|
typeBool,
|
||||||
|
'true',
|
||||||
|
).toDatabaseJson(),
|
||||||
|
);
|
||||||
|
await db.insert(
|
||||||
|
preferenceTable,
|
||||||
|
Preference(
|
||||||
|
'autoDownloadStickersFromContacts',
|
||||||
|
typeBool,
|
||||||
|
'true',
|
||||||
|
).toDatabaseJson(),
|
||||||
|
);
|
||||||
|
}
|
||||||
45
lib/service/database/migrations/0000_stickers_hash_key.dart
Normal file
45
lib/service/database/migrations/0000_stickers_hash_key.dart
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
import 'package:moxxyv2/service/database/constants.dart';
|
||||||
|
import 'package:sqflite_sqlcipher/sqflite.dart';
|
||||||
|
|
||||||
|
Future<void> upgradeFromV17ToV18(Database db) async {
|
||||||
|
// Update messages
|
||||||
|
await db.execute(
|
||||||
|
'ALTER TABLE $messagesTable DROP COLUMN stickerId;',
|
||||||
|
);
|
||||||
|
await db.execute(
|
||||||
|
'ALTER TABLE $messagesTable ADD COLUMN stickerHashKey TEXT;',
|
||||||
|
);
|
||||||
|
|
||||||
|
// Drop stickers
|
||||||
|
await db.execute('DROP TABLE $stickerPacksTable;');
|
||||||
|
await db.execute('DROP TABLE $stickersTable;');
|
||||||
|
|
||||||
|
await db.execute(
|
||||||
|
'''
|
||||||
|
CREATE TABLE $stickersTable (
|
||||||
|
hashKey TEXT PRIMARY KEY,
|
||||||
|
mediaType TEXT NOT NULL,
|
||||||
|
desc TEXT NOT NULL,
|
||||||
|
size INTEGER NOT NULL,
|
||||||
|
width INTEGER,
|
||||||
|
height INTEGER,
|
||||||
|
hashes TEXT NOT NULL,
|
||||||
|
urlSources TEXT NOT NULL,
|
||||||
|
path TEXT NOT NULL,
|
||||||
|
stickerPackId TEXT NOT NULL,
|
||||||
|
CONSTRAINT fk_sticker_pack FOREIGN KEY (stickerPackId) REFERENCES $stickerPacksTable (id)
|
||||||
|
ON DELETE CASCADE
|
||||||
|
)''',
|
||||||
|
);
|
||||||
|
await db.execute(
|
||||||
|
'''
|
||||||
|
CREATE TABLE $stickerPacksTable (
|
||||||
|
id TEXT PRIMARY KEY,
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
description TEXT NOT NULL,
|
||||||
|
hashAlgorithm TEXT NOT NULL,
|
||||||
|
hashValue TEXT NOT NULL,
|
||||||
|
stickerHashKey TEXT NOT NULL
|
||||||
|
)''',
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
import 'package:moxxyv2/service/database/constants.dart';
|
||||||
|
import 'package:sqflite_sqlcipher/sqflite.dart';
|
||||||
|
|
||||||
|
Future<void> upgradeFromV18ToV19(Database db) async {
|
||||||
|
await db.execute(
|
||||||
|
'ALTER TABLE $stickerPacksTable DROP COLUMN stickerHashKey;',
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
import 'package:moxxyv2/service/database/constants.dart';
|
||||||
|
import 'package:moxxyv2/service/database/helpers.dart';
|
||||||
|
import 'package:sqflite_sqlcipher/sqflite.dart';
|
||||||
|
|
||||||
|
Future<void> upgradeFromV19ToV20(Database db) async {
|
||||||
|
await db.execute(
|
||||||
|
'ALTER TABLE $stickerPacksTable ADD COLUMN restricted DEFAULT ${boolToInt(false)};',
|
||||||
|
);
|
||||||
|
await db.execute(
|
||||||
|
'ALTER TABLE $stickersTable ADD COLUMN suggests DEFAULT "";',
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
import 'package:moxxyv2/service/database/constants.dart';
|
||||||
|
import 'package:moxxyv2/service/database/helpers.dart';
|
||||||
|
import 'package:sqflite_sqlcipher/sqflite.dart';
|
||||||
|
|
||||||
|
Future<void> upgradeFromV20ToV21(Database db) async {
|
||||||
|
await db.execute(
|
||||||
|
'ALTER TABLE $stickerPacksTable DROP COLUMN restricted;',
|
||||||
|
);
|
||||||
|
await db.execute(
|
||||||
|
'ALTER TABLE $stickersTable DROP COLUMN suggests;',
|
||||||
|
);
|
||||||
|
|
||||||
|
await db.execute(
|
||||||
|
'ALTER TABLE $stickerPacksTable ADD COLUMN restricted INTEGER NOT NULL DEFAULT ${boolToInt(false)};',
|
||||||
|
);
|
||||||
|
await db.execute(
|
||||||
|
'ALTER TABLE $stickersTable ADD COLUMN suggests TEXT NOT NULL DEFAULT "";',
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
import 'package:moxxyv2/service/database/constants.dart';
|
||||||
|
import 'package:sqflite_sqlcipher/sqflite.dart';
|
||||||
|
|
||||||
|
Future<void> upgradeFromV21ToV22(Database db) async {
|
||||||
|
await db.execute(
|
||||||
|
'ALTER TABLE $stickersTable DROP COLUMN suggests;',
|
||||||
|
);
|
||||||
|
|
||||||
|
await db.execute(
|
||||||
|
'ALTER TABLE $stickersTable ADD COLUMN suggests TEXT NOT NULL DEFAULT "{}";',
|
||||||
|
);
|
||||||
|
}
|
||||||
14
lib/service/database/migrations/0000_stickers_privacy.dart
Normal file
14
lib/service/database/migrations/0000_stickers_privacy.dart
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import 'package:moxxyv2/service/database/constants.dart';
|
||||||
|
import 'package:moxxyv2/shared/models/preference.dart';
|
||||||
|
import 'package:sqflite_sqlcipher/sqflite.dart';
|
||||||
|
|
||||||
|
Future<void> upgradeFromV24ToV25(Database db) async {
|
||||||
|
await db.insert(
|
||||||
|
preferenceTable,
|
||||||
|
Preference(
|
||||||
|
'isStickersNodePublic',
|
||||||
|
typeBool,
|
||||||
|
'true',
|
||||||
|
).toDatabaseJson(),
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
import 'package:moxxyv2/service/database/constants.dart';
|
||||||
|
import 'package:sqflite_sqlcipher/sqflite.dart';
|
||||||
|
|
||||||
|
Future<void> upgradeFromV29ToV30(Database db) async {
|
||||||
|
await db.execute(
|
||||||
|
'ALTER TABLE $conversationsTable ADD COLUMN sharedMediaAmount INTEGER NOT NULL DEFAULT 0;',
|
||||||
|
);
|
||||||
|
|
||||||
|
// Get all conversations
|
||||||
|
final conversations = await db.query(
|
||||||
|
conversationsTable,
|
||||||
|
);
|
||||||
|
|
||||||
|
for (final conversation in conversations) {
|
||||||
|
// Count the amount of shared media
|
||||||
|
final jid = conversation['jid']! as String;
|
||||||
|
final result = Sqflite.firstIntValue(
|
||||||
|
await db.rawQuery(
|
||||||
|
'SELECT COUNT(*) FROM $mediaTable WHERE conversation_jid = ?',
|
||||||
|
[jid],
|
||||||
|
),
|
||||||
|
) ??
|
||||||
|
0;
|
||||||
|
|
||||||
|
final c = Map<String, Object?>.from(conversation)..remove('id');
|
||||||
|
await db.update(
|
||||||
|
conversationsTable,
|
||||||
|
{
|
||||||
|
...c,
|
||||||
|
'sharedMediaAmount': result,
|
||||||
|
},
|
||||||
|
where: 'jid = ?',
|
||||||
|
whereArgs: [jid],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
import 'package:moxxyv2/service/database/constants.dart';
|
||||||
|
import 'package:sqflite_sqlcipher/sqflite.dart';
|
||||||
|
|
||||||
|
Future<void> upgradeFromV27ToV28(Database db) async {
|
||||||
|
// Collect conversations so that we have a mapping id -> jid
|
||||||
|
final idMap = <int, String>{};
|
||||||
|
final conversations = await db.query(conversationsTable);
|
||||||
|
for (final c in conversations) {
|
||||||
|
idMap[c['id']! as int] = c['jid']! as String;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Migrate the conversations
|
||||||
|
await db.execute(
|
||||||
|
'''
|
||||||
|
CREATE TABLE ${conversationsTable}_new (
|
||||||
|
jid TEXT NOT NULL PRIMARY KEY,
|
||||||
|
title TEXT NOT NULL,
|
||||||
|
avatarUrl TEXT NOT NULL,
|
||||||
|
lastChangeTimestamp INTEGER NOT NULL,
|
||||||
|
unreadCounter INTEGER NOT NULL,
|
||||||
|
open INTEGER NOT NULL,
|
||||||
|
muted INTEGER NOT NULL,
|
||||||
|
encrypted INTEGER NOT NULL,
|
||||||
|
lastMessageId INTEGER,
|
||||||
|
contactId TEXT,
|
||||||
|
contactAvatarPath TEXT,
|
||||||
|
contactDisplayName TEXT,
|
||||||
|
CONSTRAINT fk_last_message FOREIGN KEY (lastMessageId) REFERENCES $messagesTable (id),
|
||||||
|
CONSTRAINT fk_contact_id FOREIGN KEY (contactId) REFERENCES $contactsTable (id)
|
||||||
|
ON DELETE SET NULL
|
||||||
|
)''',
|
||||||
|
);
|
||||||
|
await db.execute(
|
||||||
|
'INSERT INTO ${conversationsTable}_new SELECT jid, title, avatarUrl, lastChangeTimestamp, unreadCounter, open, muted, encrypted, lastMessageId, contactid, contactAvatarPath, contactDisplayName from $conversationsTable',
|
||||||
|
);
|
||||||
|
await db.execute('DROP TABLE $conversationsTable;');
|
||||||
|
await db.execute(
|
||||||
|
'ALTER TABLE ${conversationsTable}_new RENAME TO $conversationsTable;',
|
||||||
|
);
|
||||||
|
|
||||||
|
// Add the jid column to shared media
|
||||||
|
await db.execute(
|
||||||
|
"ALTER TABLE $mediaTable ADD COLUMN conversation_jid TEXT NOT NULL DEFAULT '';",
|
||||||
|
);
|
||||||
|
|
||||||
|
// Update all shared media items
|
||||||
|
for (final entry in idMap.entries) {
|
||||||
|
await db.update(
|
||||||
|
mediaTable,
|
||||||
|
{
|
||||||
|
'conversation_jid': entry.value,
|
||||||
|
},
|
||||||
|
where: 'conversation_id = ?',
|
||||||
|
whereArgs: [entry.key],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Migrate shared media
|
||||||
|
await db.execute(
|
||||||
|
'''
|
||||||
|
CREATE TABLE ${mediaTable}_new (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
path TEXT NOT NULL,
|
||||||
|
mime TEXT,
|
||||||
|
timestamp INTEGER NOT NULL,
|
||||||
|
conversation_jid TEXT NOT NULL,
|
||||||
|
message_id INTEGER,
|
||||||
|
FOREIGN KEY (conversation_jid) REFERENCES $conversationsTable (jid),
|
||||||
|
FOREIGN KEY (message_id) REFERENCES $messagesTable (id)
|
||||||
|
)''',
|
||||||
|
);
|
||||||
|
await db.execute(
|
||||||
|
'INSERT INTO ${mediaTable}_new SELECT id, path, mime, timestamp, message_id, conversation_jid from $mediaTable',
|
||||||
|
);
|
||||||
|
await db.execute('DROP TABLE $mediaTable;');
|
||||||
|
await db.execute('ALTER TABLE ${mediaTable}_new RENAME TO $mediaTable;');
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
import 'package:moxxyv2/service/database/constants.dart';
|
||||||
|
import 'package:sqflite_sqlcipher/sqflite.dart';
|
||||||
|
|
||||||
|
Future<void> upgradeFromV30ToV31(Database db) async {
|
||||||
|
await db.execute(
|
||||||
|
'ALTER TABLE $conversationsTable ADD COLUMN type TEXT NOT NULL DEFAULT "chat";',
|
||||||
|
);
|
||||||
|
}
|
||||||
15
lib/service/database/migrations/0001_debug_menu.dart
Normal file
15
lib/service/database/migrations/0001_debug_menu.dart
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import 'package:moxxyv2/service/database/constants.dart';
|
||||||
|
import 'package:moxxyv2/service/database/helpers.dart';
|
||||||
|
import 'package:moxxyv2/shared/models/preference.dart';
|
||||||
|
import 'package:sqflite_sqlcipher/sqflite.dart';
|
||||||
|
|
||||||
|
Future<void> upgradeFromV25ToV26(Database db) async {
|
||||||
|
await db.insert(
|
||||||
|
preferenceTable,
|
||||||
|
Preference(
|
||||||
|
'showDebugMenu',
|
||||||
|
typeBool,
|
||||||
|
boolToString(false),
|
||||||
|
).toDatabaseJson(),
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import 'package:moxxyv2/service/database/constants.dart';
|
||||||
|
import 'package:sqflite_sqlcipher/sqflite.dart';
|
||||||
|
|
||||||
|
Future<void> upgradeFromV28ToV29(Database db) async {
|
||||||
|
await db.delete(
|
||||||
|
preferenceTable,
|
||||||
|
where: 'key = "autoAcceptSubscriptionRequests"',
|
||||||
|
);
|
||||||
|
}
|
||||||
9
lib/service/database/migrations/0001_subscriptions.dart
Normal file
9
lib/service/database/migrations/0001_subscriptions.dart
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import 'package:moxxyv2/service/database/constants.dart';
|
||||||
|
import 'package:sqflite_sqlcipher/sqflite.dart';
|
||||||
|
|
||||||
|
Future<void> upgradeFromV26ToV27(Database db) async {
|
||||||
|
await db.execute('''
|
||||||
|
CREATE TABLE $subscriptionsTable(
|
||||||
|
jid TEXT PRIMARY KEY
|
||||||
|
)''');
|
||||||
|
}
|
||||||
226
lib/service/database/migrations/0002_file_metadata_table.dart
Normal file
226
lib/service/database/migrations/0002_file_metadata_table.dart
Normal file
@@ -0,0 +1,226 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
import 'package:moxxmpp/moxxmpp.dart';
|
||||||
|
import 'package:moxxyv2/service/database/constants.dart';
|
||||||
|
import 'package:moxxyv2/service/database/helpers.dart';
|
||||||
|
import 'package:moxxyv2/service/files.dart';
|
||||||
|
import 'package:moxxyv2/shared/models/file_metadata.dart';
|
||||||
|
import 'package:path/path.dart' as path;
|
||||||
|
import 'package:sqflite_sqlcipher/sqflite.dart';
|
||||||
|
|
||||||
|
Future<void> upgradeFromV31ToV32(Database db) async {
|
||||||
|
// Create the tracking table
|
||||||
|
await db.execute('''
|
||||||
|
CREATE TABLE $fileMetadataTable (
|
||||||
|
id TEXT NOT NULL PRIMARY KEY,
|
||||||
|
path TEXT,
|
||||||
|
sourceUrls TEXT,
|
||||||
|
mimeType TEXT,
|
||||||
|
thumbnailType TEXT,
|
||||||
|
thumbnailData TEXT,
|
||||||
|
width INTEGER,
|
||||||
|
height INTEGER,
|
||||||
|
plaintextHashes TEXT,
|
||||||
|
encryptionKey TEXT,
|
||||||
|
encryptionIv TEXT,
|
||||||
|
encryptionScheme TEXT,
|
||||||
|
cipherTextHashes TEXT,
|
||||||
|
filename TEXT NOT NULL,
|
||||||
|
size INTEGER
|
||||||
|
)''');
|
||||||
|
await db.execute('''
|
||||||
|
CREATE TABLE $fileMetadataHashesTable (
|
||||||
|
algorithm TEXT NOT NULL,
|
||||||
|
value TEXT NOT NULL,
|
||||||
|
id TEXT NOT NULL,
|
||||||
|
CONSTRAINT f_primarykey PRIMARY KEY (algorithm, value),
|
||||||
|
CONSTRAINT fk_id FOREIGN KEY (id) REFERENCES $fileMetadataTable (id)
|
||||||
|
ON DELETE CASCADE
|
||||||
|
)''');
|
||||||
|
|
||||||
|
// Add the file_metadata_id column
|
||||||
|
await db.execute(
|
||||||
|
'ALTER TABLE $messagesTable ADD COLUMN file_metadata_id TEXT DEFAULT NULL;',
|
||||||
|
);
|
||||||
|
|
||||||
|
// Migrate the media messages' attributes to new table
|
||||||
|
final messages = await db.query(
|
||||||
|
messagesTable,
|
||||||
|
where: 'isMedia = ${boolToInt(true)}',
|
||||||
|
);
|
||||||
|
for (final message in messages) {
|
||||||
|
// Do we know of a hash?
|
||||||
|
String id;
|
||||||
|
if (message['plaintextHashes'] != null) {
|
||||||
|
// Plaintext hashes available (SFS)
|
||||||
|
final plaintextHashes = deserializeHashMap(
|
||||||
|
message['plaintextHashes']! as String,
|
||||||
|
);
|
||||||
|
final result = await db.query(
|
||||||
|
fileMetadataHashesTable,
|
||||||
|
where: 'algorithm = ? AND value = ?',
|
||||||
|
whereArgs: [
|
||||||
|
plaintextHashes.entries.first.key,
|
||||||
|
plaintextHashes.entries.first.value,
|
||||||
|
],
|
||||||
|
limit: 1,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (result.isEmpty) {
|
||||||
|
final metadata = FileMetadata(
|
||||||
|
getStrongestHashFromMap(plaintextHashes) ??
|
||||||
|
DateTime.now().millisecondsSinceEpoch.toString(),
|
||||||
|
message['mediaUrl'] as String?,
|
||||||
|
message['srcUrl'] != null ? [message['srcUrl']! as String] : null,
|
||||||
|
message['mediaType'] as String?,
|
||||||
|
message['mediaSize'] as int?,
|
||||||
|
message['thumbnailData'] != null ? 'blurhash' : null,
|
||||||
|
message['thumbnailData'] as String?,
|
||||||
|
message['mediaWidth'] as int?,
|
||||||
|
message['mediaHeight'] as int?,
|
||||||
|
plaintextHashes,
|
||||||
|
message['key'] as String?,
|
||||||
|
message['iv'] as String?,
|
||||||
|
message['encryptionScheme'] as String?,
|
||||||
|
message['plaintextHashes'] == null
|
||||||
|
? null
|
||||||
|
: deserializeHashMap(message['ciphertextHashes']! as String),
|
||||||
|
message['filename']! as String,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Create the metadata
|
||||||
|
await db.insert(
|
||||||
|
fileMetadataTable,
|
||||||
|
metadata.toDatabaseJson(),
|
||||||
|
);
|
||||||
|
id = metadata.id;
|
||||||
|
} else {
|
||||||
|
id = result[0]['id']! as String;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// No plaintext hashes are available (OOB data)
|
||||||
|
int? size;
|
||||||
|
int? height;
|
||||||
|
int? width;
|
||||||
|
Map<HashFunction, String>? hashes;
|
||||||
|
String? filePath;
|
||||||
|
String? urlSource;
|
||||||
|
String? mediaType;
|
||||||
|
String? filename;
|
||||||
|
if (message['filename'] == null) {
|
||||||
|
// We are dealing with a sticker
|
||||||
|
assert(
|
||||||
|
message['stickerPackId'] != null,
|
||||||
|
'The message must contain a sticker',
|
||||||
|
);
|
||||||
|
assert(
|
||||||
|
message['stickerHashKey'] != null,
|
||||||
|
'The message must contain a sticker',
|
||||||
|
);
|
||||||
|
final sticker = (await db.query(
|
||||||
|
stickersTable,
|
||||||
|
where: 'stickerPackId = ? AND hashKey = ?',
|
||||||
|
whereArgs: [message['stickerPackId'], message['stickerHashKey']],
|
||||||
|
limit: 1,
|
||||||
|
))
|
||||||
|
.first;
|
||||||
|
size = sticker['size']! as int;
|
||||||
|
width = sticker['width'] as int?;
|
||||||
|
height = sticker['height'] as int?;
|
||||||
|
hashes = deserializeHashMap(sticker['hashes']! as String);
|
||||||
|
filePath = sticker['path']! as String;
|
||||||
|
urlSource =
|
||||||
|
((jsonDecode(sticker['urlSources']! as String) as List<dynamic>)
|
||||||
|
.cast<String>())
|
||||||
|
.first;
|
||||||
|
mediaType = sticker['mediaType']! as String;
|
||||||
|
filename = path.basename(sticker['path']! as String);
|
||||||
|
} else {
|
||||||
|
size = message['mediaSize'] as int?;
|
||||||
|
width = message['mediaWidth'] as int?;
|
||||||
|
height = message['mediaHeight'] as int?;
|
||||||
|
filePath = message['mediaUrl'] as String?;
|
||||||
|
urlSource = message['srcUrl'] as String?;
|
||||||
|
mediaType = message['mediaType'] as String?;
|
||||||
|
filename = message['filename'] as String?;
|
||||||
|
}
|
||||||
|
|
||||||
|
final metadata = FileMetadata(
|
||||||
|
DateTime.now().millisecondsSinceEpoch.toString(),
|
||||||
|
filePath,
|
||||||
|
urlSource != null ? [urlSource] : null,
|
||||||
|
mediaType,
|
||||||
|
size,
|
||||||
|
message['thumbnailData'] != null ? 'blurhash' : null,
|
||||||
|
message['thumbnailData'] as String?,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
hashes,
|
||||||
|
message['key'] as String?,
|
||||||
|
message['iv'] as String?,
|
||||||
|
message['encryptionScheme'] as String?,
|
||||||
|
null,
|
||||||
|
filename!,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Create the metadata
|
||||||
|
await db.insert(
|
||||||
|
fileMetadataTable,
|
||||||
|
metadata.toDatabaseJson(),
|
||||||
|
);
|
||||||
|
id = metadata.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the message
|
||||||
|
await db.update(
|
||||||
|
messagesTable,
|
||||||
|
{
|
||||||
|
'file_metadata_id': id,
|
||||||
|
},
|
||||||
|
where: 'id = ?',
|
||||||
|
whereArgs: [message['id']],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove columns and add foreign key
|
||||||
|
await db.execute(
|
||||||
|
'''
|
||||||
|
CREATE TABLE ${messagesTable}_new (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
sender TEXT NOT NULL,
|
||||||
|
body TEXT,
|
||||||
|
timestamp INTEGER NOT NULL,
|
||||||
|
sid TEXT NOT NULL,
|
||||||
|
conversationJid TEXT NOT NULL,
|
||||||
|
isFileUploadNotification INTEGER NOT NULL,
|
||||||
|
encrypted INTEGER NOT NULL,
|
||||||
|
errorType INTEGER,
|
||||||
|
warningType INTEGER,
|
||||||
|
received INTEGER,
|
||||||
|
displayed INTEGER,
|
||||||
|
acked INTEGER,
|
||||||
|
originId TEXT,
|
||||||
|
quote_id INTEGER,
|
||||||
|
file_metadata_id TEXT,
|
||||||
|
isDownloading INTEGER NOT NULL,
|
||||||
|
isUploading INTEGER NOT NULL,
|
||||||
|
isRetracted INTEGER,
|
||||||
|
isEdited INTEGER NOT NULL,
|
||||||
|
reactions TEXT NOT NULL,
|
||||||
|
containsNoStore INTEGER NOT NULL,
|
||||||
|
stickerPackId TEXT,
|
||||||
|
stickerHashKey TEXT,
|
||||||
|
pseudoMessageType INTEGER,
|
||||||
|
pseudoMessageData TEXT,
|
||||||
|
CONSTRAINT fk_quote FOREIGN KEY (quote_id) REFERENCES $messagesTable (id)
|
||||||
|
CONSTRAINT fk_file_metadata FOREIGN KEY (file_metadata_id) REFERENCES $fileMetadataTable (id)
|
||||||
|
)''',
|
||||||
|
);
|
||||||
|
|
||||||
|
await db.execute(
|
||||||
|
'INSERT INTO ${messagesTable}_new SELECT id, sender, body, timestamp, sid, conversationJid, isFileUploadNotification, encrypted, errorType, warningType, received, displayed, acked, originId, quote_id, file_metadata_id, isDownloading, isUploading, isRetracted, isEdited, reactions, containsNoStore, stickerPackId, stickerHashKey, pseudoMessageType, pseudoMessageData FROM $messagesTable',
|
||||||
|
);
|
||||||
|
await db.execute('DROP TABLE $messagesTable');
|
||||||
|
await db.execute(
|
||||||
|
'ALTER TABLE ${messagesTable}_new RENAME TO $messagesTable;',
|
||||||
|
);
|
||||||
|
}
|
||||||
24
lib/service/database/migrations/0002_indices.dart
Normal file
24
lib/service/database/migrations/0002_indices.dart
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import 'package:moxxyv2/service/database/constants.dart';
|
||||||
|
import 'package:sqflite_sqlcipher/sqflite.dart';
|
||||||
|
|
||||||
|
Future<void> upgradeFromV36ToV37(Database db) async {
|
||||||
|
// Queries against messages by id (and sid/originId happen regularly)
|
||||||
|
await db.execute(
|
||||||
|
'CREATE INDEX idx_messages_id ON $messagesTable (id, sid, originId)',
|
||||||
|
);
|
||||||
|
|
||||||
|
// Conversations are often queried by their jid
|
||||||
|
await db.execute(
|
||||||
|
'CREATE INDEX idx_conversation_id ON $conversationsTable (jid)',
|
||||||
|
);
|
||||||
|
|
||||||
|
// Reactions must be quickly queried
|
||||||
|
await db.execute(
|
||||||
|
'CREATE INDEX idx_reactions_message_id ON $reactionsTable (message_id, senderJid)',
|
||||||
|
);
|
||||||
|
|
||||||
|
// File metadata should also be quickly queriable by its id
|
||||||
|
await db.execute(
|
||||||
|
'CREATE INDEX idx_file_metadata_message_id ON $fileMetadataTable (id)',
|
||||||
|
);
|
||||||
|
}
|
||||||
60
lib/service/database/migrations/0002_reactions.dart
Normal file
60
lib/service/database/migrations/0002_reactions.dart
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
import 'package:moxxyv2/service/database/constants.dart';
|
||||||
|
import 'package:moxxyv2/service/database/helpers.dart';
|
||||||
|
import 'package:sqflite_sqlcipher/sqflite.dart';
|
||||||
|
|
||||||
|
Future<void> upgradeFromV34ToV35(Database db) async {
|
||||||
|
// Create the table
|
||||||
|
await db.execute('''
|
||||||
|
CREATE TABLE $reactionsTable (
|
||||||
|
senderJid TEXT NOT NULL,
|
||||||
|
emoji TEXT NOT NULL,
|
||||||
|
message_id INTEGER NOT NULL,
|
||||||
|
CONSTRAINT pk_sender PRIMARY KEY (senderJid, emoji, message_id),
|
||||||
|
CONSTRAINT fk_message FOREIGN KEY (message_id) REFERENCES $messagesTable (id)
|
||||||
|
ON DELETE CASCADE
|
||||||
|
)''');
|
||||||
|
|
||||||
|
// Figure out our JID
|
||||||
|
final rawJid = await db.query(
|
||||||
|
xmppStateTable,
|
||||||
|
where: "key = 'jid'",
|
||||||
|
limit: 1,
|
||||||
|
);
|
||||||
|
String? jid;
|
||||||
|
if (rawJid.isNotEmpty) {
|
||||||
|
jid = rawJid.first['value']! as String;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Migrate messages
|
||||||
|
final messages = await db.query(
|
||||||
|
messagesTable,
|
||||||
|
where: "reactions IS NOT '[]'",
|
||||||
|
);
|
||||||
|
for (final message in messages) {
|
||||||
|
final reactions =
|
||||||
|
(jsonDecode(message['reactions']! as String) as List<dynamic>)
|
||||||
|
.cast<Map<String, Object?>>();
|
||||||
|
|
||||||
|
for (final reaction in reactions) {
|
||||||
|
final senders = [
|
||||||
|
...reaction['senders']! as List<String>,
|
||||||
|
if (intToBool(reaction['reactedBySelf']! as int) && jid != null) jid,
|
||||||
|
];
|
||||||
|
|
||||||
|
for (final sender in senders) {
|
||||||
|
await db.insert(
|
||||||
|
reactionsTable,
|
||||||
|
{
|
||||||
|
'senderJid': sender,
|
||||||
|
'emoji': reaction['emoji']! as String,
|
||||||
|
'message_id': message['id']! as int,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove the column
|
||||||
|
await db.execute('ALTER TABLE $messagesTable DROP COLUMN reactions');
|
||||||
|
}
|
||||||
15
lib/service/database/migrations/0002_reactions_2.dart
Normal file
15
lib/service/database/migrations/0002_reactions_2.dart
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import 'package:moxxyv2/service/database/constants.dart';
|
||||||
|
import 'package:sqflite_sqlcipher/sqflite.dart';
|
||||||
|
|
||||||
|
Future<void> upgradeFromV35ToV36(Database db) async {
|
||||||
|
await db.execute('DROP TABLE $reactionsTable');
|
||||||
|
await db.execute('''
|
||||||
|
CREATE TABLE $reactionsTable (
|
||||||
|
senderJid TEXT NOT NULL,
|
||||||
|
emoji TEXT NOT NULL,
|
||||||
|
message_id INTEGER NOT NULL,
|
||||||
|
CONSTRAINT pk_sender PRIMARY KEY (senderJid, emoji, message_id),
|
||||||
|
CONSTRAINT fk_message FOREIGN KEY (message_id) REFERENCES $messagesTable (id)
|
||||||
|
ON DELETE CASCADE
|
||||||
|
)''');
|
||||||
|
}
|
||||||
14
lib/service/database/migrations/0002_shared_media.dart
Normal file
14
lib/service/database/migrations/0002_shared_media.dart
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import 'package:moxxyv2/service/database/constants.dart';
|
||||||
|
import 'package:sqflite_sqlcipher/sqflite.dart';
|
||||||
|
|
||||||
|
Future<void> upgradeFromV33ToV34(Database db) async {
|
||||||
|
// Remove the shared media counter...
|
||||||
|
await db.execute(
|
||||||
|
'ALTER TABLE $conversationsTable DROP COLUMN sharedMediaAmount',
|
||||||
|
);
|
||||||
|
|
||||||
|
// ... and the entire table.
|
||||||
|
await db.execute(
|
||||||
|
'DROP TABLE $mediaTable',
|
||||||
|
);
|
||||||
|
}
|
||||||
113
lib/service/database/migrations/0002_sticker_metadata.dart
Normal file
113
lib/service/database/migrations/0002_sticker_metadata.dart
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
import 'package:collection/collection.dart';
|
||||||
|
import 'package:moxxyv2/service/database/constants.dart';
|
||||||
|
import 'package:path/path.dart' as path;
|
||||||
|
import 'package:sqflite_sqlcipher/sqflite.dart';
|
||||||
|
|
||||||
|
Future<void> upgradeFromV32ToV33(Database db) async {
|
||||||
|
final stickers = await db.query(stickersTable);
|
||||||
|
|
||||||
|
await db.execute(
|
||||||
|
'''
|
||||||
|
CREATE TABLE ${stickersTable}_new (
|
||||||
|
id TEXT PRIMARY KEY,
|
||||||
|
desc TEXT NOT NULL,
|
||||||
|
suggests TEXT NOT NULL,
|
||||||
|
file_metadata_id TEXT NOT NULL,
|
||||||
|
stickerPackId TEXT NOT NULL,
|
||||||
|
CONSTRAINT fk_sticker_pack FOREIGN KEY (stickerPackId) REFERENCES $stickerPacksTable (id)
|
||||||
|
ON DELETE CASCADE,
|
||||||
|
CONSTRAINT fk_file_metadata FOREIGN KEY (file_metadata_id) REFERENCES $fileMetadataTable (id)
|
||||||
|
)''',
|
||||||
|
);
|
||||||
|
|
||||||
|
// Mapping stickerHashKey -> fileMetadataId
|
||||||
|
final stickerHashMap = <String, String>{};
|
||||||
|
for (final sticker in stickers) {
|
||||||
|
final hashes =
|
||||||
|
(jsonDecode(sticker['hashes']! as String) as Map<String, dynamic>)
|
||||||
|
.cast<String, String>();
|
||||||
|
|
||||||
|
final buffer = StringBuffer();
|
||||||
|
for (var i = 0; i < hashes.length; i++) {
|
||||||
|
buffer.write('(algorithm = ? AND value = ?) AND');
|
||||||
|
}
|
||||||
|
final query = buffer.toString();
|
||||||
|
|
||||||
|
final rawFm = await db.query(
|
||||||
|
fileMetadataHashesTable,
|
||||||
|
where: query.substring(0, query.length - 1 - 3),
|
||||||
|
whereArgs: hashes.entries
|
||||||
|
.map<List<String>>((entry) => [entry.key, entry.value])
|
||||||
|
.flattened
|
||||||
|
.toList(),
|
||||||
|
limit: 1,
|
||||||
|
);
|
||||||
|
|
||||||
|
String fileMetadataId;
|
||||||
|
if (rawFm.isEmpty) {
|
||||||
|
// Create the metadata
|
||||||
|
fileMetadataId = DateTime.now().toString();
|
||||||
|
await db.insert(
|
||||||
|
fileMetadataTable,
|
||||||
|
{
|
||||||
|
'id': fileMetadataId,
|
||||||
|
'path': sticker['path']! as String,
|
||||||
|
'size': sticker['size']! as int,
|
||||||
|
'width': sticker['width'] as int?,
|
||||||
|
'height': sticker['height'] as int?,
|
||||||
|
'plaintextHashes': sticker['hashes']! as String,
|
||||||
|
'mimeType': sticker['mediaType']! as String,
|
||||||
|
'sourceUrls': sticker['urlSources'],
|
||||||
|
'filename': path.basename(sticker['path']! as String),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// Create hash pointers
|
||||||
|
for (final hashEntry in hashes.entries) {
|
||||||
|
await db.insert(
|
||||||
|
fileMetadataHashesTable,
|
||||||
|
{
|
||||||
|
'algorithm': hashEntry.key,
|
||||||
|
'value': hashEntry.value,
|
||||||
|
'id': fileMetadataId,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
fileMetadataId = rawFm.first['id']! as String;
|
||||||
|
}
|
||||||
|
|
||||||
|
final hashKey = sticker['hashKey']! as String;
|
||||||
|
stickerHashMap[hashKey] = fileMetadataId;
|
||||||
|
await db.insert(
|
||||||
|
'${stickersTable}_new',
|
||||||
|
{
|
||||||
|
'id': hashKey,
|
||||||
|
'desc': sticker['desc']! as String,
|
||||||
|
'suggests': sticker['suggests']! as String,
|
||||||
|
'file_metadata_id': fileMetadataId,
|
||||||
|
'stickerPackId': sticker['stickerPackId']! as String,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rename the table
|
||||||
|
await db.execute('DROP TABLE $stickersTable');
|
||||||
|
await db.execute('ALTER TABLE ${stickersTable}_new RENAME TO $stickersTable');
|
||||||
|
|
||||||
|
// Migrate messages
|
||||||
|
for (final stickerEntry in stickerHashMap.entries) {
|
||||||
|
await db.update(
|
||||||
|
messagesTable,
|
||||||
|
{
|
||||||
|
'file_metadata_id': stickerEntry.value,
|
||||||
|
},
|
||||||
|
where: 'stickerHashKey = ?',
|
||||||
|
whereArgs: [stickerEntry.key],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove the hash key from messages
|
||||||
|
await db.execute('ALTER TABLE $messagesTable DROP COLUMN stickerHashKey');
|
||||||
|
}
|
||||||
13
lib/service/database/migrations/0003_avatar_hashes.dart
Normal file
13
lib/service/database/migrations/0003_avatar_hashes.dart
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import 'package:moxxyv2/service/database/constants.dart';
|
||||||
|
import 'package:sqflite_sqlcipher/sqflite.dart';
|
||||||
|
|
||||||
|
Future<void> upgradeFromV37ToV38(Database db) async {
|
||||||
|
await db
|
||||||
|
.execute('ALTER TABLE $conversationsTable ADD COLUMN avatarHash TEXT');
|
||||||
|
await db.execute(
|
||||||
|
'ALTER TABLE $conversationsTable RENAME COLUMN avatarUrl TO avatarPath',
|
||||||
|
);
|
||||||
|
await db.execute(
|
||||||
|
'ALTER TABLE $rosterTable RENAME COLUMN avatarUrl TO avatarPath',
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import 'package:moxxyv2/service/database/constants.dart';
|
||||||
|
import 'package:moxxyv2/shared/warning_types.dart';
|
||||||
|
import 'package:sqflite_sqlcipher/sqflite.dart';
|
||||||
|
|
||||||
|
Future<void> upgradeFromV44ToV45(Database db) async {
|
||||||
|
await db.update(
|
||||||
|
messagesTable,
|
||||||
|
{
|
||||||
|
'errorType': null,
|
||||||
|
'warningType': MessageWarningType.chatEncryptedButFilePlaintext.value,
|
||||||
|
},
|
||||||
|
where: 'errorType = ?',
|
||||||
|
// NOTE: 10 is the old id of this error
|
||||||
|
whereArgs: [10],
|
||||||
|
);
|
||||||
|
}
|
||||||
12
lib/service/database/migrations/0003_groupchat_table.dart
Normal file
12
lib/service/database/migrations/0003_groupchat_table.dart
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import 'package:moxxyv2/service/database/constants.dart';
|
||||||
|
import 'package:sqflite_sqlcipher/sqflite.dart';
|
||||||
|
|
||||||
|
Future<void> upgradeFromV42ToV43(Database db) async {
|
||||||
|
await db.execute(
|
||||||
|
'''
|
||||||
|
CREATE TABLE $groupchatTable (
|
||||||
|
jid TEXT PRIMARY KEY,
|
||||||
|
nick TEXT NOT NULL
|
||||||
|
)''',
|
||||||
|
);
|
||||||
|
}
|
||||||
428
lib/service/database/migrations/0003_jid_attribute.dart
Normal file
428
lib/service/database/migrations/0003_jid_attribute.dart
Normal file
@@ -0,0 +1,428 @@
|
|||||||
|
import 'package:get_it/get_it.dart';
|
||||||
|
import 'package:moxxyv2/service/database/constants.dart';
|
||||||
|
import 'package:moxxyv2/service/xmpp_state.dart';
|
||||||
|
import 'package:sqflite_sqlcipher/sqflite.dart';
|
||||||
|
import 'package:uuid/uuid.dart';
|
||||||
|
|
||||||
|
extension MaybeGet<K, V> on Map<K, V> {
|
||||||
|
V? maybeGet(K? key) {
|
||||||
|
if (key == null) return null;
|
||||||
|
|
||||||
|
return this[key];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> upgradeFromV45ToV46(Database db) async {
|
||||||
|
// Migrate everything to the tuple of (account JID, <old pk>)
|
||||||
|
// Things we do not migrate to this scheme:
|
||||||
|
// - Stickers: Technically, makes no sense
|
||||||
|
// - File metadata: We want to aggresively cache, so we keep it
|
||||||
|
|
||||||
|
// Get the account JID
|
||||||
|
final rawJid = await db.query(
|
||||||
|
xmppStateTable,
|
||||||
|
where: 'key = ?',
|
||||||
|
whereArgs: ['jid'],
|
||||||
|
limit: 1,
|
||||||
|
);
|
||||||
|
|
||||||
|
// [migrateRows] indicates whether we can move the data to the new JID-annotated format.
|
||||||
|
// It's false if we don't have a "logged in" JID. If we have one, it's true and we can
|
||||||
|
// move data.
|
||||||
|
final migrateRows = rawJid.isNotEmpty;
|
||||||
|
final accountJid = migrateRows ? rawJid.first['value']! as String : null;
|
||||||
|
|
||||||
|
// Store the account JID in the secure storage.
|
||||||
|
if (migrateRows) {
|
||||||
|
await GetIt.I.get<XmppStateService>().setAccountJid(accountJid!);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Migrate the XMPP state
|
||||||
|
await db.execute(
|
||||||
|
'''
|
||||||
|
CREATE TABLE ${xmppStateTable}_new (
|
||||||
|
key TEXT NOT NULL,
|
||||||
|
accountJid TEXT NOT NULL,
|
||||||
|
value TEXT,
|
||||||
|
PRIMARY KEY (key, accountJid)
|
||||||
|
)''',
|
||||||
|
);
|
||||||
|
if (migrateRows) {
|
||||||
|
for (final statePair in await db.query(xmppStateTable)) {
|
||||||
|
await db.insert(
|
||||||
|
'${xmppStateTable}_new',
|
||||||
|
{
|
||||||
|
...statePair,
|
||||||
|
'accountJid': accountJid,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await db.execute('DROP TABLE $xmppStateTable');
|
||||||
|
await db
|
||||||
|
.execute('ALTER TABLE ${xmppStateTable}_new RENAME TO $xmppStateTable');
|
||||||
|
|
||||||
|
// Migrate messages
|
||||||
|
await db.execute(
|
||||||
|
'''
|
||||||
|
CREATE TABLE ${messagesTable}_new (
|
||||||
|
id TEXT NOT NULL PRIMARY KEY,
|
||||||
|
accountJid TEXT NOT NULL,
|
||||||
|
sender TEXT NOT NULL,
|
||||||
|
body TEXT,
|
||||||
|
timestamp INTEGER NOT NULL,
|
||||||
|
sid TEXT NOT NULL,
|
||||||
|
conversationJid TEXT NOT NULL,
|
||||||
|
isFileUploadNotification INTEGER NOT NULL,
|
||||||
|
encrypted INTEGER NOT NULL,
|
||||||
|
errorType INTEGER,
|
||||||
|
warningType INTEGER,
|
||||||
|
received INTEGER,
|
||||||
|
displayed INTEGER,
|
||||||
|
acked INTEGER,
|
||||||
|
originId TEXT,
|
||||||
|
quote_id TEXT,
|
||||||
|
file_metadata_id TEXT,
|
||||||
|
isDownloading INTEGER NOT NULL,
|
||||||
|
isUploading INTEGER NOT NULL,
|
||||||
|
isRetracted INTEGER,
|
||||||
|
isEdited INTEGER NOT NULL,
|
||||||
|
containsNoStore INTEGER NOT NULL,
|
||||||
|
stickerPackId TEXT,
|
||||||
|
pseudoMessageType INTEGER,
|
||||||
|
pseudoMessageData TEXT,
|
||||||
|
CONSTRAINT fk_quote
|
||||||
|
FOREIGN KEY (quote_id)
|
||||||
|
REFERENCES $messagesTable (id)
|
||||||
|
CONSTRAINT fk_file_metadata
|
||||||
|
FOREIGN KEY (file_metadata_id)
|
||||||
|
REFERENCES $fileMetadataTable (id)
|
||||||
|
)''',
|
||||||
|
);
|
||||||
|
// Build up the message map
|
||||||
|
/// Message's old id attribute -> Message's new UUID attribute.
|
||||||
|
const uuid = Uuid();
|
||||||
|
final messageMap = <int, String>{};
|
||||||
|
|
||||||
|
if (migrateRows) {
|
||||||
|
final messages = await db.query(messagesTable);
|
||||||
|
for (final message in messages) {
|
||||||
|
messageMap[message['id']! as int] = uuid.v4();
|
||||||
|
}
|
||||||
|
// Then migrate messages
|
||||||
|
for (final message in messages) {
|
||||||
|
await db.insert('${messagesTable}_new', {
|
||||||
|
...Map.from(message)
|
||||||
|
..remove('id')
|
||||||
|
..remove('quote_id'),
|
||||||
|
'accountJid': accountJid,
|
||||||
|
'quote_id': messageMap.maybeGet(message['quote_id'] as int?),
|
||||||
|
'id': messageMap[message['id']! as int],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await db.execute('DROP TABLE $messagesTable');
|
||||||
|
await db.execute('ALTER TABLE ${messagesTable}_new RENAME TO $messagesTable');
|
||||||
|
await db.execute(
|
||||||
|
'CREATE INDEX idx_messages_sid ON $messagesTable (accountJid, sid)',
|
||||||
|
);
|
||||||
|
await db.execute(
|
||||||
|
'CREATE INDEX idx_messages_origin_sid ON $messagesTable (accountJid, originId, sid)',
|
||||||
|
);
|
||||||
|
|
||||||
|
// Migrate conversations
|
||||||
|
await db.execute(
|
||||||
|
'''
|
||||||
|
CREATE TABLE ${conversationsTable}_new (
|
||||||
|
jid TEXT NOT NULL,
|
||||||
|
accountJid TEXT NOT NULL,
|
||||||
|
title TEXT NOT NULL,
|
||||||
|
avatarPath TEXT NOT NULL,
|
||||||
|
avatarHash TEXT,
|
||||||
|
type TEXT NOT NULL,
|
||||||
|
lastChangeTimestamp INTEGER NOT NULL,
|
||||||
|
unreadCounter INTEGER NOT NULL,
|
||||||
|
open INTEGER NOT NULL,
|
||||||
|
muted INTEGER NOT NULL,
|
||||||
|
encrypted INTEGER NOT NULL,
|
||||||
|
lastMessageId TEXT,
|
||||||
|
contactId TEXT,
|
||||||
|
contactAvatarPath TEXT,
|
||||||
|
contactDisplayName TEXT,
|
||||||
|
PRIMARY KEY (jid, accountJid),
|
||||||
|
CONSTRAINT fk_last_message
|
||||||
|
FOREIGN KEY (lastMessageId)
|
||||||
|
REFERENCES $messagesTable (id),
|
||||||
|
CONSTRAINT fk_contact_id
|
||||||
|
FOREIGN KEY (contactId)
|
||||||
|
REFERENCES $contactsTable (id)
|
||||||
|
ON DELETE SET NULL
|
||||||
|
)''',
|
||||||
|
);
|
||||||
|
if (migrateRows) {
|
||||||
|
for (final conversation in await db.query(conversationsTable)) {
|
||||||
|
await db.insert(
|
||||||
|
'${conversationsTable}_new',
|
||||||
|
{
|
||||||
|
...Map.from(conversation)..remove('lastMessageId'),
|
||||||
|
'lastMessageId':
|
||||||
|
messageMap.maybeGet(conversation['lastMessageId'] as int?),
|
||||||
|
'accountJid': accountJid,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await db.execute('DROP TABLE $conversationsTable');
|
||||||
|
await db.execute(
|
||||||
|
'ALTER TABLE ${conversationsTable}_new RENAME TO $conversationsTable',
|
||||||
|
);
|
||||||
|
await db.execute(
|
||||||
|
'CREATE INDEX idx_conversation_id ON $conversationsTable (accountJid, jid)',
|
||||||
|
);
|
||||||
|
|
||||||
|
// Migrate groupchat details
|
||||||
|
await db.execute(
|
||||||
|
'''
|
||||||
|
CREATE TABLE ${groupchatTable}_new (
|
||||||
|
jid TEXT NOT NULL,
|
||||||
|
accountJid TEXT NOT NULL,
|
||||||
|
nick TEXT NOT NULL,
|
||||||
|
PRIMARY KEY (jid, accountJid),
|
||||||
|
CONSTRAINT fk_groupchat
|
||||||
|
FOREIGN KEY (jid, accountJid)
|
||||||
|
REFERENCES $conversationsTable (jid, accountJid)
|
||||||
|
ON DELETE CASCADE
|
||||||
|
)''',
|
||||||
|
);
|
||||||
|
if (migrateRows) {
|
||||||
|
for (final g in await db.query(groupchatTable)) {
|
||||||
|
await db.insert(
|
||||||
|
'${groupchatTable}_new',
|
||||||
|
{
|
||||||
|
...g,
|
||||||
|
'accountJid': accountJid,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await db.execute('DROP TABLE $groupchatTable');
|
||||||
|
await db
|
||||||
|
.execute('ALTER TABLE ${groupchatTable}_new RENAME TO $groupchatTable');
|
||||||
|
|
||||||
|
// Migrate reactions
|
||||||
|
await db.execute(
|
||||||
|
'''
|
||||||
|
CREATE TABLE ${reactionsTable}_new (
|
||||||
|
accountJid TEXT NOT NULL,
|
||||||
|
message_id TEXT NOT NULL,
|
||||||
|
senderJid TEXT NOT NULL,
|
||||||
|
emoji TEXT NOT NULL,
|
||||||
|
PRIMARY KEY (accountJid, senderJid, emoji, message_id),
|
||||||
|
CONSTRAINT fk_message
|
||||||
|
FOREIGN KEY (message_id)
|
||||||
|
REFERENCES $messagesTable (id)
|
||||||
|
ON DELETE CASCADE
|
||||||
|
)''',
|
||||||
|
);
|
||||||
|
if (migrateRows) {
|
||||||
|
for (final reaction in await db.query(reactionsTable)) {
|
||||||
|
await db.insert(
|
||||||
|
'${reactionsTable}_new',
|
||||||
|
{
|
||||||
|
...Map.from(reaction)..remove('message_id'),
|
||||||
|
'message_id': messageMap.maybeGet(reaction['message_id']! as int),
|
||||||
|
'accountJid': accountJid,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await db.execute('DROP TABLE $reactionsTable');
|
||||||
|
await db
|
||||||
|
.execute('ALTER TABLE ${reactionsTable}_new RENAME TO $reactionsTable');
|
||||||
|
|
||||||
|
// Migrate the roster
|
||||||
|
await db.execute(
|
||||||
|
'''
|
||||||
|
CREATE TABLE ${rosterTable}_new (
|
||||||
|
jid TEXT NOT NULL,
|
||||||
|
accountJid TEXT NOT NULL,
|
||||||
|
title TEXT NOT NULL,
|
||||||
|
avatarPath TEXT NOT NULL,
|
||||||
|
avatarHash TEXT NOT NULL,
|
||||||
|
subscription TEXT NOT NULL,
|
||||||
|
ask TEXT NOT NULL,
|
||||||
|
contactId TEXT,
|
||||||
|
contactAvatarPath TEXT,
|
||||||
|
contactDisplayName TEXT,
|
||||||
|
pseudoRosterItem INTEGER NOT NULL,
|
||||||
|
CONSTRAINT fk_contact_id
|
||||||
|
FOREIGN KEY (contactId)
|
||||||
|
REFERENCES $contactsTable (id)
|
||||||
|
ON DELETE SET NULL
|
||||||
|
)''',
|
||||||
|
);
|
||||||
|
if (migrateRows) {
|
||||||
|
for (final rosterItem in await db.query(rosterTable)) {
|
||||||
|
await db.insert(
|
||||||
|
'${rosterTable}_new',
|
||||||
|
{
|
||||||
|
...Map.from(rosterItem)..remove('id'),
|
||||||
|
'accountJid': accountJid,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await db.execute('DROP TABLE $rosterTable');
|
||||||
|
await db.execute('ALTER TABLE ${rosterTable}_new RENAME TO $rosterTable');
|
||||||
|
|
||||||
|
// Migrate the blocklist
|
||||||
|
await db.execute(
|
||||||
|
'''
|
||||||
|
CREATE TABLE ${blocklistTable}_new (
|
||||||
|
jid TEXT NOT NULL,
|
||||||
|
accountJid TEXT NOT NULL,
|
||||||
|
PRIMARY KEY (accountJid, jid)
|
||||||
|
);
|
||||||
|
''',
|
||||||
|
);
|
||||||
|
if (migrateRows) {
|
||||||
|
for (final blocklistItem in await db.query(blocklistTable)) {
|
||||||
|
await db.insert(
|
||||||
|
'${blocklistTable}_new',
|
||||||
|
{
|
||||||
|
...blocklistItem,
|
||||||
|
'accountJid': accountJid,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await db.execute('DROP TABLE $blocklistTable');
|
||||||
|
await db
|
||||||
|
.execute('ALTER TABLE ${blocklistTable}_new RENAME TO $blocklistTable');
|
||||||
|
|
||||||
|
// Migrate the notifications list
|
||||||
|
await db.execute(
|
||||||
|
'''
|
||||||
|
CREATE TABLE ${notificationsTable}_new (
|
||||||
|
id INTEGER NOT NULL,
|
||||||
|
conversationJid TEXT NOT NULL,
|
||||||
|
accountJid TEXT NOT NULL,
|
||||||
|
sender TEXT,
|
||||||
|
senderJid TEXT,
|
||||||
|
avatarPath TEXT,
|
||||||
|
body TEXT NOT NULL,
|
||||||
|
mime TEXT,
|
||||||
|
path TEXT,
|
||||||
|
timestamp INTEGER NOT NULL,
|
||||||
|
PRIMARY KEY (id, conversationJid, senderJid, timestamp, accountJid)
|
||||||
|
)''',
|
||||||
|
);
|
||||||
|
if (migrateRows) {
|
||||||
|
for (final notification in await db.query(notificationsTable)) {
|
||||||
|
await db.insert(
|
||||||
|
'${notificationsTable}_new',
|
||||||
|
{
|
||||||
|
...notification,
|
||||||
|
'accountJid': accountJid,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await db.execute('DROP TABLE $notificationsTable');
|
||||||
|
await db.execute(
|
||||||
|
'ALTER TABLE ${notificationsTable}_new RENAME TO $notificationsTable',
|
||||||
|
);
|
||||||
|
|
||||||
|
// Migrate OMEMO device list
|
||||||
|
await db.execute(
|
||||||
|
'''
|
||||||
|
CREATE TABLE ${omemoDeviceListTable}_new (
|
||||||
|
jid TEXT NOT NULL,
|
||||||
|
accountJid TEXT NOT NULL,
|
||||||
|
devices TEXT NOT NULL,
|
||||||
|
PRIMARY KEY (accountJid, jid)
|
||||||
|
)''',
|
||||||
|
);
|
||||||
|
{
|
||||||
|
for (final deviceListEntry in await db.query(omemoDeviceListTable)) {
|
||||||
|
await db.insert(
|
||||||
|
'${omemoDeviceListTable}_new',
|
||||||
|
{
|
||||||
|
...deviceListEntry,
|
||||||
|
'accountJid': accountJid,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await db.execute('DROP TABLE $omemoDeviceListTable');
|
||||||
|
await db.execute(
|
||||||
|
'ALTER TABLE ${omemoDeviceListTable}_new RENAME TO $omemoDeviceListTable',
|
||||||
|
);
|
||||||
|
|
||||||
|
// Migrate OMEMO trust
|
||||||
|
await db.execute(
|
||||||
|
'''
|
||||||
|
CREATE TABLE ${omemoTrustTable}_new (
|
||||||
|
jid TEXT NOT NULL,
|
||||||
|
accountJid TEXT NOT NULL,
|
||||||
|
device INTEGER NOT NULL,
|
||||||
|
trust INTEGER NOT NULL,
|
||||||
|
enabled INTEGER NOT NULL,
|
||||||
|
PRIMARY KEY (accountJid, jid, device)
|
||||||
|
)''',
|
||||||
|
);
|
||||||
|
if (migrateRows) {
|
||||||
|
for (final trustItem in await db.query(omemoTrustTable)) {
|
||||||
|
await db.insert(
|
||||||
|
'${omemoTrustTable}_new',
|
||||||
|
{
|
||||||
|
...trustItem,
|
||||||
|
'accountJid': accountJid,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await db.execute('DROP TABLE $omemoTrustTable');
|
||||||
|
await db
|
||||||
|
.execute('ALTER TABLE ${omemoTrustTable}_new RENAME TO $omemoTrustTable');
|
||||||
|
|
||||||
|
// Migrate OMEMO ratchets
|
||||||
|
await db.execute(
|
||||||
|
'''
|
||||||
|
CREATE TABLE ${omemoRatchetsTable}_new (
|
||||||
|
jid TEXT NOT NULL,
|
||||||
|
accountJid TEXT NOT NULL,
|
||||||
|
device INTEGER NOT NULL,
|
||||||
|
dhsPub TEXT NOT NULL,
|
||||||
|
dhs TEXT NOT NULL,
|
||||||
|
dhrPub TEXT,
|
||||||
|
rk TEXT NOT NULL,
|
||||||
|
cks TEXT,
|
||||||
|
ckr TEXT,
|
||||||
|
ns INTEGER NOT NULL,
|
||||||
|
nr INTEGER NOT NULL,
|
||||||
|
pn INTEGER NOT NULL,
|
||||||
|
ik TEXT NOT NULL,
|
||||||
|
ad TEXT NOT NULL,
|
||||||
|
skipped TEXT NOT NULL,
|
||||||
|
kex TEXT NOT NULL,
|
||||||
|
acked INTEGER NOT NULL,
|
||||||
|
PRIMARY KEY (accountJid, jid, device)
|
||||||
|
)''',
|
||||||
|
);
|
||||||
|
if (migrateRows) {
|
||||||
|
for (final ratchet in await db.query(omemoRatchetsTable)) {
|
||||||
|
await db.insert(
|
||||||
|
'${omemoRatchetsTable}_new',
|
||||||
|
{
|
||||||
|
...ratchet,
|
||||||
|
'accountJid': accountJid,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await db.execute('DROP TABLE $omemoRatchetsTable');
|
||||||
|
await db.execute(
|
||||||
|
'ALTER TABLE ${omemoRatchetsTable}_new RENAME TO $omemoRatchetsTable',
|
||||||
|
);
|
||||||
|
}
|
||||||
72
lib/service/database/migrations/0003_new_omemo.dart
Normal file
72
lib/service/database/migrations/0003_new_omemo.dart
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
import 'package:moxxyv2/service/database/constants.dart';
|
||||||
|
import 'package:sqflite_sqlcipher/sqflite.dart';
|
||||||
|
|
||||||
|
Future<void> upgradeFromV39ToV40(Database db) async {
|
||||||
|
// Remove the old tables
|
||||||
|
await db.execute('DROP TABLE OmemoDevices');
|
||||||
|
await db.execute('DROP TABLE OmemoDeviceList');
|
||||||
|
await db.execute('DROP TABLE OmemoTrustCacheList');
|
||||||
|
await db.execute('DROP TABLE OmemoTrustDeviceList');
|
||||||
|
await db.execute('DROP TABLE OmemoTrustEnableList');
|
||||||
|
await db.execute('DROP TABLE OmemoFingerprintCache');
|
||||||
|
|
||||||
|
// Create the new tables
|
||||||
|
await db.execute(
|
||||||
|
'''
|
||||||
|
CREATE TABLE $omemoDevicesTable (
|
||||||
|
jid TEXT NOT NULL PRIMARY KEY,
|
||||||
|
id INTEGER NOT NULL,
|
||||||
|
ikPub TEXT NOT NULL,
|
||||||
|
ik TEXT NOT NULL,
|
||||||
|
spkPub TEXT NOT NULL,
|
||||||
|
spk TEXT NOT NULL,
|
||||||
|
spkId INTEGER NOT NULL,
|
||||||
|
spkSig TEXT NOT NULL,
|
||||||
|
oldSpkPub TEXT,
|
||||||
|
oldSpk TEXT,
|
||||||
|
oldSpkId INTEGER,
|
||||||
|
opks TEXT NOT NULL
|
||||||
|
)''',
|
||||||
|
);
|
||||||
|
|
||||||
|
await db.execute(
|
||||||
|
'''
|
||||||
|
CREATE TABLE $omemoDeviceListTable (
|
||||||
|
jid TEXT NOT NULL PRIMARY KEY,
|
||||||
|
devices TEXT NOT NULL
|
||||||
|
)''',
|
||||||
|
);
|
||||||
|
|
||||||
|
await db.execute(
|
||||||
|
'''
|
||||||
|
CREATE TABLE $omemoRatchetsTable (
|
||||||
|
jid TEXT NOT NULL,
|
||||||
|
device INTEGER NOT NULL,
|
||||||
|
dhsPub TEXT NOT NULL,
|
||||||
|
dhs TEXT NOT NULL,
|
||||||
|
dhrPub TEXT,
|
||||||
|
rk TEXT NOT NULL,
|
||||||
|
cks TEXT,
|
||||||
|
ckr TEXT,
|
||||||
|
ns INTEGER NOT NULL,
|
||||||
|
nr INTEGER NOT NULL,
|
||||||
|
pn INTEGER NOT NULL,
|
||||||
|
ik TEXT NOT NULL,
|
||||||
|
ad TEXT NOT NULL,
|
||||||
|
skipped TEXT NOT NULL,
|
||||||
|
kex TEXT NOT NULL,
|
||||||
|
acked INTEGER NOT NULL,
|
||||||
|
PRIMARY KEY (jid, device)
|
||||||
|
)''',
|
||||||
|
);
|
||||||
|
await db.execute(
|
||||||
|
'''
|
||||||
|
CREATE TABLE $omemoTrustTable (
|
||||||
|
jid TEXT NOT NULL,
|
||||||
|
device INTEGER NOT NULL,
|
||||||
|
trust INTEGER NOT NULL,
|
||||||
|
enabled INTEGER NOT NULL,
|
||||||
|
PRIMARY KEY (jid, device)
|
||||||
|
)''',
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
|
import 'package:moxxyv2/service/database/constants.dart';
|
||||||
|
import 'package:sqflite_sqlcipher/sqflite.dart';
|
||||||
|
|
||||||
|
Future<void> upgradeFromV40ToV41(Database db) async {
|
||||||
|
final messages = await db.query(
|
||||||
|
messagesTable,
|
||||||
|
where: 'pseudoMessageType IS NOT NULL',
|
||||||
|
);
|
||||||
|
|
||||||
|
for (final message in messages) {
|
||||||
|
await db.insert(
|
||||||
|
messagesTable,
|
||||||
|
{
|
||||||
|
...message,
|
||||||
|
'pseudoMessageData': jsonEncode({
|
||||||
|
'ratchetsAdded': 1,
|
||||||
|
'ratchetsReplaced': 0,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
conflictAlgorithm: ConflictAlgorithm.replace,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user