Compare commits
2051 Commits
80fd8863c9
..
lotus
| Author | SHA1 | Date | |
|---|---|---|---|
| 24662fa994 | |||
| 230ef8ed7c | |||
| 160c09e525 | |||
| 589d45e0a0 | |||
| acd355bb5a | |||
| 6e59395fb8 | |||
| 9f4516c6a8 | |||
| 0bd2273bee | |||
| d37fa1584c | |||
| e17cb09269 | |||
| 4d55e45962 | |||
| e3532064b5 | |||
| 1e37b20c6a | |||
| 4f03775e04 | |||
| 9678b02aba | |||
| a926487f5e | |||
| ae1d30bc5a | |||
| a7d145aa70 | |||
| 472d4ba008 | |||
| 2a0478cad8 | |||
| cee0c591e2 | |||
| 68b6ffffd7 | |||
| 9bc8c4b47f | |||
| e80ebd35cb | |||
| 36343baecc | |||
| 89cf171efc | |||
| 149ec8e4e4 | |||
| d1cd963e4b | |||
| 5ef0a1fd3e | |||
| 6ace96f2cf | |||
| 2d71f2ce30 | |||
| 2c3dba55e6 | |||
| c7a04dcc70 | |||
| 4b14c15518 | |||
| c68ef346bf | |||
| c5d7fcc303 | |||
| 9bf56d5748 | |||
| d5ce56930b | |||
| 349194e7e5 | |||
| 24d6460e4c | |||
| 127e783f66 | |||
| 198fd12bb2 | |||
| 34d5209165 | |||
| 9684ab75bb | |||
| 0a6b035a67 | |||
| cbfd3e5632 | |||
| 3faf0866a0 | |||
| bab3a160c2 | |||
| 1778cd0009 | |||
| 5204766276 | |||
| 6218012d3f | |||
| ccb0c1d18e | |||
| 65e24bd446 | |||
| de6cecaffc | |||
| da545ba9b9 | |||
| 3c4842df1e | |||
| 1ee0f0b57a | |||
| 4fbbd9680b | |||
| 259a5a2b3e | |||
| 8d62be9eff | |||
| 63139350e4 | |||
| 33b33e685a | |||
| a8038bb534 | |||
| 4d0e34c4cf | |||
| 70ffd252bd | |||
| 51d468fbcc | |||
| 1c84556600 | |||
| 34997bcbd1 | |||
| 78cb2acd6c | |||
| ce8a03ab16 | |||
| 19feca4964 | |||
| adbda094e7 | |||
| 7013da70bc | |||
| 49d9410e3a | |||
| 84a2e7a93e | |||
| 950b8a8128 | |||
| af58f7a32c | |||
| 91c6f2f091 | |||
| 31cf353463 | |||
| 8912423aeb | |||
| bc85cd4984 | |||
| fc8eb70617 | |||
| 1a5896ef84 | |||
| 7b94eeaa60 | |||
| 50076962f6 | |||
| d39aef0aac | |||
| 9f533b1077 | |||
| fdaba40ba9 | |||
| caf6318a5d | |||
| 23649d85b0 | |||
| c67aed01dc | |||
| 66cc51d6d0 | |||
| 4a87588435 | |||
| c0fd372529 | |||
| 203568c967 | |||
| 0394fce929 | |||
| d2946c00ce | |||
| b7e1f89c1d | |||
| c0f9867218 | |||
| 69515e8e81 | |||
| 70b8d03c02 | |||
| f8baf3f258 | |||
| b76f1a6009 | |||
| 7841d1dc87 | |||
| ee421a71e2 | |||
| e37971456a | |||
| 79f8fabb1b | |||
| dfd2c9c49e | |||
| 5470e25bb0 | |||
| 374d6dc396 | |||
| d0715774a8 | |||
| 6f544e2b1f | |||
| e713d47319 | |||
| b361d43088 | |||
| a33d28a7ae | |||
| 4a4dede105 | |||
| b818d3fc5a | |||
| cf839e7345 | |||
| c54cb126ff | |||
| 8dc4c4d072 | |||
| 9742eaea28 | |||
| fb66c0ed90 | |||
| 9deeef6e8d | |||
| e2b957b6bd | |||
| abf15391f6 | |||
| 44e36f7dd2 | |||
| a77c4b6db5 | |||
| cb3d2c40e5 | |||
| f50e14d7a5 | |||
| 0ead519a80 | |||
| 7d98b49a30 | |||
| f054abfbd2 | |||
| 2b5c6fd606 | |||
| ffa490e767 | |||
| 8ac42cdbad | |||
| 1b4c6cab6d | |||
| 176d5d0bb7 | |||
| 3df95adc52 | |||
| a6bf4eb7e7 | |||
| baa12823f7 | |||
| 8c711f5f4a | |||
| c4f00ed483 | |||
| f5c301d5c6 | |||
| c395f7d16e | |||
| 26f900870b | |||
| bb99ad5611 | |||
| 6c58e25211 | |||
| b24ab838f8 | |||
| cf7c66b99a | |||
| 04b56ffacd | |||
| abb7f743b8 | |||
| 14cfa021c5 | |||
| 86272b6b08 | |||
| 89a2321dd4 | |||
| 6634b2b8a2 | |||
| b65e82a475 | |||
| b006f9804a | |||
| 5b27587f17 | |||
| 5d5f5f4516 | |||
| 938ead79f7 | |||
| 4a401cf816 | |||
| 5deed79b42 | |||
| 762e99a907 | |||
| f9edd2023d | |||
| 30101c83e8 | |||
| 10f6544e2e | |||
| 9c690fbdfb | |||
| 6f9bdc4d50 | |||
| 7f329e3b31 | |||
| 97d808585a | |||
| 4bb7c1ffb5 | |||
| 388a934665 | |||
| 99e6a456a7 | |||
| a5fe358313 | |||
| d7d7b59866 | |||
| 362ccff85d | |||
| 6ec0ab78d9 | |||
| e9a970a75b | |||
| 2a545b8b3e | |||
| bf1308dd55 | |||
| ca09e8e6ca | |||
| 6db07f1371 | |||
| 107921e0d0 | |||
| 053b364a44 | |||
| 3282832a4a | |||
| 00524bebe0 | |||
| 9df4d2d7ee | |||
| 2c5f0b8b28 | |||
| 702e2e00eb | |||
| 2b1c3256b6 | |||
| 6a57c13c56 | |||
| 362f4943d4 | |||
| f15c4caf97 | |||
| aa48c9ef8a | |||
| 3df9c4d9e6 | |||
| 2178295eaa | |||
| 055dcec65b | |||
| 9a24feb914 | |||
| 46567555e1 | |||
| b41bfd35c0 | |||
| 6a83e67f95 | |||
| 469b9aa9c6 | |||
| 77a29ed3c6 | |||
| a30a3d3a47 | |||
| a9787ef041 | |||
| fcf16fd654 | |||
| 0a14ec63de | |||
| 5469740f4c | |||
| 891f2daf99 | |||
| b7daabe2e0 | |||
| d78f81c3a7 | |||
| 170d22eebb | |||
| c8ff7b0718 | |||
| bafd9cbe75 | |||
| f11b308f91 | |||
| 81e1a25de6 | |||
| 8ff2f33d3a | |||
| ea15db430c | |||
| 7e4178f7e2 | |||
| 71386f4ef2 | |||
| ec391100bd | |||
| 67fb0a5120 | |||
| 6ec908407b | |||
| 58b19995b8 | |||
| a78cb46bfe | |||
| 78090d1c2d | |||
| 08937c6278 | |||
| dc01480175 | |||
| 9447e64e5e | |||
| 8ac63e3771 | |||
| 5d525d4246 | |||
| b5d831cc12 | |||
| 8866da0a82 | |||
| d1de438f67 | |||
| c6932b45fb | |||
| 517e992dec | |||
| c5fbc20394 | |||
| 4876c2e4ca | |||
| 24e6882e72 | |||
| 6a91904469 | |||
| 08e7f33cba | |||
| 657ca3a5ca | |||
| fbdd0e7083 | |||
| c80f8c6427 | |||
| 696e958a00 | |||
| d3dcf93f1a | |||
| c6760b0ba4 | |||
| 8f5afcda08 | |||
| 9273eb5f2e | |||
| ad508ac61e | |||
| 73921cb2a1 | |||
| 01ba24df12 | |||
| 51a355fe77 | |||
| 160db1eaef | |||
| ee717e8361 | |||
| 6d0b778755 | |||
| 5ae84cbeaf | |||
| 6d2bf9a582 | |||
| c798625a79 | |||
| 194d52a808 | |||
| 3eabc8f4dd | |||
| ba659bc157 | |||
| face24f2f4 | |||
| 0f4f33119b | |||
| 10cda6b632 | |||
| 0370ed525a | |||
| a3f776134f | |||
| 9232e1ec8e | |||
| 906884e434 | |||
| da35278b30 | |||
| e18e089043 | |||
| 977fa8aa1b | |||
| 986e4bb93a | |||
| 9d4679d260 | |||
| 3cbc5112a7 | |||
| ed67641dd5 | |||
| 7cf751a3a5 | |||
| 8c2f0a7bee | |||
| d43044ccbf | |||
| afe957015b | |||
| f07ff63ac1 | |||
| a552e49ece | |||
| dedbd54199 | |||
| 26f1e234a2 | |||
| 742132661f | |||
| 8c4a863c84 | |||
| 2b115390f2 | |||
| 7b01cfebf5 | |||
| 7bafefa5e7 | |||
| 0bbfe17559 | |||
| 16a15efe9b | |||
| fc9ba03943 | |||
| 16dddcb9f0 | |||
| 3db87db03f | |||
| 3a72b7c1c5 | |||
| 134ebb231d | |||
| 572bc6a4c0 | |||
| 4223ac85d3 | |||
| 3a58d4f94d | |||
| c1f7e4c21d | |||
| 74cde50df7 | |||
| faddde65fa | |||
| 79c3b664ba | |||
| 5435741d42 | |||
| 8e3acf8d00 | |||
| bdac3b2171 | |||
| 51d45088c3 | |||
| b243a18e01 | |||
| 41ff5276ca | |||
| bd8e116cf3 | |||
| 582839fddb | |||
| 10f0093f8e | |||
| 48f9221f1c | |||
| bc63714a07 | |||
| 2255795cae | |||
| 64029d9e8e | |||
| 6056aa9632 | |||
| 8c0383ab7f | |||
| af3155e169 | |||
| e5ae77a99d | |||
| b13297ce3b | |||
| 5957b1cf55 | |||
| 98cbb32b86 | |||
| db4a39ff56 | |||
| 08b1a5c2a3 | |||
| eb283c29ff | |||
| f8ab136492 | |||
| 221bd825b1 | |||
| 60cbfec951 | |||
| 90cb70c128 | |||
| 01a61e3ec2 | |||
| e280f0e312 | |||
| 46da4458ff | |||
| bb2e25de2d | |||
| 8214387479 | |||
| 0ebe24be20 | |||
| 0d303169f2 | |||
| 54a912be1c | |||
| f3022f8be9 | |||
| 58cd88d815 | |||
| 5d10afb7a6 | |||
| d58c445d74 | |||
| 97b335773b | |||
| bf8fa85055 | |||
| f8466f2f02 | |||
| 2146dac833 | |||
| fc5f808cf9 | |||
| db0b083a3e | |||
| 73420242d0 | |||
| 8dec8169dc | |||
| 6c915d03c3 | |||
| 83095cc66f | |||
| 3f4c90f56a | |||
| 6bbed6e2e8 | |||
| 9b18804927 | |||
| 6c9fd17f27 | |||
| 083985fe94 | |||
| 161b9bb75e | |||
| 4c5cff13ad | |||
| e17bb6a534 | |||
| 46b6fecdde | |||
| 8740752aec | |||
| 67e3ab1499 | |||
| bcf63490c9 | |||
| 6af3a7ebbe | |||
| 37e68e906b | |||
| 33d5549ede | |||
| 93a78184d4 | |||
| e5110a13f8 | |||
| 14336ef2a3 | |||
| f4ec09e168 | |||
| d05868179d | |||
| 5db4db1d95 | |||
| 3927f01089 | |||
| 8b84780d11 | |||
| e76f1f0ccb | |||
| 574f71d082 | |||
| d43da7554e | |||
| f24dff99ee | |||
| 710a03ccca | |||
| 9a833a42f9 | |||
| b088cb17b8 | |||
| 2e160ebacd | |||
| 5cb642b509 | |||
| 7079462503 | |||
| d6ae29f8f3 | |||
| b3e7901008 | |||
| 11346b1dc6 | |||
| ac5cd1195d | |||
| 26456eda36 | |||
| d3fba1d685 | |||
| 266e47f240 | |||
| e547248681 | |||
| ea2155c635 | |||
| b6e1a0eea9 | |||
| a5e5b3c413 | |||
| 7168b11323 | |||
| e74426cc86 | |||
| 7284132432 | |||
| 337d5e2b78 | |||
| 005f081f27 | |||
| 4e39ea79ff | |||
| 61a1f008d0 | |||
| b3666fa876 | |||
| d32055ee3d | |||
| 922f03a4f1 | |||
| 3a02484378 | |||
| 3d1590fa7b | |||
| 720d18296f | |||
| 30c6d4736f | |||
| cbbfa7e8d4 | |||
| 00be1d9c9e | |||
| 42b9cc2b64 | |||
| 408fc1b846 | |||
| c63b8519ed | |||
| d121a22c15 | |||
| 1eec57cdc9 | |||
| 5e53d68234 | |||
| b15a149d14 | |||
| 6037e10a23 | |||
| 41899adafa | |||
| c1249f3322 | |||
| 0ef8dc9baf | |||
| ce2e0ef203 | |||
| 689805ca6e | |||
| 09905828ef | |||
| a67f581c52 | |||
| 8fa079c289 | |||
| a90169abb9 | |||
| 8e62dec419 | |||
| d1c49a1738 | |||
| 343bb3a496 | |||
| c05b233b3f | |||
| 9843e8bdf1 | |||
| 2909aebb1f | |||
| abd90ab980 | |||
| 5e740496c9 | |||
| d707c4441c | |||
| df30476b53 | |||
| 18b12cfca1 | |||
| 4e80c0a0f5 | |||
| d4705f9235 | |||
| 4490993fe7 | |||
| ee7eabd2c4 | |||
| 71791e46f6 | |||
| ff7c2ed941 | |||
| 443d8407fc | |||
| 8666daaf9d | |||
| 3196d6ac3e | |||
| daca1e0cca | |||
| e836634cff | |||
| 0897e2ce4c | |||
| 4c4d61600d | |||
| 1e5d5f3fe4 | |||
| e9c7b9d600 | |||
| 7d57396de8 | |||
| cb2d730cca | |||
| ab9b02a243 | |||
| 5418b9e188 | |||
| 51e85d885c | |||
| 83cbb09bae | |||
| c0c0b54280 | |||
| d81c3c8721 | |||
| 303f6fbd45 | |||
| d792b3002c | |||
| 7a4347231c | |||
| c720cc8b64 | |||
| 7aee1f07f1 | |||
| 243a1c78c1 | |||
| 8ebb1a8d8c | |||
| 86464f4981 | |||
| 91d52e44bc | |||
| a393fab2d6 | |||
| ec8a9be44c | |||
| a1e4539852 | |||
| ccb11880d9 | |||
| ded2e7be80 | |||
| 5af2c7cc30 | |||
| ecdc7e9e8a | |||
| 9a041fab42 | |||
| 0a13f3cb68 | |||
| c6fedb7997 | |||
| 4acb7c7c20 | |||
| a96edc116f | |||
| 5cab74be39 | |||
| 9721b5f7c1 | |||
| 50ab6d3dd5 | |||
| f6cca1673a | |||
| 41ae0f4fa0 | |||
| d76cd0f7ee | |||
| 3513608435 | |||
| 92d6469530 | |||
| 487e884e51 | |||
| 9580f2a744 | |||
| 2384672a53 | |||
| de06612a17 | |||
| 038bde8b62 | |||
| ad82843ee5 | |||
| ee8bcadd93 | |||
| 3c10fcddc8 | |||
| 389d121c5d | |||
| 7d8c2c5937 | |||
| dc26e15df1 | |||
| a42845fcc6 | |||
| e8415e60b0 | |||
| 893aa92b38 | |||
| db57cc202e | |||
| f3ec49fe88 | |||
| 5f0aa5c887 | |||
| d67fe80906 | |||
| 0df997eecf | |||
| dcb3ff3129 | |||
| 0824504e5a | |||
| ccc919d2c8 | |||
| 5d984a4af4 | |||
| 083f0e7adc | |||
| 2121745537 | |||
| 337ec82211 | |||
| b9c0f01e25 | |||
| 3e429c3500 | |||
| aecc460866 | |||
| 9849de5f3c | |||
| 9a48fdeeb0 | |||
| 299794f8ce | |||
| abb6dca7bd | |||
| 3ede102a2c | |||
| d5f123a6e7 | |||
| 575511ec89 | |||
| be7dae790b | |||
| 955f1f89a1 | |||
| 856b7b4f05 | |||
| abcfa4541d | |||
| 3fbbcf0bc8 | |||
| 2d7bd3ed63 | |||
| 38a15e1ad0 | |||
| 0f91bc77f3 | |||
| e059f129f4 | |||
| 3795aaf504 | |||
| ddf0ed7493 | |||
| aa2e6744d7 | |||
| e65a898074 | |||
| 7549ce1bec | |||
| 4e0fd65dec | |||
| 846ddb7c68 | |||
| d6cb51387f | |||
| a1053ce8f1 | |||
| 3d7a449d57 | |||
| d24ad8374b | |||
| 971c6ef870 | |||
| 40432768e8 | |||
| 6acc2e9b3f | |||
| 149d53b9c6 | |||
| 44f01c0fed | |||
| 5a3f7fffad | |||
| 4e4170793e | |||
| 503f3c401c | |||
| b2ea3b41e0 | |||
| a6f5c3e842 | |||
| fad999c580 | |||
| 435b610cc7 | |||
| a0e9ba0f55 | |||
| 2b94b5bb47 | |||
| ee787b3b3f | |||
| 1204859a0b | |||
| de3d6457a0 | |||
| 97cf03189e | |||
| 0f435acb4e | |||
| 9fecf64915 | |||
| 13b4aa5f40 | |||
| 4bbc94cd7e | |||
| 6cd104eee0 | |||
| 449b2c5a5e | |||
| 9403ee2ed5 | |||
| a7eec6ee7b | |||
| 4a05d406dd | |||
| bc80365ba0 | |||
| 977c1f0f74 | |||
| df8a2f9d6d | |||
| acfd3fffaf | |||
| 8cf8e140d4 | |||
| 3aa4e4aaf5 | |||
| bd524861f4 | |||
| 07e352830d | |||
| bd8935b023 | |||
| 8237da6041 | |||
| 6e06a8536a | |||
| c2ae7826e5 | |||
| 93ed896124 | |||
| 8658e456b3 | |||
| f070b4792a | |||
| 36c7d0112b | |||
| 88c8b0eea8 | |||
| bab95cdd52 | |||
| 17a1c04ced | |||
| 9d28411db3 | |||
| 5bfe61a85e | |||
| 53a0a88e58 | |||
| d56ab14d29 | |||
| 4d2da0c030 | |||
| 69b95a8947 | |||
| 3d85501bec | |||
| d19804b5eb | |||
| 22f898d4ae | |||
| 9190a93cb0 | |||
| b388167861 | |||
| d1a3d378dc | |||
| 5a48671b19 | |||
| c285e5f892 | |||
| c122ac07de | |||
| 84d699ebd4 | |||
| 6afd19b60d | |||
| 3370ba41c2 | |||
| 62a28e3289 | |||
| 4919f58bce | |||
| 2c19f9f42f | |||
| 5458a754db | |||
| 3bf374a057 | |||
| 892a11238c | |||
| e92295f930 | |||
| 171bf5431a | |||
| 74ca459cae | |||
| d90ce5b679 | |||
| 99568dde71 | |||
| af63b528dc | |||
| 695e8fe933 | |||
| 1b89475fa7 | |||
| 61967e6b2c | |||
| c43d518995 | |||
| 5efdc41757 | |||
| f84e756864 | |||
| 73bb365ddb | |||
| 066212f176 | |||
| 194299c510 | |||
| 4d5e98c2d1 | |||
| 4a75adb7ee | |||
| f917239286 | |||
| 3db9d6decd | |||
| d8467225fb | |||
| 996aa1fc1d | |||
| 2e43ba8add | |||
| 6062ae2132 | |||
| 4c5ad69ca0 | |||
| edb1664596 | |||
| 5121f810e6 | |||
| ab6c3e3e20 | |||
| 941aba4f83 | |||
| 24aaba551e | |||
| ba08d0ad86 | |||
| 63e66990f3 | |||
| 356cc0ff37 | |||
| 4605946695 | |||
| 634ac6718c | |||
| 05c3d7c332 | |||
| a8099c672d | |||
| 842a2467e4 | |||
| 13c9b3eaa3 | |||
| 0e6b549ec9 | |||
| 1b384aa329 | |||
| 1118e91b20 | |||
| b7cc9d88cb | |||
| 563b8f5089 | |||
| 29364b2e92 | |||
| ac0345d07f | |||
| 748952a2a4 | |||
| 5114333067 | |||
| 7558f6d6fa | |||
| bb6fc297af | |||
| 0da301ef12 | |||
| f674cc7505 | |||
| 589370f1bd | |||
| e6f6e2117c | |||
| 6bec8aa2bb | |||
| 348240fbc3 | |||
| 428324d2ee | |||
| 1aca413f40 | |||
| 3189c40c2e | |||
| aeae37e838 | |||
| 4a230cbfc1 | |||
| a21af8943d | |||
| 2592bb6f8a | |||
| ff3329aa0c | |||
| bcfa47fc2d | |||
| a64c34d7fe | |||
| 5d9e4b0d65 | |||
| e6a86f4c4f | |||
| 8a072b1290 | |||
| b84927e49b | |||
| fef143dced | |||
| 066ec99699 | |||
| 7462b39f7c | |||
| 744d1efe6f | |||
| 8d0c569207 | |||
| 4e64d821f2 | |||
| 6e2a5c786a | |||
| e1f10d7c98 | |||
| 49f0ab46fa | |||
| 4ffc591983 | |||
| d8fa649745 | |||
| 3e31bfa11b | |||
| 770da845ce | |||
| 003348c54e | |||
| f775b3151a | |||
| 30a2157094 | |||
| 3c73ffa025 | |||
| 8d6e9d629b | |||
| 2f77807ebd | |||
| 55ea5a4080 | |||
| bc3f2ac438 | |||
| 3d945ec74a | |||
| 56a0ce78ad | |||
| 4e8f81fa80 | |||
| 4c9641c278 | |||
| 5fb90a6516 | |||
| 7527fc22e0 | |||
| 86fbce0a7c | |||
| d9fee274c2 | |||
| f63a5c7e35 | |||
| b82533a11f | |||
| 9890fed1aa | |||
| 0ac8179ed2 | |||
| 4e1c4f6872 | |||
| c4af5f2909 | |||
| ef925f6e7f | |||
| 411e4f52c6 | |||
| 3d9b266c71 | |||
| 4a03b33442 | |||
| 317a7ca861 | |||
| b291f13ebd | |||
| 4a88b37343 | |||
| 564dd38ffc | |||
| ec253b4d71 | |||
| 7410e00475 | |||
| 1abda8056b | |||
| 0346ccad4f | |||
| 697182519e | |||
| 459773e532 | |||
| d3a8f09766 | |||
| aa175e0b6f | |||
| be6d0f61e1 | |||
| a2e651c869 | |||
| 9bacdd1018 | |||
| 96074475fb | |||
| cb44815ca2 | |||
| f0bf08a7dc | |||
| 3531ce994e | |||
| 6195f5eaab | |||
| d4737964bc | |||
| 1771d5de88 | |||
| f41fc064e8 | |||
| b8b58bcf5d | |||
| 3e2eca7ea1 | |||
| 97cd12cdfe | |||
| 3c0d633f87 | |||
| 5242912910 | |||
| 040168e4df | |||
| 7fe2b6eac5 | |||
| 96c329a482 | |||
| e3708b0dd6 | |||
| 4773c07692 | |||
| ea4cb1fed4 | |||
| 9f46f33237 | |||
| 2d5d896418 | |||
| 7bad15f12b | |||
| 210c7108a0 | |||
| 2d307a79e7 | |||
| 7cf53b1c02 | |||
| 45c4f6295b | |||
| 1e3892641a | |||
| 6c5498bf40 | |||
| 1720413eae | |||
| ac8df3c2df | |||
| a94ba68cf9 | |||
| f364fd4408 | |||
| dfe108eb96 | |||
| a2ca3195ea | |||
| 87cc753366 | |||
| 85c9febe91 | |||
| b8b4483cc0 | |||
| 60faedd466 | |||
| b822870006 | |||
| e68549f907 | |||
| abae424458 | |||
| 6eda805a3a | |||
| b95b49ba5c | |||
| 8e03e3667c | |||
| fa42b502b4 | |||
| bae7b4ef17 | |||
| d0a798383a | |||
| 2e50a87f8c | |||
| 4cd207bf1f | |||
| 22526710a0 | |||
| 4f3a27bede | |||
| f69a197ac9 | |||
| 677c9ddd4a | |||
| f9de1e276b | |||
| 3a16fc59f4 | |||
| 225df0786e | |||
| 257438378f | |||
| 80dd4e0aac | |||
| 1c8412bd5c | |||
| fb5fe67ded | |||
| 2fd3bcff0a | |||
| 4969e7fb35 | |||
| 3ef12e1d24 | |||
| 100ba037ef | |||
| aa94195184 | |||
| 16f2a06186 | |||
| 4f2a018ca9 | |||
| 96380d0662 | |||
| b2008340dd | |||
| 125b815ac7 | |||
| 62d6b8e582 | |||
| b8d80da7e0 | |||
| e2f1089766 | |||
| e1c2430db9 | |||
| 49e038e23b | |||
| c60ea57f3b | |||
| cc9a7b1620 | |||
| 8799ae5246 | |||
| df5cca4e9a | |||
| 75c58f8d1a | |||
| 585d9111c9 | |||
| dd581a20cb | |||
| 1ffe97ff0f | |||
| 4300896c72 | |||
| 9e80f94fab | |||
| 4555538d7c | |||
| 1e806c7589 | |||
| 1191ca6a1b | |||
| 71bde10887 | |||
| 1583d892ac | |||
| 4ad15af141 | |||
| e7d7160f6f | |||
| 72dadb0a07 | |||
| a0ea330c07 | |||
| fee8ad1c09 | |||
| 37998dd853 | |||
| 5df8e4cc8d | |||
| 4d742d11bc | |||
| 99c8030322 | |||
| b19a630e90 | |||
| f7da99ea1c | |||
| e0e43625fa | |||
| 6ce5874c18 | |||
| 34a307adfd | |||
| cb011cecd2 | |||
| 9eeba3573f | |||
| f442b7e3a8 | |||
| fcba6b11ed | |||
| ff8a0b24be | |||
| e012a9741f | |||
| 2de4f88c96 | |||
| 4539c1e6e2 | |||
| ee0cba97ad | |||
| 5f9196e459 | |||
| ed937a67dc | |||
| 5b23b32537 | |||
| 1843001172 | |||
| d9747596b9 | |||
| 853e64cbe5 | |||
| fbc87f7f15 | |||
| c24f184cac | |||
| ec9069d3a2 | |||
| f10df7d8da | |||
| b88ead029f | |||
| be776312fe | |||
| a5ca64e8f3 | |||
| 2faa04f441 | |||
| 9364882c1b | |||
| 9c4f71274c | |||
| d0d1b625aa | |||
| 56bd5060d9 | |||
| c35a6cef38 | |||
| bed36a3f1a | |||
| f9c525dd44 | |||
| 5f089131ba | |||
| 7a57e0322f | |||
| 8a34c5cfa2 | |||
| de5e7726f7 | |||
| 9b7f16ab10 | |||
| b0b646cb9e | |||
| d5cecf0b33 | |||
| c5aa16c685 | |||
| 0ce0685056 | |||
| 46a45d4fc9 | |||
| 1d7f529808 | |||
| 539373aa64 | |||
| 872c847a3e | |||
| fe425f7187 | |||
| 5ff92c109b | |||
| ca53a16a92 | |||
| b1262e0555 | |||
| ab9d567bd6 | |||
| 30efeabdd1 | |||
| d807dd0b1d | |||
| be5df48757 | |||
| 4e0e4cc606 | |||
| ff6a505c16 | |||
| 274c2defbf | |||
| a1e6319cfc | |||
| 4f454d1e12 | |||
| 248e3cddef | |||
| 02a0c644b9 | |||
| 1b281964bf | |||
| e9d7c8dd74 | |||
| 4ec6c34e12 | |||
| 8f94469c5a | |||
| 3e1a3aac4d | |||
| 973a20f9ca | |||
| 679963fa77 | |||
| 579b8ccc5f | |||
| b3988f6e7c | |||
| 4007418192 | |||
| d834b1a221 | |||
| a45010c7db | |||
| d4a4526457 | |||
| a68ed7d3e9 | |||
| 5a8b82a12e | |||
| 6c38dbd21b | |||
| fa683dafd9 | |||
| 9163896172 | |||
| 1a0d6b10ec | |||
| 2a78b3cf8f | |||
| 6eb3d70596 | |||
| 69081298b4 | |||
| a03441c9af | |||
| 8f19374e52 | |||
| 190a66b8d6 | |||
| 886c798983 | |||
| 91cd926f79 | |||
| b4e6a5bdd2 | |||
| f1f0a126bb | |||
| 72cdd578ee | |||
| 02adc1c2c2 | |||
| 6aca6b2e7c | |||
| 6ed6107381 | |||
| b68205fb9d | |||
| 82c79ad1ba | |||
| 5c261275d0 | |||
| fb8b706c80 | |||
| 0f84928e7c | |||
| 6b85a85170 | |||
| a7b9475660 | |||
| 25d924aa81 | |||
| 50dbd81d2d | |||
| a1a019b6db | |||
| 009d1893b1 | |||
| 64fe4c99d0 | |||
| 5d83be2a9e | |||
| a9be0a6628 | |||
| f5079300b8 | |||
| cb7fd4ddd4 | |||
| 192c21c108 | |||
| 498263025e | |||
| 59eb93cfac | |||
| dbc63c51d1 | |||
| a0585d040a | |||
| cd4709899a | |||
| 46e3cb901a | |||
| b1a89f92bc | |||
| bedac23b47 | |||
| ec5d676911 | |||
| 20e11f665a | |||
| e3882f39d7 | |||
| eba69b3f0a | |||
| b35bb8d830 | |||
| d0aeb24bad | |||
| 3805283e88 | |||
| f832f3fbf2 | |||
| eb5c50c61c | |||
| 9109be896d | |||
| fe2e58c744 | |||
| e36e45fcfa | |||
| a62cafd972 | |||
| fb4be9da74 | |||
| 9dd4ad4ae4 | |||
| 152061a518 | |||
| 29a9161680 | |||
| 3cd03b8bc6 | |||
| b37328d382 | |||
| 10cc53cfa4 | |||
| 7c76553ef6 | |||
| 67ac5f4d69 | |||
| 24acd61a93 | |||
| 539fcd5bad | |||
| 360fdc291d | |||
| c180d9a17d | |||
| 8ee95ab25b | |||
| 9cbeec16d0 | |||
| 2f1d4eca5e | |||
| 3d9e9606c8 | |||
| e14a52620f | |||
| cfd2cb3b9c | |||
| bd5475685b | |||
| a387124a62 | |||
| ac9142b797 | |||
| 2a91679091 | |||
| 5da2006a6e | |||
| 28d87dbe53 | |||
| 922688a754 | |||
| 001dc502e0 | |||
| 93d18162bf | |||
| 49aa615341 | |||
| f8bc7abfbc | |||
| 4d58479ed4 | |||
| 64f2f25702 | |||
| c15321e78d | |||
| d2b3c766a0 | |||
| 6b94aff5d4 | |||
| 181b69468f | |||
| 1defce2d24 | |||
| 0c703fd2b0 | |||
| 43ab0b75e8 | |||
| 562ae67479 | |||
| d02a3276bf | |||
| 6b41a68775 | |||
| af60faab99 | |||
| 173a8b38e9 | |||
| 9cfb34c8d5 | |||
| 8ba8d1b77c | |||
| 8949c16678 | |||
| c9f95ca753 | |||
| c48f7c24e8 | |||
| 40088fdf8a | |||
| c69f790bd3 | |||
| 87c54476a6 | |||
| 3e81ca305f | |||
| 3f25e25c89 | |||
| 28708a6d34 | |||
| 02c54e2abf | |||
| a52402fe8e | |||
| 3e89b7190a | |||
| 422b56e13e | |||
| 405086550a | |||
| c0095fba0c | |||
| 5a1f7c1731 | |||
| ba4cf13146 | |||
| 3d4f4fccab | |||
| ffc69a8ad0 | |||
| 0aea10ed0b | |||
| 0b30967891 | |||
| 9c67910e93 | |||
| e43ecf74d7 | |||
| 006b08ae6e | |||
| b7a82901e2 | |||
| ab2525a433 | |||
| c7beb56966 | |||
| 59c042d313 | |||
| d414aa88ac | |||
| 5c91f8b6fa | |||
| c558c80199 | |||
| e6dedeb095 | |||
| 1eb2ae1b85 | |||
| 7e0c85a6fd | |||
| d24e6738a4 | |||
| a54640c305 | |||
| 12afb8753e | |||
| 47256fc450 | |||
| 5e88e387df | |||
| 22fe4373b6 | |||
| b571badd80 | |||
| 6bb728ac09 | |||
| 90db3e09a0 | |||
| e26fb29965 | |||
| d2a5a2e346 | |||
| e9fa181300 | |||
| b58cd897b1 | |||
| c10069fc89 | |||
| c82b958b49 | |||
| 94789817e2 | |||
| 41d3192bb0 | |||
| 9df4fe72fe | |||
| c9beeb0e8f | |||
| 50ee16413e | |||
| 714e115429 | |||
| f4b0dfa3fa | |||
| 2eafb7e82d | |||
| b93c9200ba | |||
| 0aa18693d0 | |||
| 6638f27c6c | |||
| aa8ba771c1 | |||
| 820d33a667 | |||
| e3a78b4628 | |||
| f5a75c399e | |||
| 470cb4693a | |||
| 5bf3b7cb4f | |||
| 7fc0d29762 | |||
| cc65173321 | |||
| 27bfbb4ff0 | |||
| 583c425100 | |||
| 7209bd752e | |||
| f99219bb87 | |||
| 71bfae7018 | |||
| 99fa4cfdd7 | |||
| 39cdcb8c6b | |||
| 2fb026ed10 | |||
| 2e55c7e47b | |||
| a91ac41317 | |||
| 0f78f9cf32 | |||
| cf6df01cd1 | |||
| b2fe5f50c6 | |||
| e79024f1fd | |||
| b6f3c87742 | |||
| fc4f04d695 | |||
| 93eafd5710 | |||
| faa3799e6a | |||
| 023a98a7b1 | |||
| 44b42c005b | |||
| 973dee2857 | |||
| a69727ac2c | |||
| ac315cf565 | |||
| 81ee1ae436 | |||
| 92cdb6337b | |||
| 2170f55d3e | |||
| 583ee52b6d | |||
| db14d2359c | |||
| 65745ee6a7 | |||
| 86d45b0edd | |||
| 2ada86adfc | |||
| 6f882a403d | |||
| 6b97487d47 | |||
| 24c50870ec | |||
| 7434cd1798 | |||
| cdf6f73f37 | |||
| 5447e15533 | |||
| 10f390c185 | |||
| 8a5e9069d5 | |||
| 021a650ba3 | |||
| cc58daadf9 | |||
| 8c5b918b0e | |||
| d7520a37b3 | |||
| e7aea8dd10 | |||
| f9bfb66e46 | |||
| dfd30c8f92 | |||
| 5df3dc4677 | |||
| d2d9147a6f | |||
| d69530ae03 | |||
| 271de29aa5 | |||
| 7df38a2cc8 | |||
| 0e0a700f63 | |||
| 31739ca28d | |||
| f2e9cb18c3 | |||
| b1b698db05 | |||
| b2930e73ba | |||
| b7971e881c | |||
| 28b99c58f2 | |||
| a25e277a61 | |||
| 73a7132f2e | |||
| e7325018c3 | |||
| 0941951c92 | |||
| 17abb5404f | |||
| d54a48b45a | |||
| aeba091d7c | |||
| b119a18602 | |||
| 9995c911cc | |||
| e85a70822d | |||
| feabcc0480 | |||
| db0423e265 | |||
| 2db1c3271a | |||
| 2f2a549bf4 | |||
| 67a4d77f13 | |||
| 6ba74769bf | |||
| cf52eadf40 | |||
| bfd0dc5d3a | |||
| 5ede48f6fb | |||
| e5804a65c3 | |||
| edb390ae48 | |||
| 13fac8a7e8 | |||
| 5efe224c7a | |||
| 6026cf1f4a | |||
| 77bce30981 | |||
| 6709e70574 | |||
| 9bc861ecc5 | |||
| 5012d7b5a8 | |||
| 244fa23b95 | |||
| 824c0c76e4 | |||
| fa7fc324d7 | |||
| 5ba94af61d | |||
| c2a5004770 | |||
| a524435fae | |||
| 1e08541498 | |||
| 39e45baafe | |||
| dc0c30625d | |||
| 569611a1fa | |||
| 167dc15037 | |||
| 38f923fc24 | |||
| 538df1f253 | |||
| 4d08b1d5f2 | |||
| 8688d6a34c | |||
| 62498c513e | |||
| 8d861eaf42 | |||
| add75871be | |||
| c405bbd099 | |||
| e18984869f | |||
| c4c0edf563 | |||
| ab369c5766 | |||
| e85d85122f | |||
| ac6631e4b3 | |||
| 33ff24ebaf | |||
| 55755fe056 | |||
| ffc2742e49 | |||
| e15d333c8b | |||
| 36fef8a2cf | |||
| eb65bdcfb9 | |||
| 9cf2f20abc | |||
| 612464f99a | |||
| 79ce261817 | |||
| 4ff66fd231 | |||
| b2e133cd5c | |||
| d9aa4a9e49 | |||
| f2833ea929 | |||
| 803c927c10 | |||
| 92de05f1d7 | |||
| c1b5c3a61f | |||
| f021f9c03d | |||
| 4dacc0b2b6 | |||
| b9f2597ea6 | |||
| 8f244da2be | |||
| cb842dc8df | |||
| dc2e6fc353 | |||
| 3ad93663d7 | |||
| 1148364cd9 | |||
| f46f8f40f2 | |||
| 73fac10d14 | |||
| 1a70babad0 | |||
| 12e3a3625c | |||
| bf6fa84aa9 | |||
| 22123c3cc4 | |||
| 3d40e05948 | |||
| 386d2cf7f5 | |||
| f89bc00072 | |||
| 24e05bd77a | |||
| fc678f7089 | |||
| a90ae25a55 | |||
| 98b932c1ec | |||
| a36ba6a91e | |||
| 67e32493a9 | |||
| 171c1deb3c | |||
| 34907b2f38 | |||
| 5ba89dcb4c | |||
| 8387a332cd | |||
| 711c485eb0 | |||
| d67f144e90 | |||
| 347da6cdca | |||
| 05ed101578 | |||
| 30b87842d7 | |||
| d8f4b1fc7f | |||
| 6e678b5cf4 | |||
| 4797083562 | |||
| 08fc8b000f | |||
| 95d1edf7a5 | |||
| 6a54842bb2 | |||
| 643143f253 | |||
| 65ef66f1d9 | |||
| 6b67a3f02b | |||
| 1d2f8ca45f | |||
| c7b5867908 | |||
| 0157b08634 | |||
| 5fc823995b | |||
| a56e0ce84a | |||
| 9f3a4cf2ca | |||
| af40265f09 | |||
| 7b4e2d36f7 | |||
| fdd0423b1b | |||
| a81881c7dd | |||
| 81331da876 | |||
| 1fc0aa0d41 | |||
| be147c0ad9 | |||
| 0c2b174d21 | |||
| 0e40de0445 | |||
| baafeb6d94 | |||
| a1113f7cd6 | |||
| 338954732d | |||
| 75c90927c6 | |||
| 4d048d6116 | |||
| f85b45151c | |||
| 33cc2fbefa | |||
| 93cb9760e9 | |||
| 978351b216 | |||
| e25ffceae7 | |||
| 5c31283d29 | |||
| ba4c7b7393 | |||
| 10eea62481 | |||
| f07ca8c529 | |||
| 4e511b6e74 | |||
| fa24cc723e | |||
| c7d2d7d424 | |||
| e8beff1688 | |||
| 2ce73a4ae3 | |||
| 190d59f6a3 | |||
| 86525cdf34 | |||
| 4fd2809357 | |||
| cac04a61c0 | |||
| d545e04249 | |||
| 9ba57af91e | |||
| b0f69eca25 | |||
| 51f1949ef0 | |||
| 5f8815e52e | |||
| 73b6f3d63b | |||
| 1c67bb7402 | |||
| dad7206a92 | |||
| f8d1cd799b | |||
| 8ae9744b7d | |||
| f0440d916b | |||
| 0f21878819 | |||
| 9960b5e3e4 | |||
| 3c5c2ffaf4 | |||
| b95dbd9a4b | |||
| f196a94cf5 | |||
| e6b6ffb5d8 | |||
| ff84f9fcbc | |||
| cb64ff27a3 | |||
| 3c425a14c8 | |||
| ab308c4a1b | |||
| abc4f6f21b | |||
| 18e6af11c8 | |||
| 59999bf1b4 | |||
| 8708cf1d49 | |||
| d3b334e4fa | |||
| 0cfe2df7d6 | |||
| 00bc2c29a9 | |||
| 99d9e85b33 | |||
| 14f1a42267 | |||
| c62f997070 | |||
| 0ff7d1e2e0 | |||
| 680c331af9 | |||
| 14ba3448f7 | |||
| ff7e954f89 | |||
| df79956001 | |||
| 578527cfac | |||
| 71f25908f8 | |||
| eaa0717e1b | |||
| b6a7107c80 | |||
| f275e1a5b4 | |||
| 4b4b022591 | |||
| fc11d478b7 | |||
| cba0eacf0a | |||
| 19aec41ef5 | |||
| 27efb449d8 | |||
| b565c6913f | |||
| a4ed3743ed | |||
| 8a8a19f070 | |||
| 0cf96617c8 | |||
| 6bbb83b072 | |||
| cb1a912403 | |||
| 8a6dbab00f | |||
| b67cba8ecd | |||
| 26040d89f8 | |||
| 93209ab549 | |||
| 7a1af5abf1 | |||
| 14c3c6d20d | |||
| 6cc22876fd | |||
| b1888f2595 | |||
| 1e4539e017 | |||
| 17e8e6354a | |||
| 4ae601e27b | |||
| 78b9da6467 | |||
| 4d16cbd04c | |||
| c117a41400 | |||
| f1ff627d37 | |||
| d00aeff6dc | |||
| e0ef934d9c | |||
| 94313ba6e6 | |||
| ed68c504fd | |||
| 6d965fdc6d | |||
| 048f405e50 | |||
| 75fca57424 | |||
| df4cad035e | |||
| 815421d9bf | |||
| 99d98ab1cb | |||
| b2ea4f30b9 | |||
| 919c7cdc4e | |||
| 859f5d512c | |||
| 3929b44b2a | |||
| 1b4d70bb50 | |||
| cf8aaaf777 | |||
| 452376ebd0 | |||
| 34fa13a4c8 | |||
| e23a920e07 | |||
| 3a5368f9cc | |||
| 73aebbbb95 | |||
| a932fcfdbe | |||
| 3e121dc5a6 | |||
| a26e8c080a | |||
| 9adb564811 | |||
| 3fafb8230c | |||
| 304ccd65ee | |||
| b36331583c | |||
| 67853a05c0 | |||
| 06013332a4 | |||
| d04db4965e | |||
| f1dfcf2c3a | |||
| d1fde7c5a9 | |||
| 76beaf21cc | |||
| a785013308 | |||
| 50ade8717c | |||
| 2a4673842c | |||
| ca2852f73a | |||
| 42c1000e00 | |||
| 0e0c561b15 | |||
| 9df98975a9 | |||
| fe4a49eb3b | |||
| aa36af8368 | |||
| 87865b92c0 | |||
| 62f3b7dd26 | |||
| f529d20c02 | |||
| 2b3f066fc7 | |||
| 50de9e2617 | |||
| 17c00c87dc | |||
| 6a697e2162 | |||
| 7a69505f70 | |||
| 4d34dba5c0 | |||
| 58ba65d780 | |||
| 35a1589fdd | |||
| b2b4e20edd | |||
| 832b5034de | |||
| 7ea0435572 | |||
| d61eef7747 | |||
| fe12d6298d | |||
| f2cf13d3ee | |||
| f646d59b15 | |||
| fa46a59949 | |||
| b108ca59d0 | |||
| 31316798aa | |||
| 37966c785a | |||
| 622fee1aec | |||
| 1ba616078d | |||
| be17905b00 | |||
| 7e62c72dce | |||
| b2a8cf1cfe | |||
| ae9db88424 | |||
| 4b453ff527 | |||
| 93311d51e7 | |||
| 4f9d723b92 | |||
| ee50782759 | |||
| 99b7f3b5fe | |||
| a2f0c1af93 | |||
| 053b0f09c3 | |||
| 24879d0761 | |||
| 230a7395dc | |||
| bdd179f3ef | |||
| c3fa89f37e | |||
| 074a1c9d8f | |||
| 8a25130af6 | |||
| 206757e162 | |||
| c1e8f058e1 | |||
| 9090705e22 | |||
| 0d593c51cf | |||
| d8209d4fe5 | |||
| 14568d4d3a | |||
| 016002023c | |||
| 2b7cdfe2e2 | |||
| 6b736a07eb | |||
| 7390dd9492 | |||
| 180f5ba77a | |||
| 8c76008e3f | |||
| b51c7529c6 | |||
| ac265acf1e | |||
| c78cea857c | |||
| c89ae1c721 | |||
| f6baadc94e | |||
| 9a6029ae72 | |||
| 56043df54e | |||
| 19e7ff1977 | |||
| a143c421b9 | |||
| f3082691b5 | |||
| e863f95e25 | |||
| bc9bf1486e | |||
| d2e1cdabd0 | |||
| da0c635538 | |||
| da921a56fa | |||
| 7d9db1b5e6 | |||
| 043051f346 | |||
| dcd8c2f0ce | |||
| e0751942bf | |||
| 72b22dca5e | |||
| 5c78ae4e14 | |||
| c9fd8f6efb | |||
| e7da928650 | |||
| 54cc5900e4 | |||
| e8d7549fa1 | |||
| c2d894a04d | |||
| 3a97a07973 | |||
| 75f9c7d230 | |||
| 5cc1d4cae7 | |||
| 1664913265 | |||
| 33fafac8ae | |||
| 0bc119a5ab | |||
| d96e013c19 | |||
| 3a4b89bfbc | |||
| a37e595dd7 | |||
| a18f6faeda | |||
| f53af48fb2 | |||
| f24a6bac3e | |||
| 8bb0274263 | |||
| e796e560fc | |||
| 891e6fdaf7 | |||
| 10bc075f24 | |||
| 2c7ad560ce | |||
| eb7aeb58cb | |||
| 4c081ada1e | |||
| a0136501fc | |||
| c74346a290 | |||
| abc630b943 | |||
| 67524ee6b6 | |||
| 00bd22a41a | |||
| 4c116fb0b6 | |||
| dafe717bb7 | |||
| 6005a4d870 | |||
| d099e3ffe5 | |||
| 5c8658d756 | |||
| 523de1feea | |||
| fc5639ee49 | |||
| 5db6d138be | |||
| a9d2b565d2 | |||
| 875b33c807 | |||
| 55004ddfc6 | |||
| fceccc43d5 | |||
| 661aee5cb6 | |||
| 339db8b754 | |||
| b3189fc7ce | |||
| 96fefc7ab3 | |||
| e8d7be6862 | |||
| 963f3a14d7 | |||
| d797d8f8b5 | |||
| 7598b428b7 | |||
| 2690079a91 | |||
| 628a6311be | |||
| 1c2732eb6f | |||
| 0627036aa0 | |||
| 4a664dd0d9 | |||
| 9de069f81a | |||
| 8068dc8dec | |||
| 247cfb388c | |||
| 1e059ac908 | |||
| ce50739191 | |||
| bada0e5490 | |||
| dfda2e25e1 | |||
| 60f818565c | |||
| a65711fcba | |||
| 3b5a4918e9 | |||
| 59ffec84e3 | |||
| 3e6b4d0b89 | |||
| 3bef02552e | |||
| eb81b99210 | |||
| 0e4cb6700b | |||
| bd07272b15 | |||
| 7e7dccd19f | |||
| a20dfd0ddc | |||
| 8d474e700f | |||
| 60b4bceaff | |||
| 0112461c69 | |||
| fa083703f4 | |||
| 39f422e6f5 | |||
| 1c0b83d5ed | |||
| b255a70e36 | |||
| f68b86f1de | |||
| 86e98f169b | |||
| 3027c40d19 | |||
| da2e12ab9a | |||
| 0c79fba1a1 | |||
| 4d24295bfb | |||
| 4a04632628 | |||
| 866c7ed086 | |||
| f16410b7c1 | |||
| fe4cc83839 | |||
| a3d63bb7ba | |||
| 328f5fc799 | |||
| 6b148fdb31 | |||
| a1f413a66d | |||
| 5e2feb9661 | |||
| 726d5ac622 | |||
| a86bb7feda | |||
| 259c66f442 | |||
| 3cae1e7f63 | |||
| d5095b1edf | |||
| 8998b66eb8 | |||
| 1ab8953769 | |||
| 1c5a0b220d | |||
| 86b3948e4a | |||
| 96d013b32b | |||
| 8467db6de1 | |||
| fcde3140a7 | |||
| eaabefa5a5 | |||
| 3874104325 | |||
| a9c1489184 | |||
| 82445dca01 | |||
| f2a4903cf8 | |||
| 3e2015adee | |||
| 90fa2aa8b0 | |||
| 3a4dcaa6a9 | |||
| 64751a0664 | |||
| 77b89795fd | |||
| 2ea987e7b1 | |||
| bd1234cead | |||
| c2bf6ad49e | |||
| dd3d82c267 | |||
| 6bad01f7d8 | |||
| 3e69d4728e | |||
| e792650ab2 | |||
| 719d05c872 | |||
| ac111feb6a | |||
| a90660621b | |||
| 14411a4bc2 | |||
| 6119707825 | |||
| e6bef22dce | |||
| 91d0876611 | |||
| 7ba1f08efd | |||
| f6ef638000 | |||
| 092e457506 | |||
| d3bd4a87c5 | |||
| f5f61b6749 | |||
| 764afba27b | |||
| cd70abe24a | |||
| f1e8bdeaca | |||
| 967d82628e | |||
| 8f4c13e3ba | |||
| d90edd2cb0 | |||
| 149ef9f598 | |||
| 03cc648234 | |||
| ba1835a58a | |||
| 17194d7ece | |||
| 883cee1e89 | |||
| 44f39077d8 | |||
| 01601e81ec | |||
| 49695c87d0 | |||
| 3ee6c2c1c0 | |||
| e27b46366f | |||
| c812d0cf03 | |||
| f004c106a6 | |||
| f9fbea1069 | |||
| 14ec81ab8b | |||
| 4e3fe7ec1f | |||
| 730673b7c4 | |||
| 039f510947 | |||
| 7d6478a543 | |||
| 84c9723e2a | |||
| b5b36bcdde | |||
| 912be7863b | |||
| 2597724def | |||
| fa01058934 | |||
| 548c3036e5 | |||
| 6d11b9f071 | |||
| fda460bcb8 | |||
| df47249f21 | |||
| 0d92dc93da | |||
| b803c86b87 | |||
| f0e6793431 | |||
| 96b6366ea3 | |||
| 6bfaa9ed93 | |||
| 5d073052fb | |||
| 22b059eeb1 | |||
| 64167cb245 | |||
| 9394dc1ea6 | |||
| 696ce3581c | |||
| 7121226a66 | |||
| 470222c717 | |||
| 96fb329ea3 | |||
| 5de7fa618b | |||
| 2f33ea7b12 | |||
| 0b3aeae1ba | |||
| 18d157df80 | |||
| 81346af450 | |||
| fa992af23c | |||
| ce7d4fe4be | |||
| a6c569ca71 | |||
| fb22b6ba1c | |||
| 257bbbeb8c | |||
| 0ff32a0d8c | |||
| ed1896ce43 | |||
| dcffbd1e00 | |||
| cd671bf62e | |||
| c569c298a3 | |||
| ea1600b0e7 | |||
| e4473569f7 | |||
| 32b69cb275 | |||
| b8d4bb8577 | |||
| 79ff0143d2 | |||
| e4e97440e6 | |||
| 2b756bb5fc | |||
| 6b60b292e8 | |||
| 2d3f026758 | |||
| 98227cf409 | |||
| c2a0918f58 | |||
| b5abebc90f | |||
| e64f00a3a6 | |||
| 7f53b1c066 | |||
| d34b8d614b | |||
| 2489bf9dcf | |||
| 6e72f60b2f | |||
| 0253c24d0d | |||
| 1416778928 | |||
| 5127a98e70 | |||
| 924fad3b24 | |||
| 7eec539333 | |||
| 6f4cbad1e3 | |||
| 7fbdbe156c | |||
| 2a2909c3ac | |||
| b760a59b65 | |||
| 2435a5e9d0 | |||
| 46834501ac | |||
| 6a7fd883fe | |||
| 8830b370c0 | |||
| 2596657863 | |||
| fe4146fa9e | |||
| d307c95654 | |||
| bcb73d9168 | |||
| 39391de0ac | |||
| 004d101ffc | |||
| b91bc46b5d | |||
| ede8f0d2a5 | |||
| b2d7164296 | |||
| d4cc857538 | |||
| af1c8d9e9b | |||
| f640afb956 | |||
| b4e73e6b95 | |||
| 81dbadd2ad | |||
| 0f6594009d | |||
| bf42c2e46d | |||
| 5337e6a2aa | |||
| 1d108b358a | |||
| 7be8c1c69f | |||
| 0f56ec16d6 | |||
| 468a0e1fb5 | |||
| 60e50ae661 | |||
| 65fb3b3161 | |||
| ad8a4c74c6 | |||
| 93d36c1c8c | |||
| a3e5b53893 | |||
| 486ba8b599 | |||
| 99603aef6b | |||
| 50e1b719c5 | |||
| 037f326ff9 | |||
| dc510034b1 | |||
| 8024d82cc8 | |||
| b4b9ba58e6 | |||
| fbe5eedc75 | |||
| 1593630a7d | |||
| f7c8b76680 | |||
| d82b68c77e | |||
| 19c9441f61 | |||
| 0855defb69 | |||
| b47dc4468c | |||
| 7c64230de3 | |||
| 670403f893 | |||
| 8cf8e7efc0 | |||
| e6c01e01dc | |||
| 59c3f54aca | |||
| 3f4cc4c736 | |||
| 2db5788cdd | |||
| ab671e2f5c | |||
| f55cd39672 | |||
| 991d84a511 | |||
| d9cdba2317 | |||
| 7e338c9e8b | |||
| 41f7679de0 | |||
| ba5ca1cdb5 | |||
| 897a2c5ab1 | |||
| d76f1f2059 | |||
| 59a21bc86a | |||
| 58cf74a99c | |||
| 2219c7bb84 | |||
| ca5dfaafa4 | |||
| 20d1ad6474 | |||
| f5f51adae5 | |||
| 01f68c7531 | |||
| e6f1e7f216 | |||
| 84f68c4fc2 | |||
| d1b2c763d3 | |||
| 15d527ae99 | |||
| a4a3e5cdc6 | |||
| aed36bec6e | |||
| a84a10ab88 | |||
| 78f4aca850 | |||
| 12730f25ce | |||
| 18941d3e18 | |||
| 341fdb6bde | |||
| 0240ece6ed | |||
| 6ab6699184 | |||
| 53bc821837 | |||
| f1ae5a7e6f | |||
| 9bfd0c1d13 | |||
| 5c84b416ad | |||
| 87d25c2e60 | |||
| 64d6be7f8a | |||
| 40771de6a1 | |||
| 95ac0895f1 | |||
| 84768b4c77 | |||
| 9311cb94b4 | |||
| 34f0da1710 | |||
| b34b24ef8f | |||
| 4a8beafb39 | |||
| cd68f09b07 | |||
| fa86bc9d4b | |||
| fc657f6863 | |||
| 79244165ae | |||
| 2e2988b322 | |||
| 4916a8b7f5 | |||
| a54255a765 | |||
| 58b1deaa60 | |||
| 998e5cd259 | |||
| 4ce2d432d4 | |||
| c6c5b6ff86 | |||
| 3dd402b308 | |||
| 3e39557dd9 | |||
| beaaa5d674 | |||
| ad6dae9dd3 | |||
| 49e3d724c0 | |||
| ef42ce949d | |||
| e5ec2a2ea2 | |||
| e1df53fdc4 | |||
| 6a1865a36a | |||
| d55b05d972 | |||
| a6694a3a58 | |||
| 7a41591ff5 | |||
| 4af7c8cb8e | |||
| d488bd591c | |||
| 3fc68c2388 | |||
| 84380fedf6 | |||
| d46d1fde6b | |||
| d84df9135a | |||
| dba6e5307a | |||
| 4e868f48a7 | |||
| a60be3a705 | |||
| b286337bd1 | |||
| d4a408bb08 | |||
| c44b7fa7ac | |||
| b9b68badcd | |||
| d2ade34435 | |||
| 0b04a28b40 | |||
| 906c398cc2 | |||
| c0d2c9a714 | |||
| b3b3f4fe3c | |||
| 6b85c1a885 | |||
| 0344b465cc | |||
| 0a2629f6c0 | |||
| a58f4d0dcf | |||
| 4229eed1ba | |||
| b91710ea38 | |||
| 3f35e7fd36 | |||
| fe3691c764 | |||
| 6294e01bb9 | |||
| c50a28728e | |||
| 776f2692fa | |||
| cbc86affd4 | |||
| 887bd1e321 | |||
| faea467d88 | |||
| 00e0cd82ec | |||
| 950776b82e | |||
| 596569ed07 | |||
| 8dee2a1762 | |||
| dfa9473e00 | |||
| 44fcaf953b | |||
| f913f33786 | |||
| 0076da3449 | |||
| 429e6fc9a8 | |||
| 198f7a97ee | |||
| dfe3e4a7bd | |||
| 32124dc0d2 | |||
| 2bded9b77d | |||
| 9bd7acd0a9 | |||
| 191772e005 | |||
| c2f1e07951 | |||
| 6ed93ab830 | |||
| 1b1f035b1a | |||
| 65d404b2ea | |||
| 2e4a3b1dbf | |||
| 2803c8ee6b | |||
| d1d1e8ed2f | |||
| e7de053171 | |||
| e864a16977 | |||
| b8d7f73852 | |||
| d150e6e8a8 | |||
| c2312bb3a4 | |||
| 77c023f42c | |||
| 96def3e3c2 | |||
| e799287e28 | |||
| 72c71bd4fc | |||
| 6b625e146d | |||
| 4545d66d1a | |||
| 0a848a078e | |||
| fadca6470d | |||
| 7477dc95d4 | |||
| 11a2905a9b | |||
| 17b34c2d83 | |||
| 988cd8dc59 | |||
| a22fe931b7 | |||
| fbec513063 | |||
| c3b5344dfa | |||
| 3bd52f3409 | |||
| bb64ec196b | |||
| 21db1063d6 | |||
| 734bc689e2 | |||
| 418966be8d | |||
| 89033d7728 | |||
| 13946bf1cc | |||
| 24964d473c | |||
| 9c335b7e29 | |||
| 0a5494aeac | |||
| 39f8e8d0e8 | |||
| a205d73613 | |||
| 2b7588492a | |||
| 5017d0d5b0 | |||
| 77db169d38 | |||
| 1e77b62a68 | |||
| 8a4a2b5308 | |||
| 55d28770ea | |||
| 806b8f42f8 | |||
| b536f5f65e | |||
| 1e37ca3d5b | |||
| e8b7acff4c | |||
| 868e904835 | |||
| c0b9229a44 | |||
| 02c018e858 | |||
| 462da51ce0 | |||
| 482124e2ba | |||
| 626a0e83bf | |||
| aac83936d8 | |||
| fc2ea0e552 | |||
| 86eec05a24 | |||
| 72e6791c29 | |||
| 28d123adc2 | |||
| 18a2a7e2d4 | |||
| 5dece3123e | |||
| 1b859eba49 | |||
| 7fd7dc5f4f | |||
| c58528483c | |||
| 518677d8b0 | |||
| a1ae9e3ce9 | |||
| 5c397b34a2 | |||
| eb30fd12ae | |||
| d7d0063507 | |||
| 6b3e27307e | |||
| dd13ea78f0 | |||
| 6ec6db77c2 | |||
| 00f45fd203 | |||
| 39199a1f88 | |||
| d70807a76b | |||
| 4a6fa143b2 | |||
| 3c3c65a94e | |||
| 0341bc96a8 | |||
| f7ec6579dc | |||
| 8a41f29574 | |||
| b3c9753a17 | |||
| 0b382a684f | |||
| 705ac806bf | |||
| c6b39ac660 | |||
| d97804a13c | |||
| 8bd0db298c | |||
| 37a0e5bf9e | |||
| 1ea6ddb786 | |||
| ff1dd8935f | |||
| d2753887bc | |||
| 3c4c11d1cd | |||
| 984f9340b6 | |||
| 36d29e70a4 | |||
| e82cdfcfab | |||
| a6a5b90523 | |||
| 8dba3c9387 | |||
| e41d092974 | |||
| 03da5dcff3 | |||
| f11619675c | |||
| b0175b3356 | |||
| 0bc436e752 | |||
| e3db5921a2 | |||
| c25e17897a | |||
| bbcfd26a99 | |||
| 5990a1c9c1 | |||
| 9398cda4bd | |||
| 398886130b | |||
| bc5dd3f74a | |||
| 2c55cdc64e | |||
| f9e30484ba | |||
| 7ac08fc54f | |||
| 04be4dfad1 | |||
| e8acffa6f7 | |||
| 0cc6d44dcc | |||
| e72b177efd | |||
| 95f41d3c55 | |||
| e0448e5c91 | |||
| f151ab76ce | |||
| 631e21c7f0 | |||
| 4f29de1216 | |||
| eab4c4bb1d | |||
| ac5dbc0a76 | |||
| df23298dd2 | |||
| b338e81c7b | |||
| 8913152f3e | |||
| 87d1189115 | |||
| 8148a062eb | |||
| 3d4fd218b9 | |||
| fc36d4c388 | |||
| 7a6687f7ea | |||
| 7e4c68385a | |||
| 64284bdfdb | |||
| c5edc02e6e | |||
| b73231d1a2 | |||
| 1d2f9f0fe8 | |||
| 6d078b06f4 | |||
| e831a43dc5 | |||
| 106a560e55 | |||
| 35d39f92fc | |||
| 6c214059f4 | |||
| 4bf9846a9a | |||
| 5a915d3187 | |||
| 2c63cd393c | |||
| d9c0b0ac57 | |||
| f8f7f43e13 | |||
| c6da11f32c | |||
| a21dfa1430 | |||
| ff49ac87f9 | |||
| 32e9fb3cca | |||
| f822e3ec6f | |||
| 24f4c43ccb | |||
| c089f2dd31 | |||
| 5a2cde3e8e | |||
| 282a0e0562 | |||
| 5dcbedf841 | |||
| 0f57f6a639 | |||
| 24b439d12c | |||
| 4420b31469 | |||
| 1d335d1fd1 | |||
| b9f988a079 | |||
| 5d521d627b | |||
| a8bbf59a11 | |||
| bc75c30eab | |||
| 250196bce4 | |||
| 1669833cb2 | |||
| 2a0efaaca3 | |||
| 3d81ee689f | |||
| 3584023cb4 | |||
| 443003fe1a | |||
| a2056bb2f5 | |||
| bece2d49e8 | |||
| a27e11ac1f | |||
| 5be82c151f | |||
| c70daed0e9 | |||
| 3fe1bd7dd3 | |||
| 1beb2f546a | |||
| 56d82049d0 | |||
| 5df8eed433 | |||
| 38ab619a08 | |||
| 7c8467fbe8 | |||
| fc4b467545 | |||
| e8d260321e | |||
| bbf97260ed | |||
| a5f3bd69d7 | |||
| b415457874 | |||
| 8d8b8d8393 | |||
| f4d47b0e17 | |||
| f1ff79b427 |
@@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"defaultHomeserver": 0,
|
||||||
|
"homeserverList": ["matrix.lotusguild.org"],
|
||||||
|
"allowCustomHomeservers": false,
|
||||||
|
"featuredCommunities": {
|
||||||
|
"openAsDefault": false,
|
||||||
|
"spaces": [],
|
||||||
|
"rooms": [],
|
||||||
|
"servers": []
|
||||||
|
},
|
||||||
|
"hashRouter": {
|
||||||
|
"enabled": false,
|
||||||
|
"basename": "/"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"defaultHomeserver": 0,
|
||||||
|
"homeserverList": [
|
||||||
|
"matrix.lotusguild.org"
|
||||||
|
],
|
||||||
|
"allowCustomHomeservers": false,
|
||||||
|
"featuredCommunities": {
|
||||||
|
"openAsDefault": false,
|
||||||
|
"spaces": [],
|
||||||
|
"rooms": [],
|
||||||
|
"servers": []
|
||||||
|
},
|
||||||
|
"hashRouter": {
|
||||||
|
"enabled": false,
|
||||||
|
"basename": "/"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
VITE_APP_VERSION=lotus
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
experiment
|
|
||||||
node_modules
|
|
||||||
@@ -1,72 +0,0 @@
|
|||||||
module.exports = {
|
|
||||||
env: {
|
|
||||||
browser: true,
|
|
||||||
es2021: true,
|
|
||||||
},
|
|
||||||
extends: [
|
|
||||||
"eslint:recommended",
|
|
||||||
"plugin:react/recommended",
|
|
||||||
"plugin:react-hooks/recommended",
|
|
||||||
"plugin:@typescript-eslint/eslint-recommended",
|
|
||||||
"plugin:@typescript-eslint/recommended",
|
|
||||||
'airbnb',
|
|
||||||
'prettier',
|
|
||||||
],
|
|
||||||
parser: "@typescript-eslint/parser",
|
|
||||||
parserOptions: {
|
|
||||||
ecmaFeatures: {
|
|
||||||
jsx: true,
|
|
||||||
},
|
|
||||||
ecmaVersion: 'latest',
|
|
||||||
sourceType: 'module',
|
|
||||||
},
|
|
||||||
"globals": {
|
|
||||||
JSX: "readonly"
|
|
||||||
},
|
|
||||||
plugins: [
|
|
||||||
'react',
|
|
||||||
'@typescript-eslint'
|
|
||||||
],
|
|
||||||
rules: {
|
|
||||||
'linebreak-style': 0,
|
|
||||||
'no-underscore-dangle': 0,
|
|
||||||
"no-shadow": "off",
|
|
||||||
|
|
||||||
"import/prefer-default-export": "off",
|
|
||||||
"import/extensions": "off",
|
|
||||||
"import/no-unresolved": "off",
|
|
||||||
"import/no-extraneous-dependencies": [
|
|
||||||
"error",
|
|
||||||
{
|
|
||||||
devDependencies: true,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
|
|
||||||
'react/no-unstable-nested-components': [
|
|
||||||
'error',
|
|
||||||
{ allowAsProps: true },
|
|
||||||
],
|
|
||||||
"react/jsx-filename-extension": [
|
|
||||||
"error",
|
|
||||||
{
|
|
||||||
extensions: [".tsx", ".jsx"],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
|
|
||||||
"react/require-default-props": "off",
|
|
||||||
"react/jsx-props-no-spreading": "off",
|
|
||||||
"react-hooks/rules-of-hooks": "error",
|
|
||||||
"react-hooks/exhaustive-deps": "error",
|
|
||||||
|
|
||||||
"@typescript-eslint/no-unused-vars": "error",
|
|
||||||
"@typescript-eslint/no-shadow": "error"
|
|
||||||
},
|
|
||||||
overrides: [
|
|
||||||
{
|
|
||||||
files: ['*.ts'],
|
|
||||||
rules: {
|
|
||||||
'no-undef': 'off',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
@@ -0,0 +1,118 @@
|
|||||||
|
name: CI
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [lotus]
|
||||||
|
pull_request:
|
||||||
|
branches: [lotus]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
name: Build & Quality Checks
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Setup Node.js
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version-file: '.node-version'
|
||||||
|
cache: npm
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
# Harden against transient registry network failures (ECONNRESET etc.):
|
||||||
|
# raise npm's built-in fetch retries/timeouts and retry `npm ci` up to
|
||||||
|
# 3 times with backoff before failing the build.
|
||||||
|
run: |
|
||||||
|
npm config set fetch-retries 5
|
||||||
|
npm config set fetch-retry-mintimeout 20000
|
||||||
|
npm config set fetch-retry-maxtimeout 120000
|
||||||
|
npm config set fetch-timeout 600000
|
||||||
|
for attempt in 1 2 3; do
|
||||||
|
echo "npm ci attempt $attempt…"
|
||||||
|
npm ci && break
|
||||||
|
if [ "$attempt" = "3" ]; then
|
||||||
|
echo "npm ci failed after 3 attempts" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "npm ci failed; retrying in $((attempt * 15))s…" >&2
|
||||||
|
sleep $((attempt * 15))
|
||||||
|
done
|
||||||
|
|
||||||
|
# ── Critical gate — if this fails, nothing deploys ──────────────────
|
||||||
|
- name: Build
|
||||||
|
run: npm run build
|
||||||
|
env:
|
||||||
|
NODE_OPTIONS: '--max_old_space_size=4096'
|
||||||
|
VITE_APP_VERSION: ${{ github.sha }}
|
||||||
|
|
||||||
|
# Unit tests are a hard gate too — deterministic pure-logic tests on Node's
|
||||||
|
# built-in runner via tsx (no vitest — Vite 8 is ahead of vitest's range).
|
||||||
|
# A failure blocks the deploy.
|
||||||
|
- name: Unit tests
|
||||||
|
run: npm test
|
||||||
|
|
||||||
|
# ── Quality checks (informational — pre-existing issues exist) ───────
|
||||||
|
- name: TypeScript
|
||||||
|
run: npm run typecheck
|
||||||
|
continue-on-error: true
|
||||||
|
|
||||||
|
- name: ESLint
|
||||||
|
run: npm run check:eslint
|
||||||
|
continue-on-error: true
|
||||||
|
|
||||||
|
- name: Prettier
|
||||||
|
run: npm run check:prettier
|
||||||
|
continue-on-error: true
|
||||||
|
|
||||||
|
# ── Security ─────────────────────────────────────────────────────────
|
||||||
|
- name: Audit (high/critical)
|
||||||
|
run: npm audit --audit-level=high --omit=dev
|
||||||
|
continue-on-error: true
|
||||||
|
|
||||||
|
# ── Bundle size report ───────────────────────────────────────────────
|
||||||
|
- name: Report bundle sizes
|
||||||
|
run: |
|
||||||
|
echo "### Bundle sizes" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "| File | Size | Gzip |" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "|------|------|------|" >> $GITHUB_STEP_SUMMARY
|
||||||
|
find dist/assets -name "*.js" -not -name "*.map" | sort | while read f; do
|
||||||
|
name=$(basename "$f")
|
||||||
|
size=$(du -sh "$f" | cut -f1)
|
||||||
|
gzip_size=$(gzip -c "$f" | wc -c | awk '{printf "%.1f kB", $1/1024}')
|
||||||
|
echo "| $name | $size | $gzip_size |" >> $GITHUB_STEP_SUMMARY
|
||||||
|
done
|
||||||
|
|
||||||
|
# ── Desktop build trigger ──────────────────────────────────────────────
|
||||||
|
# Gated on `build` succeeding so a broken push (e.g. failing `npm ci` or
|
||||||
|
# `npm run build`) never bumps the cinny-desktop submodule and kicks off the
|
||||||
|
# slow Tauri release builds, which would only error out downstream. Only
|
||||||
|
# runs on a real push to lotus — not on pull_request CI runs.
|
||||||
|
trigger-desktop:
|
||||||
|
name: Trigger Desktop Build
|
||||||
|
needs: build
|
||||||
|
if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/lotus' }}
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Bump cinny submodule
|
||||||
|
env:
|
||||||
|
TOKEN: ${{ secrets.RELEASE_TOKEN }}
|
||||||
|
run: |
|
||||||
|
CINNY_SHA="${{ github.sha }}"
|
||||||
|
git clone "https://x-access-token:$TOKEN@code.lotusguild.org/LotusGuild/cinny-desktop.git" desktop
|
||||||
|
cd desktop
|
||||||
|
git config user.email "ci@lotusguild.org"
|
||||||
|
git config user.name "Lotus CI"
|
||||||
|
git submodule update --init cinny
|
||||||
|
git -C cinny fetch origin
|
||||||
|
git -C cinny checkout "$CINNY_SHA"
|
||||||
|
git add cinny
|
||||||
|
if git diff --cached --quiet; then
|
||||||
|
echo "Submodule already at $CINNY_SHA, nothing to do"
|
||||||
|
else
|
||||||
|
git commit -m "chore: bump cinny submodule to ${CINNY_SHA:0:8}"
|
||||||
|
git push origin main
|
||||||
|
echo "Pushed — cinny-desktop release.yml will start via on:push trigger"
|
||||||
|
fi
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
labels: ["needs-confirmation"]
|
labels: ['needs-confirmation']
|
||||||
body:
|
body:
|
||||||
- type: markdown #add faqs in future
|
- type: markdown #add faqs in future
|
||||||
attributes:
|
attributes:
|
||||||
@@ -7,7 +7,7 @@ body:
|
|||||||
> Please read through [the Discussion rules](https://github.com/cinnyapp/cinny/discussions/2653) and check for both existing [Discussions](https://github.com/cinnyapp/cinny/discussions?discussions_q=) and [Issues](https://github.com/cinnyapp/cinny/issues?q=sort%3Areactions-desc) prior to opening a new Discussion.
|
> Please read through [the Discussion rules](https://github.com/cinnyapp/cinny/discussions/2653) and check for both existing [Discussions](https://github.com/cinnyapp/cinny/discussions?discussions_q=) and [Issues](https://github.com/cinnyapp/cinny/issues?q=sort%3Areactions-desc) prior to opening a new Discussion.
|
||||||
- type: markdown
|
- type: markdown
|
||||||
attributes:
|
attributes:
|
||||||
value: "# Issue Details"
|
value: '# Issue Details'
|
||||||
- type: textarea
|
- type: textarea
|
||||||
attributes:
|
attributes:
|
||||||
label: Issue Description
|
label: Issue Description
|
||||||
@@ -64,7 +64,7 @@ body:
|
|||||||
- Browser:
|
- Browser:
|
||||||
- Cinny Web Version: (app.cinny.in or self hosted)
|
- Cinny Web Version: (app.cinny.in or self hosted)
|
||||||
- Cinny desktop Version: (appimage or deb or flatpak)
|
- Cinny desktop Version: (appimage or deb or flatpak)
|
||||||
- Matrix Homeserver:
|
- Matrix Homeserver:
|
||||||
placeholder: |
|
placeholder: |
|
||||||
- OS: Windows 11
|
- OS: Windows 11
|
||||||
- Browser: Chrome 120.0.6099.109
|
- Browser: Chrome 120.0.6099.109
|
||||||
@@ -80,12 +80,12 @@ body:
|
|||||||
label: Relevant Logs
|
label: Relevant Logs
|
||||||
description: |
|
description: |
|
||||||
If applicable, add browser console logs to help explain your problem.
|
If applicable, add browser console logs to help explain your problem.
|
||||||
|
|
||||||
**To get browser console logs:**
|
**To get browser console logs:**
|
||||||
- Chrome/Edge: Press F12 → Console tab
|
- Chrome/Edge: Press F12 → Console tab
|
||||||
- Firefox: Press F12 → Console tab
|
- Firefox: Press F12 → Console tab
|
||||||
- Safari: Develop → Show Web Inspector → Console
|
- Safari: Develop → Show Web Inspector → Console
|
||||||
|
|
||||||
Please wrap large log outputs in code blocks with triple backticks (```).
|
Please wrap large log outputs in code blocks with triple backticks (```).
|
||||||
placeholder: |
|
placeholder: |
|
||||||
```
|
```
|
||||||
@@ -98,7 +98,7 @@ body:
|
|||||||
render: shell
|
render: shell
|
||||||
validations:
|
validations:
|
||||||
required: false
|
required: false
|
||||||
- type: textarea
|
- type: textarea
|
||||||
attributes:
|
attributes:
|
||||||
label: Additional context
|
label: Additional context
|
||||||
description: |
|
description: |
|
||||||
@@ -119,7 +119,7 @@ body:
|
|||||||
> Use these links to review the existing Cinny [Discussions](https://github.com/cinnyapp/cinny/discussions?discussions_q=) and [Issues](https://github.com/cinnyapp/cinny/issues?q=sort%3Areactions-desc).
|
> Use these links to review the existing Cinny [Discussions](https://github.com/cinnyapp/cinny/discussions?discussions_q=) and [Issues](https://github.com/cinnyapp/cinny/issues?q=sort%3Areactions-desc).
|
||||||
- type: checkboxes #add faqs in future
|
- type: checkboxes #add faqs in future
|
||||||
attributes:
|
attributes:
|
||||||
label: "I acknowledge that:"
|
label: 'I acknowledge that:'
|
||||||
options:
|
options:
|
||||||
- label: I have searched the Cinny repository (both open and closed Discussions and Issues) and confirm this is not a duplicate of an existing issue or discussion.
|
- label: I have searched the Cinny repository (both open and closed Discussions and Issues) and confirm this is not a duplicate of an existing issue or discussion.
|
||||||
required: true
|
required: true
|
||||||
|
|||||||
@@ -2,29 +2,29 @@
|
|||||||
|
|
||||||
version: 2
|
version: 2
|
||||||
updates:
|
updates:
|
||||||
# - package-ecosystem: npm
|
# - package-ecosystem: npm
|
||||||
# directory: /
|
# directory: /
|
||||||
# schedule:
|
# schedule:
|
||||||
# interval: weekly
|
# interval: weekly
|
||||||
# day: "tuesday"
|
# day: "tuesday"
|
||||||
# time: "01:00"
|
# time: "01:00"
|
||||||
# timezone: "Asia/Kolkata"
|
# timezone: "Asia/Kolkata"
|
||||||
# open-pull-requests-limit: 15
|
# open-pull-requests-limit: 15
|
||||||
|
|
||||||
- package-ecosystem: github-actions
|
- package-ecosystem: github-actions
|
||||||
directory: /
|
directory: /
|
||||||
schedule:
|
schedule:
|
||||||
interval: weekly
|
interval: weekly
|
||||||
day: "tuesday"
|
day: 'tuesday'
|
||||||
time: "01:00"
|
time: '01:00'
|
||||||
timezone: "Asia/Kolkata"
|
timezone: 'Asia/Kolkata'
|
||||||
open-pull-requests-limit: 5
|
open-pull-requests-limit: 5
|
||||||
|
|
||||||
- package-ecosystem: docker
|
- package-ecosystem: docker
|
||||||
directory: /
|
directory: /
|
||||||
schedule:
|
schedule:
|
||||||
interval: weekly
|
interval: weekly
|
||||||
day: "tuesday"
|
day: 'tuesday'
|
||||||
time: "01:00"
|
time: '01:00'
|
||||||
timezone: "Asia/Kolkata"
|
timezone: 'Asia/Kolkata'
|
||||||
open-pull-requests-limit: 5
|
open-pull-requests-limit: 5
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ jobs:
|
|||||||
- name: Setup node
|
- name: Setup node
|
||||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||||
with:
|
with:
|
||||||
node-version-file: ".node-version"
|
node-version-file: '.node-version'
|
||||||
package-manager-cache: false
|
package-manager-cache: false
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: npm ci
|
run: npm ci
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
name: Deploy PR to Netlify
|
name: Deploy PR to Netlify
|
||||||
run-name: "Deploy PR to Netlify (${{ github.event.workflow_run.head_branch }})"
|
run-name: 'Deploy PR to Netlify (${{ github.event.workflow_run.head_branch }})'
|
||||||
|
|
||||||
on:
|
on:
|
||||||
workflow_run:
|
workflow_run:
|
||||||
workflows: ["Build pull request"]
|
workflows: ['Build pull request']
|
||||||
types: [completed]
|
types: [completed]
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
deploy-pull-request:
|
deploy-pull-request:
|
||||||
@@ -42,7 +42,7 @@ jobs:
|
|||||||
uses: nwtgck/actions-netlify@4cbaf4c08f1a7bfa537d6113472ef4424e4eb654 # v3.0.0
|
uses: nwtgck/actions-netlify@4cbaf4c08f1a7bfa537d6113472ef4424e4eb654 # v3.0.0
|
||||||
with:
|
with:
|
||||||
publish-dir: dist
|
publish-dir: dist
|
||||||
deploy-message: "Deploy PR ${{ steps.pr.outputs.id }}"
|
deploy-message: 'Deploy PR ${{ steps.pr.outputs.id }}'
|
||||||
alias: ${{ steps.pr.outputs.id }}
|
alias: ${{ steps.pr.outputs.id }}
|
||||||
# These don't work because we're in workflow_run
|
# These don't work because we're in workflow_run
|
||||||
enable-pull-request-comment: false
|
enable-pull-request-comment: false
|
||||||
@@ -59,5 +59,5 @@ jobs:
|
|||||||
pr-number: ${{ steps.pr.outputs.id }}
|
pr-number: ${{ steps.pr.outputs.id }}
|
||||||
comment-tag: ${{ steps.pr.outputs.id }}
|
comment-tag: ${{ steps.pr.outputs.id }}
|
||||||
message: |
|
message: |
|
||||||
Preview: ${{ steps.netlify.outputs.deploy-url }}
|
Preview: ${{ steps.netlify.outputs.deploy-url }}
|
||||||
⚠️ Exercise caution. Use test accounts. ⚠️
|
⚠️ Exercise caution. Use test accounts. ⚠️
|
||||||
|
|||||||
@@ -20,13 +20,13 @@ jobs:
|
|||||||
|
|
||||||
- name: Set up QEMU
|
- name: Set up QEMU
|
||||||
uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3.7.0
|
uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3.7.0
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0
|
uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0
|
||||||
|
|
||||||
- name: Login to Docker Hub #Do not update this action from a outside PR
|
- name: Login to Docker Hub #Do not update this action from a outside PR
|
||||||
if: github.event.pull_request.head.repo.fork == false
|
if: github.event.pull_request.head.repo.fork == false
|
||||||
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
|
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
|
||||||
with:
|
with:
|
||||||
username: ${{ secrets.DOCKER_USERNAME }}
|
username: ${{ secrets.DOCKER_USERNAME }}
|
||||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||||
@@ -34,7 +34,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Login to the Github Container registry #Do not update this action from a outside PR
|
- name: Login to the Github Container registry #Do not update this action from a outside PR
|
||||||
if: github.event.pull_request.head.repo.fork == false
|
if: github.event.pull_request.head.repo.fork == false
|
||||||
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
|
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
|
||||||
with:
|
with:
|
||||||
registry: ghcr.io
|
registry: ghcr.io
|
||||||
username: ${{ github.actor }}
|
username: ${{ github.actor }}
|
||||||
@@ -43,14 +43,14 @@ jobs:
|
|||||||
|
|
||||||
- name: Extract metadata (tags, labels) for Docker, GHCR
|
- name: Extract metadata (tags, labels) for Docker, GHCR
|
||||||
id: meta
|
id: meta
|
||||||
uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # v5.10.0
|
uses: docker/metadata-action@80c7e94dd9b9319bd5eb7a0e0fe9291e23a2a2e9 # v6.1.0
|
||||||
with:
|
with:
|
||||||
images: |
|
images: |
|
||||||
ajbura/cinny
|
ajbura/cinny
|
||||||
ghcr.io/${{ github.repository }}
|
ghcr.io/${{ github.repository }}
|
||||||
|
|
||||||
- name: Build Docker image (no push)
|
- name: Build Docker image (no push)
|
||||||
uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0
|
uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.0
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
platforms: linux/amd64
|
platforms: linux/amd64
|
||||||
|
|||||||
@@ -23,4 +23,4 @@ jobs:
|
|||||||
collapsibleThreshold: 25
|
collapsibleThreshold: 25
|
||||||
failOnDowngrade: false
|
failOnDowngrade: false
|
||||||
path: package-lock.json
|
path: package-lock.json
|
||||||
updateComment: true
|
updateComment: true
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ jobs:
|
|||||||
- name: Setup node
|
- name: Setup node
|
||||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||||
with:
|
with:
|
||||||
node-version-file: ".node-version"
|
node-version-file: '.node-version'
|
||||||
package-manager-cache: false
|
package-manager-cache: false
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: npm ci
|
run: npm ci
|
||||||
|
|||||||
@@ -70,27 +70,27 @@ jobs:
|
|||||||
- name: Set up QEMU
|
- name: Set up QEMU
|
||||||
uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3.7.0
|
uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3.7.0
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0
|
uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0
|
||||||
- name: Login to Docker Hub #Do not update this action from a outside PR
|
- name: Login to Docker Hub #Do not update this action from a outside PR
|
||||||
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
|
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
|
||||||
with:
|
with:
|
||||||
username: ${{ secrets.DOCKER_USERNAME }}
|
username: ${{ secrets.DOCKER_USERNAME }}
|
||||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||||
- name: Login to the Github Container registry #Do not update this action from a outside PR
|
- name: Login to the Github Container registry #Do not update this action from a outside PR
|
||||||
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
|
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
|
||||||
with:
|
with:
|
||||||
registry: ghcr.io
|
registry: ghcr.io
|
||||||
username: ${{ github.actor }}
|
username: ${{ github.actor }}
|
||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
- name: Extract metadata (tags, labels) for Docker, GHCR
|
- name: Extract metadata (tags, labels) for Docker, GHCR
|
||||||
id: meta
|
id: meta
|
||||||
uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # v5.10.0
|
uses: docker/metadata-action@80c7e94dd9b9319bd5eb7a0e0fe9291e23a2a2e9 # v6.1.0
|
||||||
with:
|
with:
|
||||||
images: |
|
images: |
|
||||||
${{ secrets.DOCKER_USERNAME }}/cinny
|
${{ secrets.DOCKER_USERNAME }}/cinny
|
||||||
ghcr.io/${{ github.repository }}
|
ghcr.io/${{ github.repository }}
|
||||||
- name: Build and push Docker image
|
- name: Build and push Docker image
|
||||||
uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0
|
uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.0
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
platforms: linux/amd64,linux/arm64
|
platforms: linux/amd64,linux/arm64
|
||||||
|
|||||||
@@ -4,4 +4,5 @@ node_modules
|
|||||||
devAssets
|
devAssets
|
||||||
|
|
||||||
.DS_Store
|
.DS_Store
|
||||||
.idea
|
.ideapackage-lock.json
|
||||||
|
public/decorations/
|
||||||
|
|||||||
@@ -1,2 +1,3 @@
|
|||||||
legacy-peer-deps=true
|
legacy-peer-deps=true
|
||||||
save-exact=true
|
save-exact=true
|
||||||
|
@lotusguild:registry=https://code.lotusguild.org/api/packages/LotusGuild/npm/
|
||||||
@@ -17,23 +17,23 @@ diverse, inclusive, and healthy community.
|
|||||||
Examples of behavior that contributes to a positive environment for our
|
Examples of behavior that contributes to a positive environment for our
|
||||||
community include:
|
community include:
|
||||||
|
|
||||||
* Demonstrating empathy and kindness toward other people
|
- Demonstrating empathy and kindness toward other people
|
||||||
* Being respectful of differing opinions, viewpoints, and experiences
|
- Being respectful of differing opinions, viewpoints, and experiences
|
||||||
* Giving and gracefully accepting constructive feedback
|
- Giving and gracefully accepting constructive feedback
|
||||||
* Accepting responsibility and apologizing to those affected by our mistakes,
|
- Accepting responsibility and apologizing to those affected by our mistakes,
|
||||||
and learning from the experience
|
and learning from the experience
|
||||||
* Focusing on what is best not just for us as individuals, but for the
|
- Focusing on what is best not just for us as individuals, but for the
|
||||||
overall community
|
overall community
|
||||||
|
|
||||||
Examples of unacceptable behavior include:
|
Examples of unacceptable behavior include:
|
||||||
|
|
||||||
* The use of sexualized language or imagery, and sexual attention or
|
- The use of sexualized language or imagery, and sexual attention or
|
||||||
advances of any kind
|
advances of any kind
|
||||||
* Trolling, insulting or derogatory comments, and personal or political attacks
|
- Trolling, insulting or derogatory comments, and personal or political attacks
|
||||||
* Public or private harassment
|
- Public or private harassment
|
||||||
* Publishing others' private information, such as a physical or email
|
- Publishing others' private information, such as a physical or email
|
||||||
address, without their explicit permission
|
address, without their explicit permission
|
||||||
* Other conduct which could reasonably be considered inappropriate in a
|
- Other conduct which could reasonably be considered inappropriate in a
|
||||||
professional setting
|
professional setting
|
||||||
|
|
||||||
## Enforcement Responsibilities
|
## Enforcement Responsibilities
|
||||||
@@ -106,7 +106,7 @@ Violating these terms may lead to a permanent ban.
|
|||||||
### 4. Permanent Ban
|
### 4. Permanent Ban
|
||||||
|
|
||||||
**Community Impact**: Demonstrating a pattern of violation of community
|
**Community Impact**: Demonstrating a pattern of violation of community
|
||||||
standards, including sustained inappropriate behavior, harassment of an
|
standards, including sustained inappropriate behavior, harassment of an
|
||||||
individual, or aggression toward or disparagement of classes of individuals.
|
individual, or aggression toward or disparagement of classes of individuals.
|
||||||
|
|
||||||
**Consequence**: A permanent ban from any sort of public interaction within
|
**Consequence**: A permanent ban from any sort of public interaction within
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ First off, thanks for taking the time to contribute! ❤️
|
|||||||
All types of contributions are encouraged and valued. Please make sure to read the relevant section before making your contribution. It will make it a lot easier for us maintainers and smooth out the experience for all involved. The community looks forward to your contributions. 🎉
|
All types of contributions are encouraged and valued. Please make sure to read the relevant section before making your contribution. It will make it a lot easier for us maintainers and smooth out the experience for all involved. The community looks forward to your contributions. 🎉
|
||||||
|
|
||||||
> And if you like the project, but just don't have time to contribute, that's fine. There are other easy ways to support the project and show your appreciation, which we would also be very happy about:
|
> And if you like the project, but just don't have time to contribute, that's fine. There are other easy ways to support the project and show your appreciation, which we would also be very happy about:
|
||||||
|
>
|
||||||
> - Star the project
|
> - Star the project
|
||||||
> - Tweet about it (tag @cinnyapp)
|
> - Tweet about it (tag @cinnyapp)
|
||||||
> - Refer this project in your project's readme
|
> - Refer this project in your project's readme
|
||||||
@@ -18,6 +19,7 @@ Bug reports and feature suggestions must use descriptive and concise titles and
|
|||||||
## Pull requests
|
## Pull requests
|
||||||
|
|
||||||
> ### Legal Notice
|
> ### Legal Notice
|
||||||
|
>
|
||||||
> When contributing to this project, you must agree that you have authored 100% of the content, that you have the necessary rights to the content and that the content you contribute may be provided under the project license. You will also be asked to [sign the CLA](https://github.com/cinnyapp/cla) upon submiting your pull request.
|
> When contributing to this project, you must agree that you have authored 100% of the content, that you have the necessary rights to the content and that the content you contribute may be provided under the project license. You will also be asked to [sign the CLA](https://github.com/cinnyapp/cla) upon submiting your pull request.
|
||||||
|
|
||||||
**NOTE: If you want to add new features, please discuss with maintainers before coding or opening a pull request.** This is to ensure that we are on same track and following our roadmap.
|
**NOTE: If you want to add new features, please discuss with maintainers before coding or opening a pull request.** This is to ensure that we are on same track and following our roadmap.
|
||||||
@@ -26,9 +28,9 @@ Bug reports and feature suggestions must use descriptive and concise titles and
|
|||||||
|
|
||||||
Example:
|
Example:
|
||||||
|
|
||||||
|Not ideal|Better|
|
| Not ideal | Better |
|
||||||
|---|----|
|
| ----------------------------------- | --------------------------------------------- |
|
||||||
|Fixed markAllAsRead in RoomTimeline|Fix read marker when paginating room timeline|
|
| Fixed markAllAsRead in RoomTimeline | Fix read marker when paginating room timeline |
|
||||||
|
|
||||||
It is not always possible to phrase every change in such a manner, but it is desired.
|
It is not always possible to phrase every change in such a manner, but it is desired.
|
||||||
|
|
||||||
@@ -39,6 +41,7 @@ Also, we use [ESLint](https://eslint.org/) for clean and stylistically consisten
|
|||||||
**For any query or design discussion, join our [Matrix room](https://matrix.to/#/#cinny:matrix.org).**
|
**For any query or design discussion, join our [Matrix room](https://matrix.to/#/#cinny:matrix.org).**
|
||||||
|
|
||||||
## Helpful links
|
## Helpful links
|
||||||
|
|
||||||
- [BEM methodology](http://getbem.com/introduction/)
|
- [BEM methodology](http://getbem.com/introduction/)
|
||||||
- [Atomic design](https://bradfrost.com/blog/post/atomic-web-design/)
|
- [Atomic design](https://bradfrost.com/blog/post/atomic-web-design/)
|
||||||
- [Matrix JavaScript SDK documentation](https://matrix-org.github.io/matrix-js-sdk/index.html)
|
- [Matrix JavaScript SDK documentation](https://matrix-org.github.io/matrix-js-sdk/index.html)
|
||||||
|
|||||||
@@ -0,0 +1,655 @@
|
|||||||
|
# HANDOFF — Forking & Self-Building Element Call ("Lotus Call")
|
||||||
|
|
||||||
|
> **Audience:** a fresh Claude/engineer session with **no prior context** on this
|
||||||
|
> project. Read this top-to-bottom before touching anything. This document is the
|
||||||
|
> single source of truth for the Element Call (EC) fork initiative.
|
||||||
|
>
|
||||||
|
> **Status:** **PHASE 0–2 IMPLEMENTED (build-verified, not yet live-tested)**
|
||||||
|
> (2026-06-30). The fork exists, builds, is published, and cinny consumes it
|
||||||
|
> (Phase 0/1). **All 7 Phase-2 EC features are implemented on the fork's `lotus`
|
||||||
|
> branch**, each additive + flag-gated, build+typecheck-clean, per-feature
|
||||||
|
> reviewed (+ a holistic multi-agent review), and pushed. **None are live-tested
|
||||||
|
> yet** — every one needs the `LOTUS_TESTING.md` §D sweep, and the **cinny host
|
||||||
|
> side must be wired** (set flags / send actions / handle call_state) — see §12.
|
||||||
|
> See **§9** Phase 0/1 results, **§10** cutover, **§11** Phase-2 seams, **§12**
|
||||||
|
> Phase-2 status + cinny integration checklist. Created 2026-06 from `LotusGuild/cinny`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. Phase 0 Results (verified 2026-06-29)
|
||||||
|
|
||||||
|
**Decisions taken with the user:** scope = Phase 0 recon; consumption model =
|
||||||
|
**private npm package** (§5 option 1). Recommended registry = **Gitea's built-in
|
||||||
|
npm registry** (`code.lotusguild.org`) — zero new infra.
|
||||||
|
|
||||||
|
### 9.1 Version → tag → commit mapping (LOCKED)
|
||||||
|
|
||||||
|
| Source | Value |
|
||||||
|
| :--------------------------------------------------- | :----------------------------------------- |
|
||||||
|
| cinny `package.json` pin | `@element-hq/element-call-embedded@0.20.1` |
|
||||||
|
| Bundle self-report (`VITE_APP_VERSION`/`appVersion`) | `embedded-v0.20.1` |
|
||||||
|
| npm registry `gitHead` for 0.20.1 | `2d74c48151d9edc01c65a22a91478aac81bf24d0` |
|
||||||
|
| GitHub tag `v0.20.1` → commit | `2d74c48…` ✅ **same commit** |
|
||||||
|
|
||||||
|
→ **Fork from upstream tag `v0.20.1` (commit `2d74c48`).** The embedded package
|
||||||
|
version equals the element-call release tag; repo `package.json` version is
|
||||||
|
`0.0.0` and the real version is stamped at publish time from the tag.
|
||||||
|
|
||||||
|
### 9.2 The shipped npm dist is a CLEAN upstream build
|
||||||
|
|
||||||
|
No `lotus`/`denoise`/`rnnoise` strings anywhere in
|
||||||
|
`node_modules/@element-hq/element-call-embedded/dist`. **All Lotus customization
|
||||||
|
(denoise shim) is injected at cinny build time, not baked into the package** — so
|
||||||
|
swapping the source does not disturb cinny's denoise injection layer. The
|
||||||
|
ringtone/reaction assets (`baduntss`, `cat`, `clap`, `call_declined`, …) are
|
||||||
|
upstream EC's own, not ours.
|
||||||
|
|
||||||
|
### 9.3 Build toolchain & mechanism
|
||||||
|
|
||||||
|
- **Node `24`** (`.node-version`), **pnpm `10.33.0`** (`packageManager` field,
|
||||||
|
via corepack).
|
||||||
|
- Build: **`pnpm run build:embedded`** = `vite build --config
|
||||||
|
vite-embedded.config.ts` with `NODE_OPTIONS=--max-old-space-size=16384`.
|
||||||
|
- Output dir is **repo-root `dist/`**; CI stages it into **`embedded/web/dist`**
|
||||||
|
(the `embedded/web/` dir holds the publish template: `package.json`, README,
|
||||||
|
both LICENSE files).
|
||||||
|
- Publish workflow upstream = `.github/workflows/publish-embedded-packages.yaml`:
|
||||||
|
builds → `npm version <tag> --no-git-tag-version` → `npm publish --provenance
|
||||||
|
--access public` to npmjs as `@element-hq/element-call-embedded`. (Also
|
||||||
|
Android/Maven + iOS/SwiftPM — irrelevant; we are web-only.)
|
||||||
|
|
||||||
|
### 9.4 Build reproduction — PARITY CONFIRMED
|
||||||
|
|
||||||
|
Cloned `element-call@v0.20.1` to `/root/code/element-call` (shallow), built with
|
||||||
|
isolated Node 24 / pnpm 10.33.0 (system Node 20 / cinny untouched). Result vs the
|
||||||
|
shipped npm dist:
|
||||||
|
|
||||||
|
- **137 of 147 files byte-identical** (same Vite content-hash): all CSS, fonts,
|
||||||
|
wasm, audio, JSON locale files, and `IndexedDBWorker`.
|
||||||
|
- **Only 5 JS chunks differ** (`index`, `pako.esm`, `polyfill-force`,
|
||||||
|
`rust-crypto`, `spa`) — **cause isolated to the version define**: our local
|
||||||
|
build baked `appVersion:\`dev\``(because`VITE_APP_VERSION`was unset) vs the
|
||||||
|
npm build's`appVersion:\`embedded-v0.20.1\``. `index.html` is identical modulo
|
||||||
|
the hashed asset filenames. **Benign** — our CI sets the version from the git
|
||||||
|
tag, so a tagged CI build will match.
|
||||||
|
|
||||||
|
### 9.5 Fork CI (drafted)
|
||||||
|
|
||||||
|
`.gitea/workflows/ci.yml` is staged in the clone (models cinny's
|
||||||
|
`.gitea/workflows/ci.yml` + upstream's publish flow). Linux-only (`ubuntu-latest`)
|
||||||
|
— the Windows worker is for cinny-desktop/Tauri, not the EC web bundle. Build job
|
||||||
|
on PR/push to `lotus`; publish job on `v*` tag → `@lotusguild/element-call-embedded`
|
||||||
|
to the Gitea npm registry (needs `secrets.GITEA_NPM_TOKEN`).
|
||||||
|
|
||||||
|
### 9.6 Phase 1 — DONE (2026-06-29)
|
||||||
|
|
||||||
|
1. ✅ **Fork repo live:** `code.lotusguild.org/LotusGuild/element-call` (public,
|
||||||
|
AGPL), default branch `lotus`, full history (7018 commits) + tag `v0.20.1`.
|
||||||
|
Branch `lotus` = `v0.20.1` + 2-file diff (CI workflow + embedded package
|
||||||
|
rename).
|
||||||
|
2. ✅ **Package published:** `@lotusguild/element-call-embedded@0.20.1` on the
|
||||||
|
Gitea npm registry (published manually from the version-faithful build while
|
||||||
|
the admin token was available). **Publicly readable** (unauth `npm install`
|
||||||
|
works → devs/CI need no token to consume; only publishing needs one).
|
||||||
|
3. ✅ **cinny wired & built clean** (Node 24): `.npmrc` scope line +
|
||||||
|
`package.json` dep + `vite.config.js` `viteStaticCopy` src. `npm install`
|
||||||
|
swapped the package (resolved from Gitea), `npm run build` succeeded,
|
||||||
|
`dist/public/element-call/` populated, bundle reports `appVersion:
|
||||||
|
embedded-v0.20.1`, **denoise shim injected + all denoise assets copied**
|
||||||
|
(injection layer unchanged). **These cinny edits are staged in the working
|
||||||
|
tree, NOT committed/pushed** — pushing triggers CI → desktop → deploy, so it's
|
||||||
|
gated on the §D live test (see §10).
|
||||||
|
|
||||||
|
### 9.8 Reproducibility note (important)
|
||||||
|
|
||||||
|
A from-source rebuild is **NOT byte-identical** to upstream's npm tarball.
|
||||||
|
137/147 files match exactly (CSS, fonts, wasm, audio, worker); the 5 JS chunks
|
||||||
|
(`index`, `pako.esm`, `polyfill-force`, `rust-crypto`, `spa`) differ because the
|
||||||
|
rolldown/oxc **minifier mangles export names differently** across build
|
||||||
|
environments (and the version-define is one input). This is normal and benign —
|
||||||
|
the code is functionally equivalent. **Do not chase byte-parity; the §D live call
|
||||||
|
test is the real parity gate.**
|
||||||
|
|
||||||
|
### 9.9 Remaining follow-ups (not blocking the cutover)
|
||||||
|
|
||||||
|
- **CI publishing:** `.gitea/workflows/ci.yml` publishes on a `v*` tag but needs
|
||||||
|
(a) a Gitea Actions runner for `LotusGuild/element-call`, and (b) a **durable**
|
||||||
|
`GITEA_NPM_TOKEN` repo secret with package read/write (the admin token used for
|
||||||
|
the manual publish is being deleted, so it was deliberately NOT baked in). Until
|
||||||
|
then, publishing is manual (`npm version <tag>` in `embedded/web` →
|
||||||
|
`npm publish`).
|
||||||
|
- Decide rebase cadence vs upstream (0.20.2 / 0.20.3 already out — see §9.1).
|
||||||
|
|
||||||
|
### 9.7 Ready-to-apply artifacts (staged 2026-06-29)
|
||||||
|
|
||||||
|
**Fork side — already committed** on branch `lotus` in `/root/code/element-call`
|
||||||
|
(remote `lotus` = `code.lotusguild.org/LotusGuild/element-call.git`, push deferred
|
||||||
|
until the repo exists). Minimal 2-file diff vs tag `v0.20.1`:
|
||||||
|
`.gitea/workflows/ci.yml` (new) + `embedded/web/package.json` (rename to
|
||||||
|
`@lotusguild/element-call-embedded`). Push with:
|
||||||
|
`git push -u lotus lotus && git push lotus v0.20.1` (and tag `v0.20.1` on our side
|
||||||
|
to trigger the first publish, or push our own `v0.20.1` tag).
|
||||||
|
|
||||||
|
**cinny side — NOT yet applied** (applying before the package is published breaks
|
||||||
|
`npm ci`). Exactly 3 edits + a lockfile regen:
|
||||||
|
|
||||||
|
1. `.npmrc` — append the scoped-registry line:
|
||||||
|
```
|
||||||
|
@lotusguild:registry=https://code.lotusguild.org/api/packages/LotusGuild/npm/
|
||||||
|
```
|
||||||
|
(CI/auth: `//code.lotusguild.org/api/packages/LotusGuild/npm/:_authToken=${GITEA_NPM_TOKEN}`
|
||||||
|
— inject via env in CI, do not commit a plaintext token.)
|
||||||
|
2. `package.json:104` —
|
||||||
|
`"@element-hq/element-call-embedded": "0.20.1"` →
|
||||||
|
`"@lotusguild/element-call-embedded": "0.20.1"`.
|
||||||
|
3. `vite.config.js:25` — `viteStaticCopy` src:
|
||||||
|
`node_modules/@element-hq/element-call-embedded/dist` →
|
||||||
|
`node_modules/@lotusguild/element-call-embedded/dist`.
|
||||||
|
**`stripBase: 4` stays unchanged** — `node_modules/@lotusguild/element-call-embedded/dist`
|
||||||
|
is still exactly 4 leading segments. (Update the comment's path reference too.)
|
||||||
|
4. `package-lock.json` — regenerated by `npm install`, not hand-edited (drops the
|
||||||
|
`registry.npmjs.org/@element-hq/...` resolved URL for the Gitea one).
|
||||||
|
|
||||||
|
The denoise injection (`lotusDenoise()` in `vite.config.js`) is **unchanged** — it
|
||||||
|
keys off `dist/public/element-call/index.html`, which our fork's bundle still
|
||||||
|
produces identically (verified: `index.html` byte-identical modulo asset hashes).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 0. TL;DR / The Goal
|
||||||
|
|
||||||
|
We embed **Element Call** (the Matrix group-VoIP/video app) inside Lotus Chat to
|
||||||
|
power voice/video channels. Today we consume Element's **pre-compiled npm
|
||||||
|
bundle** and can only steer it from the outside (a limited widget API + fragile
|
||||||
|
same-origin DOM hacks). Several in-call problems are **unfixable from outside**
|
||||||
|
because they live in EC's compiled JS.
|
||||||
|
|
||||||
|
**We want true ownership: fork `element-hq/element-call`, build it from source
|
||||||
|
ourselves, host our build, and replace the npm bundle with our fork.** Then
|
||||||
|
every in-call behavior becomes editable code.
|
||||||
|
|
||||||
|
**This requires standing up a brand-new repo and build pipeline for our EC fork.**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Why fork? (What we cannot fix today)
|
||||||
|
|
||||||
|
These came out of live testing and are documented in `LOTUS_BUGS.md` →
|
||||||
|
"Known Element Call iframe limitations":
|
||||||
|
|
||||||
|
| Issue | What's wrong | Why outside-fixes fail |
|
||||||
|
| :----------------------------------------------------- | :------------------------------------------------------------------------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||||
|
| **A6** — avatar decorations in-call | Our profile-decoration overlays don't appear on in-call video tiles | The video grid is rendered by EC's React app inside the iframe. We can only inject overlay DOM (fragile) — we can't make it a first-class part of the tile. |
|
||||||
|
| **A5** — focus camera / fullscreen during screenshare | Can't reliably spotlight a participant's camera while someone screenshares | EC's **layout logic** (screenshare priority, spotlight) is compiled JS we don't control. We currently DOM-click tiles as a hack. |
|
||||||
|
| **A7** — mic dead after EC's "Reconnect" | After EC's own mid-call reconnect, the local mic isn't re-published | EC's reconnect/track-republish path is internal. (Partly entangled with our denoise shim — see §6.) |
|
||||||
|
| Native theming | EC's UI doesn't match Lotus design; we inject CSS hacks | Real theming needs source-level component/token changes. |
|
||||||
|
| Decorations, custom controls, custom layouts, branding | all blocked | all require source access |
|
||||||
|
|
||||||
|
**Bottom line:** the iframe is **same-origin** (we self-host it), so we can read
|
||||||
|
and even write its DOM — but we **do not own its source**, so we can't change its
|
||||||
|
**behavior/logic**, only poke at its rendered output. Forking removes that wall.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. How EC is integrated TODAY (the current architecture)
|
||||||
|
|
||||||
|
Understand this fully before changing it — the fork must slot into the same
|
||||||
|
integration seams.
|
||||||
|
|
||||||
|
### 2.1 Where the EC bundle comes from
|
||||||
|
|
||||||
|
- npm package: **`@element-hq/element-call-embedded`**, pinned to **`0.20.1`** in
|
||||||
|
`cinny/package.json` (line ~104).
|
||||||
|
- It ships a **pre-built `dist/`**. At cinny build time,
|
||||||
|
`vite-plugin-static-copy` copies that `dist/` flat into
|
||||||
|
**`public/element-call/`** (see `cinny/vite.config.js`, the `copyFiles`
|
||||||
|
target with `rename: { stripBase: 4 }` — note the stripBase gotcha documented
|
||||||
|
there; getting this wrong 404s the widget).
|
||||||
|
- It is **NOT committed** to git (`git ls-files public/element-call` → 0). It's a
|
||||||
|
build artifact materialized from `node_modules`.
|
||||||
|
|
||||||
|
### 2.2 How EC is loaded & controlled
|
||||||
|
|
||||||
|
- The widget iframe `src` is **same-origin**:
|
||||||
|
`${BASE_URL}/public/element-call/index.html?<params>` (see
|
||||||
|
`cinny/src/app/plugins/call/CallEmbed.ts`, `getWidget()` /
|
||||||
|
`getIframe()`). Sandbox: `allow-forms allow-scripts allow-same-origin
|
||||||
|
allow-popups allow-modals allow-downloads`; `allow="microphone; camera;
|
||||||
|
display-capture; autoplay; clipboard-write;"`.
|
||||||
|
- **Control surface #1 — the official widget API** (`matrix-widget-api`):
|
||||||
|
`ClientWidgetApi` + a custom `CallWidgetDriver`. This is the robust,
|
||||||
|
version-stable channel (theme change, hangup, capabilities, timeline events).
|
||||||
|
Files: `plugins/call/CallEmbed.ts`, `plugins/call/CallWidgetDriver.ts`,
|
||||||
|
`plugins/call/utils.ts` (capabilities), `plugins/call/CallControl.ts`.
|
||||||
|
- **Control surface #2 — same-origin DOM poking** (fragile, version-coupled):
|
||||||
|
reading `iframe.contentDocument` to detect speakers/mute state and
|
||||||
|
`.click()`-ing tiles to focus a camera. Files:
|
||||||
|
`hooks/useCallSpeakers.ts` (reads `[data-muted]`, `[data-video-fit]`),
|
||||||
|
`plugins/call/CallControl.ts` (`focusCameraParticipant` — tile selectors).
|
||||||
|
**These selectors break on every EC version bump.** A fork lets us replace
|
||||||
|
these hacks with real APIs/props.
|
||||||
|
- **Control surface #3 — URL params + build-time injection** for our denoise
|
||||||
|
shim (see §6).
|
||||||
|
|
||||||
|
### 2.3 Full file inventory (everything that touches EC in cinny)
|
||||||
|
|
||||||
|
Plugin / core:
|
||||||
|
|
||||||
|
- `src/app/plugins/call/CallEmbed.ts` — iframe creation, widget API wiring, theme sync, hangup, load watchdog/self-heal, denoise URL params.
|
||||||
|
- `src/app/plugins/call/CallControl.ts` — control state + **DOM-poking** (`focusCameraParticipant`, spotlight).
|
||||||
|
- `src/app/plugins/call/CallControl.tsx` _(call-status variant)_ and `features/call-status/CallControl.tsx`.
|
||||||
|
- `src/app/plugins/call/CallWidgetDriver.ts` — widget driver (capabilities, event relay).
|
||||||
|
- `src/app/plugins/call/utils.ts` — widget capabilities set.
|
||||||
|
- `src/app/plugins/call/hooks.ts`, `index.ts` — plugin exports/hooks.
|
||||||
|
- `src/app/state/callEmbed.ts` — jotai atoms for the active embed.
|
||||||
|
|
||||||
|
React / UI:
|
||||||
|
|
||||||
|
- `src/app/components/CallEmbedProvider.tsx` — the big one: incoming-call ring/banner, RTCNotification + **RTCDecline** listeners, PiP, mute badges, fullscreen, ringtones.
|
||||||
|
- `src/app/features/call/CallView.tsx` — prescreen lobby vs joined (the iframe placement target), load-error recovery UI.
|
||||||
|
- `src/app/features/call/CallControls.tsx` — in-call control bar (mic/cam/deafen/screenshare/fullscreen/more/PiP).
|
||||||
|
- `src/app/features/call/CallMemberCard.tsx` — **lobby** participant roster (this is where `AvatarDecoration` works today; in-call grid is EC's).
|
||||||
|
- `src/app/features/call/PrescreenControls.tsx` — join controls.
|
||||||
|
- `src/app/features/call-status/*` — `CallStatus.tsx`, `MemberGlance.tsx` (the "Focus camera" menu lives here), `LiveChip.tsx`.
|
||||||
|
- `src/app/features/room-nav/RoomNavItem.tsx`, `features/room/Room.tsx`, `features/room/RoomViewHeader.tsx`, `pages/client/space/Space.tsx`, `pages/CallStatusRenderer.tsx`, `pages/Router.tsx` — call entry points / status surfacing.
|
||||||
|
|
||||||
|
Hooks:
|
||||||
|
|
||||||
|
- `src/app/hooks/useCallEmbed.ts`, `useCall.ts`, `useCallSpeakers.ts` (DOM-poking), `useCallJoinLeaveSounds.ts`, `useAfkAutoMute.ts`.
|
||||||
|
|
||||||
|
Build:
|
||||||
|
|
||||||
|
- `cinny/vite.config.js` — `copyFiles` (EC dist copy) + `lotusDenoise()` plugin (denoise asset copy + index.html shim injection, in `closeBundle`).
|
||||||
|
|
||||||
|
Utils:
|
||||||
|
|
||||||
|
- `src/app/utils/ringtones.ts`, `utils/denoisePipeline.ts`, `utils/lotusDenoiseUtils.ts`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Hosting / infra context (the OTHER repo)
|
||||||
|
|
||||||
|
There are **two repos**:
|
||||||
|
|
||||||
|
1. **`LotusGuild/cinny`** (`/root/code/cinny`) — this Lotus Chat fork. Consumes EC.
|
||||||
|
2. **`LotusGuild/matrix`** (`/root/code/matrix`) — the **infra/homeserver** repo.
|
||||||
|
Subdirs: `livekit/` (the SFU EC talks to), `deploy/`, `draupnir/`,
|
||||||
|
`hookshot/`, `landing/`, `matrixbot/`, `systemd/`. Gitea remote
|
||||||
|
`code.lotusguild.org/LotusGuild/matrix`, branch `main`.
|
||||||
|
|
||||||
|
EC needs a **LiveKit SFU** + the **livekit-jwt-service**; those live in
|
||||||
|
`matrix/livekit/`. A self-hosted EC build must be configured to point at our
|
||||||
|
homeserver (`matrix.lotusguild.org` / synapse) and our LiveKit. EC's runtime
|
||||||
|
`config.json` (homeserver, livekit URL, feature flags) is part of what we'll own
|
||||||
|
once we build it ourselves.
|
||||||
|
|
||||||
|
Deployment today: `chat.lotusguild.org` (the cinny web build, which embeds EC at
|
||||||
|
`/public/element-call/`). cinny-desktop (`LotusGuild/cinny-desktop`, a Tauri
|
||||||
|
wrapper, bumped by cinny CI) embeds the same.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. The plan (proposed — confirm with the user before executing)
|
||||||
|
|
||||||
|
### Decision: **YES, create a new repo.** `LotusGuild/element-call`
|
||||||
|
|
||||||
|
Rationale: EC is a large standalone app (React + LiveKit client SDK + matrixRTC +
|
||||||
|
its own Vite build + heavy deps). Keep it out of cinny so cinny's build stays
|
||||||
|
clean — cinny keeps consuming a **built EC `dist/`**, exactly as today, just
|
||||||
|
sourced from **our fork** instead of npm.
|
||||||
|
|
||||||
|
### Phase 0 — Recon (no code)
|
||||||
|
|
||||||
|
- Fork `github.com/element-hq/element-call` → `LotusGuild/element-call` on Gitea.
|
||||||
|
- Pin to the upstream tag matching **0.20.1** (`element-call-embedded` 0.20.1's
|
||||||
|
corresponding `element-call` release) so behavior matches what's shipping now.
|
||||||
|
Verify the embedded-package version ↔ element-call repo tag mapping.
|
||||||
|
- Read EC's own build docs: it builds the "embedded" widget bundle (the thing
|
||||||
|
currently published as `@element-hq/element-call-embedded`). Reproduce that
|
||||||
|
build locally and confirm the output matches `public/element-call/` today.
|
||||||
|
- **License:** element-call is **AGPL-3.0**, same as Lotus Chat — compatible.
|
||||||
|
Our fork must remain AGPL and publish source.
|
||||||
|
|
||||||
|
### Phase 1 — Reproduce current behavior from our fork (parity, no features)
|
||||||
|
|
||||||
|
- Build our fork's embedded bundle; wire cinny to consume it instead of the npm
|
||||||
|
package (see §5 for the consumption options). Smoke-test: a call works exactly
|
||||||
|
as today (web + desktop), denoise shim still injects, widget API + theme still
|
||||||
|
work. **No behavior change yet** — this de-risks the swap.
|
||||||
|
|
||||||
|
### Phase 2 — Replace the outside hacks with source-level features
|
||||||
|
|
||||||
|
Tackle the §1 issues in EC's source:
|
||||||
|
|
||||||
|
- **A6:** render avatar decorations as part of the video-tile component
|
||||||
|
(read decoration data we pass in via widget data / URL param / a small bridge).
|
||||||
|
- **A5:** fix focus/spotlight + screenshare-coexistence in EC's layout code;
|
||||||
|
expose a clean widget action so cinny can trigger it (kill the DOM `.click()`).
|
||||||
|
- **A7:** fix mic re-publish on reconnect; reconcile with our denoise shim (§6) —
|
||||||
|
ideally move denoise INTO the fork as a real audio-processing step instead of a
|
||||||
|
`getUserMedia` monkeypatch.
|
||||||
|
- Native Lotus theming/branding at the source (kill the injected-CSS hacks).
|
||||||
|
- Then retire the DOM-poking in `useCallSpeakers.ts` / `CallControl.ts` in favor
|
||||||
|
of real widget messages.
|
||||||
|
|
||||||
|
### Phase 3 — Maintenance posture
|
||||||
|
|
||||||
|
- Decide rebase cadence vs. upstream element-call releases. Keep customizations
|
||||||
|
isolated (feature flags / minimal-diff patches) to ease rebasing.
|
||||||
|
- CI in the new repo builds + publishes the embedded dist as a versioned
|
||||||
|
artifact; cinny CI consumes a pinned version.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. How cinny should consume the fork (pick one — decide with user)
|
||||||
|
|
||||||
|
1. **Private npm package** (mirror the current model): our fork's CI publishes
|
||||||
|
`@lotusguild/element-call-embedded` to a registry; cinny depends on it and
|
||||||
|
`viteStaticCopy` keeps working almost unchanged. _Cleanest swap; needs a
|
||||||
|
registry._
|
||||||
|
2. **Git submodule + build in cinny CI:** add the fork as a submodule, build it
|
||||||
|
during cinny's build, copy its `dist/` to `public/element-call/`. _No
|
||||||
|
registry; heavier cinny CI._
|
||||||
|
3. **CI artifact copy:** fork CI uploads a `dist` tarball; cinny CI downloads a
|
||||||
|
pinned version at build. _Decoupled; needs artifact plumbing._
|
||||||
|
|
||||||
|
**Recommendation: Option 1** — it changes the least in cinny (just swap the
|
||||||
|
package name in `package.json` + the `viteStaticCopy` src path) and preserves the
|
||||||
|
clean cinny/EC separation.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. The denoise shim — critical interaction (don't break this)
|
||||||
|
|
||||||
|
Lotus ships ML noise suppression by **injecting a same-origin pre-init shim into
|
||||||
|
EC's `index.html` at build time** (cinny `vite.config.js` → `lotusDenoise()`,
|
||||||
|
`closeBundle`). The shim monkeypatches `getUserMedia` **before EC captures the
|
||||||
|
mic** and routes audio through RNNoise/Speex/DTLN AudioWorklets, then EC/LiveKit
|
||||||
|
publishes the processed track. It's activated via URL params
|
||||||
|
(`lotusDenoise=ml&lotusModel=…&lotusGate=…`) set in `CallEmbed.ts`.
|
||||||
|
|
||||||
|
- Assets copied to `public/element-call/denoise/` at build (sapphi RNNoise/Speex/
|
||||||
|
gate worklets + `@workadventure/noise-suppression` DTLN tree).
|
||||||
|
- Related: `utils/denoisePipeline.ts`, `utils/lotusDenoiseUtils.ts`,
|
||||||
|
`settings/general/DenoiseTester.tsx`, `VoiceMessageRecorder.tsx`.
|
||||||
|
- **Known issues:** denoise quality is still poor (tracked separately); and the
|
||||||
|
mic-after-reconnect bug (A7) is suspected to involve the shim's getUserMedia
|
||||||
|
patch handing back a stale processed stream when EC re-acquires the mic.
|
||||||
|
|
||||||
|
**Once we own the fork, the right move is to make denoise a first-class
|
||||||
|
audio-processing stage inside EC** (not an index.html monkeypatch) — more robust,
|
||||||
|
survives reconnects, and removes the build-time injection hack. Until then, the
|
||||||
|
fork's `index.html` must remain injectable the same way, or the shim must be
|
||||||
|
re-homed into the fork.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Doc-accuracy notes / corrections for the new session
|
||||||
|
|
||||||
|
- `LOTUS_TODO.md` (~line 533) calls EC a **"cross-origin iframe"** — **outdated.**
|
||||||
|
EC is **same-origin** today (self-hosted under our domain;
|
||||||
|
`iframe.sandbox` includes `allow-same-origin`; we read `contentDocument`), and
|
||||||
|
**as of 2026-06-29 we own the fork's source** (`@lotusguild/element-call-embedded`).
|
||||||
|
The _practical_ point it made still holds _until we ship the audio-inject API_:
|
||||||
|
**LiveKit's `LocalAudioTrack` lives in EC's module scope**, not on `window`, so
|
||||||
|
cinny can't reach it even same-origin — which is why the in-call soundboard had
|
||||||
|
to be local-playback-only. **The fork removes this wall:** EC can expose a real
|
||||||
|
`io.lotus.inject_audio` widget action (Phase 2) that mixes into the published
|
||||||
|
track from inside its own module scope.
|
||||||
|
- `LOTUS_FEATURES.md` documents the EC upgrade history (0.16.3 → 0.19.4 →
|
||||||
|
0.20.1), the dark-mode CSS injection, and AFK auto-mute — all relevant prior
|
||||||
|
art for what the fork must preserve.
|
||||||
|
- `LOTUS_TESTING.md` §D is the **EC regression sweep** to re-run after the fork
|
||||||
|
swap (Phase 1 parity check).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. First actions for the new session
|
||||||
|
|
||||||
|
1. Read this file, then skim §2.3's files in `cinny` to internalize the seams.
|
||||||
|
2. Confirm with the user: new repo name, consumption model (§5), rebase cadence.
|
||||||
|
3. Phase 0: fork element-call, map 0.20.1 ↔ element-call tag, reproduce the
|
||||||
|
embedded build locally, diff against `public/element-call/`.
|
||||||
|
4. Phase 1: wire cinny to the fork, run `LOTUS_TESTING.md` §D parity sweep.
|
||||||
|
5. Only then start Phase 2 features (A5/A6/A7, theming, denoise-in-source).
|
||||||
|
|
||||||
|
**Cross-references:** `LOTUS_BUGS.md` (EC limitations + verify queue),
|
||||||
|
`LOTUS_TODO.md` (denoise/soundboard constraints), `LOTUS_FEATURES.md` (EC history),
|
||||||
|
`LOTUS_TESTING.md` §D (regression sweep). Infra: `/root/code/matrix` (`livekit/`,
|
||||||
|
`deploy/`).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. Live cutover — the remaining steps (Phase 1 finish)
|
||||||
|
|
||||||
|
The fork is published and cinny builds against it locally (§9.6). What's left to
|
||||||
|
go live:
|
||||||
|
|
||||||
|
1. **Run `LOTUS_TESTING.md` §D** against a local cinny build (`npm run build` is
|
||||||
|
already proven; serve `dist/` or `npm run dev`). Verify a real call: join,
|
||||||
|
mic/cam, screenshare, theme sync, denoise on, widget hangup — web first.
|
||||||
|
2. **Commit the cinny edits** (currently staged, uncommitted in the working tree):
|
||||||
|
`.npmrc`, `package.json`, `package-lock.json`, `vite.config.js`. Suggested
|
||||||
|
message: `chore(call): consume self-built @lotusguild/element-call-embedded`.
|
||||||
|
3. **Push to `lotus`** → cinny CI builds, then `trigger-desktop` bumps
|
||||||
|
cinny-desktop → Tauri release. Re-run §D on **cinny-desktop** (the path where
|
||||||
|
the old `stripBase` bug bit — verify the widget loads, not a 404).
|
||||||
|
4. Only then start **Phase 2** (A5/A6/A7, theming, denoise-in-source).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 11. Phase 2 — implementation seams (mapped 2026-06-29)
|
||||||
|
|
||||||
|
The exact integration points for each Phase 2 item, found by reading the EC fork
|
||||||
|
|
||||||
|
- cinny source. **All of these are media-path / in-call features that cannot be
|
||||||
|
functionally verified without a live Matrix + LiveKit call** — implement each as
|
||||||
|
a minimal, **feature-flagged, additive** diff (no behavior change unless cinny
|
||||||
|
opts in), build-verify the fork (`pnpm build:embedded`, ~15s) AND cinny
|
||||||
|
(`npm run build`), then gate shipping on `LOTUS_TESTING.md` §D.
|
||||||
|
|
||||||
|
**Shared widget channel (the backbone for #2/#3/#4/#7):**
|
||||||
|
|
||||||
|
- EC→cinny: `widget.api.transport.send("io.lotus.<x>", data)` (see
|
||||||
|
`element-call/src/widget.ts`).
|
||||||
|
- cinny→EC actions: add the action name to the `lazyActions` allow-list in
|
||||||
|
`widget.ts` (the array at ~L101) and handle it in EC; cinny sends via
|
||||||
|
`this.call.transport.send(...)`.
|
||||||
|
- cinny receives EC→cinny actions via the existing `listenAction(type, cb)`
|
||||||
|
helper in `plugins/call/CallEmbed.ts:626` (auto-replies `{}` so the transport
|
||||||
|
doesn't time out — same pattern as `io.element.device_mute`).
|
||||||
|
|
||||||
|
**#2 mute/speaker events** — Source: subscribe to `vm.userMedia$`
|
||||||
|
(`CallViewModel`), per member `speaking$` + `audioEnabled$`
|
||||||
|
(`state/media/UserMediaViewModel.ts:47-48`); aggregate and
|
||||||
|
`transport.send("io.lotus.call_state", {participants:[{id,speaking,audioEnabled}]})`.
|
||||||
|
Mount in `room/InCallView.tsx` via `useEffect` guarded by `widget !== null`.
|
||||||
|
cinny: `listenAction("io.lotus.call_state")` in `CallEmbed.ts`, feed
|
||||||
|
`hooks/useCallSpeakers.ts` → delete its `contentDocument` `[data-muted]` /
|
||||||
|
`[data-video-fit]` scrape. _Additive, low risk._
|
||||||
|
|
||||||
|
**#4 spotlight/focus** — EC: add `io.lotus.focus_participant` to the `lazyActions`
|
||||||
|
list (`widget.ts`), drive `vm`'s spotlight (`spotlightSpeaker$` /
|
||||||
|
`spotlight$` in `CallViewModel.ts:898/1001`) to pin a given identity, coexisting
|
||||||
|
with `hasRemoteScreenShares$` (L1008). cinny: replace
|
||||||
|
`CallControl.ts` `focusCameraParticipant` `.click()` walk with
|
||||||
|
`transport.send("io.lotus.focus_participant", {userId})`. _Additive, low risk._
|
||||||
|
|
||||||
|
**#3 audio-inject** — EC: add `io.lotus.inject_audio` action; mix an
|
||||||
|
`AudioBufferSourceNode` into the published mic track. The local publish path is
|
||||||
|
`state/CallViewModel/localMember/Publisher.ts` + `LocalMember.ts` (LiveKit
|
||||||
|
`localParticipant`); create a `MediaStreamAudioDestinationNode`, mix mic + clip,
|
||||||
|
`replaceTrack`. cinny soundboard calls the action instead of local-only playback.
|
||||||
|
_Medium; touches publish path → live-test carefully._
|
||||||
|
|
||||||
|
**#1 denoise-in-source** — replace the cinny `lotusDenoise()` `getUserMedia`
|
||||||
|
monkeypatch with a real processing stage in EC's mic capture
|
||||||
|
(`Publisher.ts`/`LocalMember.ts`; note EC has a `TrackProcessorContext` +
|
||||||
|
`BlurBackgroundTransformer` precedent in `livekit/`). EC re-runs it on every
|
||||||
|
(re)publish → fixes A7. Remove `vite.config.js` `lotusDenoise()` + URL params in
|
||||||
|
`CallEmbed.ts`; move `denoise/` assets into the fork. _Highest value, highest
|
||||||
|
risk — most live testing._
|
||||||
|
|
||||||
|
**#5 theming** — add a Lotus/TDS theme in EC's theme system (`src/useTheme.ts` +
|
||||||
|
EC theme tokens / CSS); driven by the existing `setTheme()` channel cinny already
|
||||||
|
calls (`CallEmbed.ts:277`). Bake transparent background. Delete cinny's
|
||||||
|
`applyStyles()` injection + `background:none !important`. _Medium._
|
||||||
|
|
||||||
|
**#6 in-call decorations** — render the decoration APNG in EC's tile component
|
||||||
|
(`tile/GridTile.tsx`); pass slugs via widget member data. cinny already has the
|
||||||
|
decoration data + `AvatarDecoration` (lobby `CallMemberCard.tsx`). _Medium-Large._
|
||||||
|
|
||||||
|
**#7 quality controls** — set audio `maxBitrate` via
|
||||||
|
`RTCRtpSender.setParameters` and screenshare `getDisplayMedia` constraints in
|
||||||
|
EC's publish path (`Publisher.ts`); configurable via `config.json` / a widget
|
||||||
|
message. Keep the server `voice-limit-guard` as enforcement. _Medium._
|
||||||
|
|
||||||
|
**Rollback:** revert the 4 cinny files (restores `@element-hq/...@0.20.1` from
|
||||||
|
npmjs). The fork repo/package can stay; nothing else depends on it until pushed.
|
||||||
|
|
||||||
|
### Local repro/build environment (this session, 2026-06-29)
|
||||||
|
|
||||||
|
- Upstream cloned + our `lotus` branch at `/root/code/element-call` (remote
|
||||||
|
`lotus` → Gitea; origin → github upstream, now un-shallowed/full history).
|
||||||
|
- Isolated **Node 24.18.0** lives in the session scratchpad (system Node is 20);
|
||||||
|
cinny's `.node-version` is `24.13.1`, so use Node 24 to build cinny too.
|
||||||
|
- Build the embedded bundle: in `/root/code/element-call`, with Node 24 + pnpm
|
||||||
|
10.33.0 on PATH, `VITE_APP_VERSION=embedded-v0.20.1 pnpm run build:embedded`
|
||||||
|
→ output in `dist/`; stage to `embedded/web/dist` before publishing.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 12. Phase 2 — IMPLEMENTED on the fork (2026-06-30)
|
||||||
|
|
||||||
|
All 7 EC features are on the `lotus` branch of `LotusGuild/element-call`, each
|
||||||
|
**additive + feature-flagged** (a vanilla call with no `lotus*` params / no Lotus
|
||||||
|
actions behaves exactly like upstream), build + `tsc` clean, per-feature reviewed
|
||||||
|
(fixes applied) and holistically reviewed. **Not yet live-tested** — all need the
|
||||||
|
`LOTUS_TESTING.md` §D sweep.
|
||||||
|
|
||||||
|
Fork modules live under `element-call/src/lotus/*`; mounts are `useEffect`s in
|
||||||
|
`src/room/InCallView.tsx`. Custom widget actions are in `src/lotus/lotusActions.ts`
|
||||||
|
(toWidget ones allow-listed in `src/widget.ts`).
|
||||||
|
|
||||||
|
| # | Feature | Enable via | EC module |
|
||||||
|
| :-- | :------------------------------------------------------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :------------------------------------------------ | ---------------------------------------------------- |
|
||||||
|
| 2 | Speaker/mute/camera state → host | URL `lotusCallState=1` | `lotusCallState.ts` (sends `io.lotus.call_state`) |
|
||||||
|
| 4 | Focus/spotlight a participant (works during screenshare) | action `io.lotus.focus_participant {userId | null}` | `lotusFocus.ts` + `CallViewModel` spotlight override |
|
||||||
|
| 3 | Soundboard audio-inject (heard by peers) | URL `lotusAudioInject=1` + action `io.lotus.inject_audio {url,volume?}` | `lotusAudioInject.ts` |
|
||||||
|
| 7 | Audio/screenshare quality caps | action `io.lotus.set_quality {audioMaxBitrate?,screenshareMaxBitrate?,screenshareMaxFramerate?}` | `lotusQuality.ts` |
|
||||||
|
| 5 | Transparent bg + Lotus theme | URL `lotusTransparent=1` / `lotusTheme=1` | `useTheme.ts` + `index.css` |
|
||||||
|
| 6 | In-call avatar decorations | action `io.lotus.decorations {decorations:{userId:url}}` | `lotusDecorations.ts` + `MediaView.tsx` |
|
||||||
|
| 1 | ML denoise in-source (fixes A7) | URL **`lotusDenoiseSource=1`** (+`lotusModel`,`lotusGate`,`lotusGateThreshold`,`lotusDenoiseBase`) — deliberately NOT the existing `lotusDenoise=ml` (that drives the host shim; reusing it would double-process) | `lotusDenoise.ts` + `lotusDenoiseProcessor.ts` |
|
||||||
|
|
||||||
|
**Security hardening applied** (holistic audit): `lotusDenoiseBase` forced
|
||||||
|
same-origin before `audioWorklet.addModule` (was an arbitrary-code-load vector
|
||||||
|
via a crafted link); audio-inject gated behind `lotusAudioInject=1`; decoration
|
||||||
|
roster capped. Only `https`/`blob` URLs accepted for inject/decoration assets.
|
||||||
|
|
||||||
|
### 12.1 cinny host integration checklist (REQUIRED to light these up)
|
||||||
|
|
||||||
|
The EC side is additive and dormant until cinny opts in. Host work needed (in
|
||||||
|
`src/app/plugins/call/CallEmbed.ts` unless noted):
|
||||||
|
|
||||||
|
> ⚠️ **CRITICAL TIMING (protocol audit F1):** only send `io.lotus.*` **toWidget**
|
||||||
|
> actions (#3 focus, #6 decorations, #7 quality, audio-inject) **after** the call
|
||||||
|
> is joined (`CallEmbed.onCallJoined` / `this.joined`). Those actions are
|
||||||
|
> allow-listed at EC app-init (so `preventDefault` suppresses the auto-error)
|
||||||
|
> but their handlers only mount with `InCallView` (post-join). Sending earlier
|
||||||
|
> leaves the host's `transport.send` pending until the **10s timeout**. Queue and
|
||||||
|
> flush on join, or no-op before join.
|
||||||
|
>
|
||||||
|
> Also: **F3** — the fork implements only `rnnoise`/`speex`; cinny's `dtln`/
|
||||||
|
> `deepfilternet` selections silently fall back to rnnoise (now logged). Restrict
|
||||||
|
> the embedded-call model picker to rnnoise/speex, or implement the others in
|
||||||
|
> `lotusDenoiseProcessor.ts`. **F4** — cinny sends `lotusNativeNS`, which the
|
||||||
|
> fork ignores; drop it or wire it in. **F7** — no widget _capability_ changes
|
||||||
|
> needed; custom actions bypass capability checks.
|
||||||
|
|
||||||
|
1. **Set the URL flags** on the widget iframe params (the `URLSearchParams` in
|
||||||
|
`CallEmbed`): `lotusCallState=1`, `lotusTransparent=1`/`lotusTheme=1`,
|
||||||
|
`lotusAudioInject=1` as desired. (Denoise already sets `lotusDenoise=ml` etc.)
|
||||||
|
2. **Ack `io.lotus.call_state`**: add `listenAction('io.lotus.call_state', …)` —
|
||||||
|
without a reply the fork's sends time out every 250ms. Feed the payload into
|
||||||
|
`useCallSpeakers` and RETIRE its `contentDocument` DOM scrape.
|
||||||
|
3. **Send actions** via `this.call.transport.send(...)`:
|
||||||
|
`io.lotus.focus_participant` (replace `CallControl.focusCameraParticipant`’s
|
||||||
|
`.click()`), `io.lotus.inject_audio` (from the soundboard), `io.lotus.set_quality`
|
||||||
|
(from quality settings), `io.lotus.decorations` (push the MSC4133 decoration
|
||||||
|
map; resolve mxc→https first).
|
||||||
|
4. **#1 denoise cutover**: once verified, STOP injecting the `lotusDenoise()`
|
||||||
|
shim in `cinny/vite.config.js` and remove the `index.html` injection — the
|
||||||
|
fork now does denoise in-source. Keep shipping the `denoise/` assets (the
|
||||||
|
fork loads `./denoise/…` at runtime) until those move into the fork build.
|
||||||
|
5. Re-run `LOTUS_TESTING.md` §D for each feature; only then ship.
|
||||||
|
|
||||||
|
### 12.2 Holistic multi-agent review — outstanding follow-ups (non-blocking)
|
||||||
|
|
||||||
|
Four aspect-agents reviewed the whole fork. Criticals were fixed in-branch (the
|
||||||
|
denoise restart-silence/A7 bug; the `lotusDenoiseBase` code-load vector;
|
||||||
|
audio-inject opt-in gate; #6 rendering in the wrong component; #7 simulcast cap).
|
||||||
|
Remaining, deliberately deferred:
|
||||||
|
|
||||||
|
- **Denoise H2 (double-processing):** if cinny is set to `lotusDenoise=ml` while
|
||||||
|
ALSO still injecting its build-time `getUserMedia` shim, audio is denoised
|
||||||
|
twice. The #1 cutover MUST remove the cinny-side injection (it currently has
|
||||||
|
none injected into the iframe — keep it that way). Hard requirement, not code.
|
||||||
|
- **Denoise M1 (perf):** in-source uses non-SIMD `rnnoise.wasm`; the reference
|
||||||
|
preferred SIMD with detection. Perf-only; add SIMD detection later.
|
||||||
|
- **dtln/deepfilternet (F3): RESOLVED** — all four models
|
||||||
|
(rnnoise/speex/dtln/deepfilternet) are now implemented in
|
||||||
|
`lotusDenoiseProcessor.ts` (faithful port of cinny's `build/lotus-denoise.js`
|
||||||
|
pipeline). This also fixed a real bug (the gate worklet name was `noiseGate`;
|
||||||
|
correct is the hyphenated `noise-gate`) and added per-model sample rates
|
||||||
|
(DTLN 16 kHz, others 48 kHz), context `resume()`, and SIMD wasm selection.
|
||||||
|
Still needs live §D testing per model, and depends on cinny shipping the
|
||||||
|
DTLN (`denoise/workadventure/`) + DeepFilterNet (`denoise/deepfilternet/`)
|
||||||
|
asset trees (it already does).
|
||||||
|
- **Rebase-fragility (build agent MED):** the `CallViewModel` spotlight override
|
||||||
|
edits hot upstream lines (renamed `spotlightSpeaker$`→`autoSpotlightSpeaker$`).
|
||||||
|
For cheaper future rebases, refactor it into a `src/lotus/lotusSpotlight.ts`
|
||||||
|
wrapper that takes the upstream stream and returns the overridden one, leaving
|
||||||
|
upstream's definition byte-identical (a single import + two token swaps).
|
||||||
|
- **Denoise asset coupling (build agent HIGH):** the fork loads `./denoise/*`
|
||||||
|
shipped by cinny, not by the fork build (documented in the processor). Add an
|
||||||
|
integration smoke-check that `GET …/element-call/denoise/rnnoise.wasm` == 200,
|
||||||
|
and pin the `@sapphi-red/web-noise-suppressor` version both repos expect.
|
||||||
|
- **Unconditional effect registration (build agent LOW):** focus/audio-inject/
|
||||||
|
quality/decorations register widget handlers on every embedded call (true
|
||||||
|
no-ops for a non-Lotus host). Intentional; gate behind a coarse `lotus=1` flag
|
||||||
|
if strict zero-footprint is desired.
|
||||||
|
- **Privacy (security agent):** decoration/inject URLs accept any `https`; ideally
|
||||||
|
restrict to the homeserver media origin host-side. Call-state exposes
|
||||||
|
userId/deviceId/speaking to the (trusted, same-origin) host — documented.
|
||||||
|
|
||||||
|
**Nothing here blocks the §D live test — but every feature still needs it.**
|
||||||
|
|
||||||
|
### 12.3 Safe rollout when prod is the only test environment
|
||||||
|
|
||||||
|
Every Phase-2 feature is now **dormant by default** — with the flags cinny sets
|
||||||
|
today, the fork behaves identically to the parity build (`#1` was decoupled onto
|
||||||
|
`lotusDenoiseSource=1` so it no longer collides with the host's `lotusDenoise=ml`
|
||||||
|
shim). This enables a low-risk incremental rollout even without a staging env:
|
||||||
|
|
||||||
|
1. **Ship dormant first.** Publish the `lotus` branch (e.g. `0.20.1-lotus.1`),
|
||||||
|
bump cinny's pin, deploy. With no Lotus flags set / no Lotus actions sent,
|
||||||
|
this is upstream-equivalent (only inert, holistically-reviewed code runs).
|
||||||
|
"Testing" here = confirm a normal call still works.
|
||||||
|
2. **Enable ONE feature at a time**, each independently revertable:
|
||||||
|
- URL-flag features (#2 `lotusCallState`, #5 `lotusTransparent`/`lotusTheme`,
|
||||||
|
#1 `lotusDenoiseSource`): add the flag in `CallEmbed.getWidget`, deploy,
|
||||||
|
test that one feature, roll back just that flag if needed.
|
||||||
|
- Action features (#3,#4,#6,#7): wire the host send + (for #2) the
|
||||||
|
`listenAction` ack, gated on join (§12.1 F1).
|
||||||
|
3. **#1 denoise cutover is a coordinated 2-step** (do together): set
|
||||||
|
`lotusDenoiseSource=1` AND remove the `lotusDenoise()` shim injection +
|
||||||
|
`lotusDenoise=ml` param in cinny — otherwise audio is denoised twice.
|
||||||
|
Roll back = revert both.
|
||||||
|
4. Baseline is always upstream-equivalent, so any single feature can be disabled
|
||||||
|
by flipping its flag/send off without touching the rest.
|
||||||
|
|
||||||
|
**Blocker to step 1:** publishing the `lotus` branch needs a Gitea npm token
|
||||||
|
(the admin token used for the `0.20.1` parity publish was deleted). Either
|
||||||
|
provide a token for a manual `npm publish`, or stand up the Gitea Actions runner
|
||||||
|
|
||||||
|
- `GITEA_NPM_TOKEN` secret so a `v0.20.1-lotus.1` tag auto-publishes.
|
||||||
@@ -0,0 +1,114 @@
|
|||||||
|
# Lotus Chat — Open Bugs & Technical Debt
|
||||||
|
|
||||||
|
**Only OPEN and awaiting-verification items live here.** Resolved findings
|
||||||
|
(fixed-and-verified, false-positives, won't-fix) have been removed to keep this
|
||||||
|
actionable — the full history is in git. Items fixed in code but not yet
|
||||||
|
verified in a real environment are in **Needs Verification** below and have
|
||||||
|
step-by-step checks in [`LOTUS_TESTING.md`](./LOTUS_TESTING.md).
|
||||||
|
|
||||||
|
> Design rules for any fix here: follow the **Native-Cinny Law** and **TDS
|
||||||
|
> Design Law** in [`LOTUS_TODO.md`](./LOTUS_TODO.md).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ⚠️ Needs Verification — fixed in code, awaiting live testing
|
||||||
|
|
||||||
|
Implemented and gate-green; confirm each per `LOTUS_TESTING.md`, then delete the row.
|
||||||
|
|
||||||
|
| ID | Item | File / area | Test |
|
||||||
|
| :--- | :------------------------------------------------------------------------------------- | :--------------------------------------------------- | :-------------------------------------------------------------------------------- |
|
||||||
|
| #2 | Chat-background animation flicker (`contain:paint`) | `lotus/chatBackground.ts` | F1 |
|
||||||
|
| #4 | Ringtone re-fixes: classic loudness + caller decline notice (A2 ✓ live) | `CallEmbedProvider.tsx`, `ringtones.ts` | A1,A3,A4 |
|
||||||
|
| #6 | Background vs. seasonal theme mutual exclusion | `state/settings.ts`, `General.tsx` | F2 |
|
||||||
|
| #7 | Composer toolbar touch targets (≥44px) | `room/RoomInput.tsx` | E1 |
|
||||||
|
| #8 | Room Settings horizontal overflow (mobile) | `components/page/style.css.ts` | E2 |
|
||||||
|
| #9 | Modal fullscreen on mobile (`useModalStyle`) | 22+ modal files | E3 |
|
||||||
|
| #10 | Composer not hidden by keyboard (`100dvh`) | `src/index.css` | E4 |
|
||||||
|
| #12 | PiP "All muted" badge re-fixed (was firing on any single mute) | `hooks/useCallSpeakers.ts` | G1 |
|
||||||
|
| N96 | Call-recovery overlay single "Back" button | `call/CallView.tsx` | A7 |
|
||||||
|
| N95 | AFK-monitor mic released on mute (OS indicator clears) | `hooks/useAfkAutoMute.ts` | L1 |
|
||||||
|
| N108 | Maskable PWA icons (Android adaptive) | `public/manifest.json` + `res/android/maskable-*` | L2 |
|
||||||
|
| EC | EC iframe load watchdog + self-heal + recovery UI | `plugins/call/CallEmbed.ts`, `CallView.tsx` | A7 |
|
||||||
|
| N105 | Notification clicks work after tab close (SW `notificationclick` + `showNotification`) | `sw.ts`, `utils/dom.ts`, `ClientNonUIFeatures.tsx` | get a msg notif, close the tab, click it → app focuses/opens + routes to the room |
|
||||||
|
| Gal | MediaGallery lazy-decrypt (true virtualization deferred) | `room/MediaGallery.tsx` | H1 |
|
||||||
|
| a11y | aria-labels: edit-history / reaction / thread / reply | `message/*` (`FallbackContent`, `Reaction`, `Reply`) | I |
|
||||||
|
|
||||||
|
**Verified working in live testing (2026-06):** A2, B1–B4, C1, C3, D (mic/camera/deafen/screenshare/fullscreen/more-menu/PiP). Denoise quality in D is still poor — tracked under the denoise project, not a regression.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🧩 Element Call source-level items — now actionable via the fork
|
||||||
|
|
||||||
|
> 🔱 **[EC-FORK]** **UPDATE 2026-06-29: the fork is live.** We now own and
|
||||||
|
> self-build Element Call (`LotusGuild/element-call` →
|
||||||
|
> `@lotusguild/element-call-embedded`, Phase 1 done & cinny wired). A5/A6/A7
|
||||||
|
> below are **no longer "won't fix"** — they are ordinary source changes. See
|
||||||
|
> [`HANDOFF_ELEMENT_CALL_FORK.md`](./HANDOFF_ELEMENT_CALL_FORK.md) §10 + the Phase
|
||||||
|
> 2 work list. (The iframe is **same-origin** / self-hosted; the old blocker was
|
||||||
|
> that we didn't own EC's compiled source — which we now do.)
|
||||||
|
|
||||||
|
The in-call participant grid is rendered **inside EC's app**. Previously a
|
||||||
|
pre-built npm bundle we could only style/place around; now editable source.
|
||||||
|
Items from testing, with their fork-level fix path:
|
||||||
|
|
||||||
|
- **A5 — "Focus camera":** EC supports native tile-pinning. Our bottom-bar "Focus
|
||||||
|
camera" is a programmatic wrapper that **`.click()`s the tile** today
|
||||||
|
(`CallControl.ts` `focusCameraParticipant`), and during a screenshare EC
|
||||||
|
spotlights the shared screen so a camera pin may not override it. **Fork fix:**
|
||||||
|
add an `io.lotus.focus_participant` widget action that pins a participant in
|
||||||
|
EC's layout (coexisting with / overriding the screenshare spotlight); cinny
|
||||||
|
sends it via the widget API and the DOM-click hack is deleted. _Status: Open —
|
||||||
|
Actionable (Phase 2)._
|
||||||
|
- **A6 — avatar decorations in-call:** decorations render on **our** pre-join
|
||||||
|
lobby roster (`CallMemberCard`) but not on EC's in-call video tiles. **Fork
|
||||||
|
fix:** render the decoration APNG inside EC's participant-tile component, fed
|
||||||
|
decoration slugs via widget member data. _Status: Open — Actionable (Phase 2)._
|
||||||
|
- **A7 — mic dead after EC's "Reconnect":** the mid-call "Connection lost /
|
||||||
|
Reconnect" screen is **EC's own** (our load watchdog only covers an initial
|
||||||
|
hung load). After EC reconnects, the mic isn't re-published through our denoise
|
||||||
|
`getUserMedia` shim until a clean End+rejoin. **Fork fix:** move denoise into
|
||||||
|
EC's mic-capture/publish pipeline as a first-class audio stage — EC re-runs it
|
||||||
|
on every (re)publish, so reconnects keep denoise alive natively, and the
|
||||||
|
build-time `index.html` injection is removed. _Status: Open — Actionable
|
||||||
|
(Phase 2); root cause is the `getUserMedia` monkeypatch, not EC itself._
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔴 Open — Actionable
|
||||||
|
|
||||||
|
### Calls / Audio
|
||||||
|
|
||||||
|
- **N127 — ML denoise shim is never injected in `vite dev`.** The `lotusDenoise` plugin injects only on `closeBundle` (build), so ML noise suppression is silently inactive during local dev. Add a dev-mode injection (`configureServer` / `transformIndexHtml`). Dev-only impact. _Note: this **dissolves entirely** once denoise moves in-source in the fork (A7 fix) — there is then no build-time injection to be missing in dev._
|
||||||
|
|
||||||
|
### Security & Privacy
|
||||||
|
|
||||||
|
- **N97 — Access token stored in plaintext `localStorage`** (`state/sessions.ts`), vulnerable to XSS; device ID likewise. Architectural — needs a token-protection / session-storage redesign.
|
||||||
|
- **Session writes are non-atomic and not cross-tab synced** (`state/sessions.ts`) — risks inconsistent state / races across tabs.
|
||||||
|
- **Persisted PII without encryption:** user status message + expiry (`settings/account/Profile.tsx`), unsent composer drafts (`room/RoomInput.tsx`). Leak risk on shared devices.
|
||||||
|
|
||||||
|
### PWA / Offline / Notifications
|
||||||
|
|
||||||
|
- **N107 — SW has no `push` handler** — Web Push delivery is entirely non-functional. Needs a `push` listener + a Matrix push-gateway integration.
|
||||||
|
- **No app-asset caching strategy** (`src/sw.ts`) — no offline capability.
|
||||||
|
- ~~**`manifest: false`** may block PWA install~~ — **verified OK (2026-06):** `index.html` links `/manifest.json`, which exists in `public/` and is copied to `dist/`; VitePWA intentionally doesn't generate one. Not a bug.
|
||||||
|
|
||||||
|
### Dependencies & Build
|
||||||
|
|
||||||
|
- **`matrix-js-sdk` pinned to a Release Candidate** (`41.6.0-rc.0`); `@atlaskit` and build tools (`vite`, `typescript`, `eslint`) on unstable/experimental pins — review for stable versions; RC SDK is a tree-shaking/bundle-size risk.
|
||||||
|
- **Build-time overhead:** `lotusDenoise` does heavy sequential `fs` work in `closeBundle`; `viteStaticCopy` config is complex with redundant renames — could be streamlined.
|
||||||
|
|
||||||
|
### Code Hygiene / DevEx
|
||||||
|
|
||||||
|
- **Automated test suite — 231 tests across ~26 modules, a hard CI gate.** `npm test` runs Node's built-in runner via `tsx` (not vitest — Vite 8 is ahead of vitest's range) and **blocks the build job on failure**. Broad pure-logic coverage: utils (common, regex, sanitize/XSS, time, matrix, matrix-uia, mimeTypes, sort, accentColor, findAndReplace, AsyncSearch, ASCIILexicalTable, keyboard, room, matrix-crypto, featureCheck), state (settings, sessions, recentSearches, upload, typingMembers), plugins (matrix-to, call/utils, markdown/utils), lotus/avatarDecorations, search filters. Prevention work has caught + fixed **2 real bugs** (`findAndReplace` infinite-loop; `getSettings` crash-on-load when storage is blocked). **Next:** component/integration tests (the untestable-under-tsx DOM/React surface).
|
||||||
|
- **Extensive `as any` casts** across `src/` — gradual typing cleanup.
|
||||||
|
- **`types/matrix/` mirrors SDK types** instead of importing them — drift risk.
|
||||||
|
- **Hardcoded CDN URL** should move to an env var (the decoration CDN is now single-sourced in `avatarDecorations.ts`, but the literal is still in-repo).
|
||||||
|
- **`patch-folds.mjs` edits `node_modules` directly** — consider `patch-package`.
|
||||||
|
- **Infra docs:** `contrib/nginx` lacks security headers (HSTS/CSP) + uses rewrites over `try_files`; `contrib/caddy` has a placeholder path. CI/CD (`prod-deploy.yml`): sequential deploy, aggressive 1-min Netlify timeout, `package-manager-cache: false`.
|
||||||
|
- **README:** keep the fork-sync version + logo path current. (`CONTRIBUTING.md` is intentionally left as upstream Cinny's — not a Lotus concern.)
|
||||||
|
- **Architecture notes (low priority):** deep `features/` + `hooks/` nesting, many small coupled hooks, possible dead CSS/components, `SpacingVariant` / `DropTarget` recipe simplification.
|
||||||
|
- **Git workflow (forward-looking):** keep commits scoped — past monolithic "fix all bugs" commits and inconsistent prefixes hurt `git bisect`.
|
||||||
|
|
||||||
|
### Big Projects
|
||||||
|
|
||||||
|
- **#5 — Seasonal themes & chat-background redesign.** Current backgrounds are basic CSS; goal is high-fidelity, research-backed, GPU-accelerated designs (layered `oklch`, `backdrop-filter`, `contain:paint`) with WCAG-AA overlay contrast. Treat each as its own design sprint.
|
||||||
@@ -0,0 +1,419 @@
|
|||||||
|
# Lotus Chat — Manual Testing Guide
|
||||||
|
|
||||||
|
**Generated:** June 2026
|
||||||
|
**Scope:** Everything landed on the `lotus` branch since the v4.12.3 merge that I (Claude) could **not** verify statically and that needs a human in a real environment to confirm. Work through it top-to-bottom; the highest-risk / hardest-to-reproduce items are first.
|
||||||
|
|
||||||
|
> **How to report back:** For each numbered check, tell me **PASS** / **FAIL** (or **partial**). On any FAIL, include: what you saw vs. expected, the browser/OS (and whether web LXC 106 or the desktop/Tauri build), the theme you were on, and any **browser console** errors (F12 → Console). Screenshots help for anything visual.
|
||||||
|
|
||||||
|
## Environment notes
|
||||||
|
|
||||||
|
- You push from your own machine; these commits are local on `lotus` until you do.
|
||||||
|
- Test the **web** build (LXC 106 / `code.lotusguild.org`) first; re-run the **call** + **poll** sections on the **desktop (Tauri)** build too, since CSP and the EC iframe behave differently there.
|
||||||
|
- Several call features need a **second participant** (second account on another device/browser, or a colleague). Items that need this are marked **👥 2 people**.
|
||||||
|
- A couple of call items need a **third room/call** in parallel — marked **👥👥**.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Commits covered
|
||||||
|
|
||||||
|
| Commit | Area |
|
||||||
|
| :--------- | :--------------------------------------------------------------------------- |
|
||||||
|
| `caf6318a` | Poll vote buttons → folds tokens (N4) |
|
||||||
|
| `c67aed01` | In-call incoming-call banner (#4b) |
|
||||||
|
| `4a875884` | Selectable ringtone (#4a) |
|
||||||
|
| `0394fce9` | EC iframe load watchdog + recovery UI; avatar decorations on call tiles (#3) |
|
||||||
|
| `d2946c00` | Upload retry/backoff, presence-on-unload, typed m.direct |
|
||||||
|
| `b7e1f89c` | Timeline/composer/emoji perf memoization |
|
||||||
|
| `c0f98672` | Upstream **Element Call 0.20.1** merge (regression sweep) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## A. Calls — new ringtone + notification work (highest priority)
|
||||||
|
|
||||||
|
### A1. Ringtone selection — preview in Settings
|
||||||
|
|
||||||
|
**Steps**
|
||||||
|
|
||||||
|
1. Open **Settings → General**, scroll to the **Calls** section.
|
||||||
|
2. Find the new **Ringtone** dropdown (just above **Ringtone Volume**).
|
||||||
|
3. Select each option in turn: **Classic, Chime, Soft, Retro, Silent**.
|
||||||
|
|
||||||
|
**Expected**
|
||||||
|
|
||||||
|
- Selecting **Classic** plays the existing `call.ogg` clip (cut off after a few seconds).
|
||||||
|
- **Chime / Soft / Retro** each play a short, distinct synthesized preview.
|
||||||
|
- **Silent** plays nothing.
|
||||||
|
- Changing **Ringtone Volume** then re-selecting a ringtone previews at the new volume.
|
||||||
|
- No console errors.
|
||||||
|
|
||||||
|
> ⚠️ **Known browser limitation:** the synthesized tones use WebAudio. If a preview is ever silent, click anywhere on the page once (a "user gesture") and retry — browsers suspend audio until the page has been interacted with. The Settings preview is _after_ a click so it should always sound; this note matters more for A3.
|
||||||
|
|
||||||
|
### A2. Ringtone selection persists
|
||||||
|
|
||||||
|
1. Set Ringtone to **Retro**, reload the app.
|
||||||
|
2. **Expected:** the dropdown still shows **Retro** (setting persisted).
|
||||||
|
3. Bonus: in devtools, set `localStorage.settings` to a bogus `ringtoneId` and reload → it should fall back to **Classic**, not break.
|
||||||
|
|
||||||
|
### A3. Incoming call uses the selected ringtone — 👥 2 people
|
||||||
|
|
||||||
|
**Setup:** Account A (you) and Account B in a **DM** or a **private (invite-only) group** room.
|
||||||
|
|
||||||
|
1. As A, pick a non-silent ringtone (e.g. **Chime**).
|
||||||
|
2. From B, **start a call** in that DM/room. Do **not** answer on A.
|
||||||
|
|
||||||
|
**Expected on A**
|
||||||
|
|
||||||
|
- The full-screen **Incoming Call** dialog appears (caller name, room avatar, Answer / Reject).
|
||||||
|
- The **selected ringtone loops** until you answer/reject/ignore (at the set volume).
|
||||||
|
- Answer → joins the call. Reject (DM) / Ignore (group) → dialog dismisses and ring stops.
|
||||||
|
- Set ringtone to **Silent** and repeat → dialog still appears, **no sound**.
|
||||||
|
|
||||||
|
### A4. In-call banner for a second incoming call — 👥👥 (the trickiest one)
|
||||||
|
|
||||||
|
**Setup:** You (A) already **in a call** in Room 1. Account B can call you in a **different** Room 2 (a DM or private group you share). Ideally a third account C, or B leaves Room 1's call first.
|
||||||
|
|
||||||
|
1. While A is **actively in Room 1's call**, trigger an incoming call to A from **Room 2**.
|
||||||
|
|
||||||
|
**Expected on A**
|
||||||
|
|
||||||
|
- **No** full-screen takeover. Instead a **compact banner appears in the top-right corner** with the caller's avatar, room name, "Incoming voice/video call", and **Answer / Reject (or Ignore)** buttons.
|
||||||
|
- It plays a **single soft ping**, _not_ a looping ring (so it doesn't talk over your active call).
|
||||||
|
- The banner does **not** cover your active call's controls/PiP in a way that blocks them.
|
||||||
|
- **Answer** → switches you into Room 2's call. **Reject/Ignore** → banner disappears.
|
||||||
|
- The banner auto-dismisses if the caller hangs up / the call times out.
|
||||||
|
|
||||||
|
**Also verify the no-op case:** while in Room 1's call, if a notification for **Room 1 itself** arrives, **nothing** should pop up (no banner, no dialog).
|
||||||
|
|
||||||
|
### A5. Camera focus during screenshare (#1) — 👥 2 people
|
||||||
|
|
||||||
|
**Setup:** You (A) and B in a call; B (or another participant) **sharing their screen**, and at least one person with **camera on**.
|
||||||
|
|
||||||
|
1. As A, open the **participant glance** (the stacked avatars / member list for the call) and click a participant who has their **camera on**.
|
||||||
|
2. In the menu, click **"Focus camera"**.
|
||||||
|
|
||||||
|
**Expected**
|
||||||
|
|
||||||
|
- The view switches to **spotlight** and **pins that person's camera tile**, overriding the auto-spotlighted screenshare.
|
||||||
|
- It **stays** on that camera (doesn't immediately snap back to the screenshare).
|
||||||
|
- If you pick someone with their camera **off**, it should at worst just toggle spotlight (graceful fallback), not error.
|
||||||
|
|
||||||
|
### A6. Avatar decorations on call tiles (#3) — 👥 2 people
|
||||||
|
|
||||||
|
**Setup:** A participant in the call has an **avatar decoration** set (Settings → Profile decoration).
|
||||||
|
|
||||||
|
1. Join a call with that participant.
|
||||||
|
2. Look at **our** participant roster / prescreen tiles (not the avatars rendered inside the Element Call video grid — those are EC's and out of scope).
|
||||||
|
|
||||||
|
**Expected:** the decoration ring/overlay renders around that participant's avatar on the call tile, the same way it does in member lists.
|
||||||
|
|
||||||
|
### A7. EC iframe load watchdog + recovery UI (#EC, N96)
|
||||||
|
|
||||||
|
This guards against a permanently-stuck "Loading…" call. Also covers the N96 button-label fix (the old "Retry" and "Leave" buttons were identical — now there is a single **"Back"** button).
|
||||||
|
|
||||||
|
1. Normal case: **join a call** → it should connect within a few seconds as usual (the watchdog stays invisible).
|
||||||
|
2. Failure case (best-effort to reproduce): throttle your network hard (devtools → Network → Offline) **right as** you click join, or block the Element Call origin, so the iframe can't finish loading.
|
||||||
|
|
||||||
|
**Expected**
|
||||||
|
|
||||||
|
- On a genuine failure/timeout (~25s), instead of an endless spinner you get a **visible error overlay with a single "Back" button** (the old "Retry" + "Leave" pair is gone — they did the same thing and "Retry" was misleading).
|
||||||
|
- Clicking **Back** returns you to the call prescreen, where you can manually click Join to try again.
|
||||||
|
- Normal joins must **not** trigger the error overlay (no false positives) — this is the important part to confirm.
|
||||||
|
- **Self-heal:** if the error overlay appears on a slow network but EC then finishes loading anyway, the overlay should **dismiss itself** and drop you into the live call. Worth confirming on a deliberately throttled-but-not-blocked connection.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## B. Polls (N4) — render correctly on non-TDS themes
|
||||||
|
|
||||||
|
This was the actual bug: poll buttons used undefined CSS variables, so on the **default (non-Lotus-Terminal) themes** they rendered with invisible borders / no selected state.
|
||||||
|
|
||||||
|
### B1. Poll renders on a default theme — ✅ PASS
|
||||||
|
|
||||||
|
1. Switch to a **default Cinny theme** (Settings → Appearance — **not** Lotus Terminal / TDS). Test both a **dark** and a **light** theme.
|
||||||
|
2. In any room, create a poll (composer → poll button): a **single-choice** poll with 3 options.
|
||||||
|
|
||||||
|
**Expected**
|
||||||
|
|
||||||
|
- Each option is a clearly **bordered** button with visible rounded corners.
|
||||||
|
- A **radio circle** indicator is visible on the left of each option.
|
||||||
|
- Text, and (after votes) the percentage, are legible.
|
||||||
|
|
||||||
|
### B2. Voting + selected/progress state
|
||||||
|
|
||||||
|
1. **Vote** on an option.
|
||||||
|
**Expected**
|
||||||
|
|
||||||
|
- The selected option shows a **filled accent border + filled radio**, and an **accent progress-bar fill** grows behind it proportional to the vote %.
|
||||||
|
- The percentage and total vote count update.
|
||||||
|
- Click again / pick another option → selection moves correctly (single-choice replaces; the bar redraws).
|
||||||
|
|
||||||
|
### B3. Multiple-choice poll
|
||||||
|
|
||||||
|
1. Create a poll allowing **multiple selections**.
|
||||||
|
**Expected**
|
||||||
|
|
||||||
|
- Indicators are **square checkboxes** (not circles); selected ones show a **✓** that's legible against the filled box.
|
||||||
|
- You can select **several** options; each shows its own progress fill.
|
||||||
|
|
||||||
|
### B4. Lotus Terminal theme regression — ✅ PASS
|
||||||
|
|
||||||
|
1. Switch to **Lotus Terminal / TDS** theme and re-open a poll.
|
||||||
|
**Expected:** still looks correct (the fix uses theme tokens, so the TDS accent should now drive it) — no worse than before.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## C. Robustness / background behavior
|
||||||
|
|
||||||
|
### C1. Presence updates on tab close
|
||||||
|
|
||||||
|
1. Open the app, then **close the tab** (or quit the browser).
|
||||||
|
2. From another session/device, check your **presence** shortly after.
|
||||||
|
**Expected:** you go **offline/away** reliably (the unload now uses `fetch({keepalive})`). Previously this could be missed.
|
||||||
|
|
||||||
|
### C2. Upload retry on flaky network (best-effort)
|
||||||
|
|
||||||
|
1. In devtools → Network, set a throttle that drops/slows requests, or toggle Offline briefly **during** a file upload.
|
||||||
|
**Expected**
|
||||||
|
|
||||||
|
- A transient failure **retries** (up to 3×, with backoff) and the upload can still succeed once the network recovers.
|
||||||
|
- A genuine, permanent rejection (e.g. file too large / 4xx) still **fails fast** with the usual error — it should **not** spin retrying.
|
||||||
|
|
||||||
|
### C3. General timeline/composer perf (no functional regression)
|
||||||
|
|
||||||
|
The memoization changes are invisible if correct. Just confirm **nothing broke**:
|
||||||
|
|
||||||
|
- Open a busy room; scrolling, jump-to-latest, mark-as-read all still work.
|
||||||
|
- Composer: send a message, upload a file, share a location, pick an emoji and a sticker — all still work.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## D. Element Call 0.20.1 merge — regression sweep (👥 2 people)
|
||||||
|
|
||||||
|
The upstream bump changed EC's internals and DOM selectors; our call controls drive that iframe, so sweep them. In a live call with 2 people, confirm **each** of our control-bar buttons works:
|
||||||
|
|
||||||
|
- [ ] **Mic** mute/unmute (icon + actual audio)
|
||||||
|
- [ ] **Camera** on/off
|
||||||
|
- [ ] **Deafen / Sound** toggle (your deafen key too)
|
||||||
|
- [ ] **Screenshare** start/stop (and the "Share your screen?" confirm)
|
||||||
|
- [ ] **Screenshare audio** mute toggle
|
||||||
|
- [ ] **Fullscreen** toggle
|
||||||
|
- [ ] **⋮ More** menu → **Spotlight/Grid**, **Reactions**, **Settings** each open the right EC panel
|
||||||
|
- [ ] **End** call leaves cleanly
|
||||||
|
- [ ] **PTT** (push-to-talk) if enabled: hold key = transmit, release = mute; releasing on blur works
|
||||||
|
- [ ] **AFK auto-mute** if enabled: goes muted after the timeout
|
||||||
|
- [ ] **PiP** (picture-in-picture) mini window: drag, resize, fullscreen button, return-to-call; the "You muted" / "All muted" badges show on the right person
|
||||||
|
- [ ] **Denoise** (if ML noise suppression enabled): call audio still flows, no silence
|
||||||
|
|
||||||
|
If any control does nothing, that usually means an EC DOM selector changed — capture the console and tell me which button.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Backlog of previously-fixed-but-unverified items
|
||||||
|
|
||||||
|
> Sections A–D above are **this session's** work. Everything below was fixed in earlier waves and is still flagged **⚠️ UNTESTED** in `LOTUS_BUGS.md` / `LOTUS_TODO.md`. They're grouped by what kind of environment you need (mobile, desktop, screen reader, etc.) so you can knock out a whole category at once. None of these are urgent the way A–D are; do them as you have the right device handy.
|
||||||
|
|
||||||
|
## E. Mobile / responsive (needs a real phone, or devtools device emulation)
|
||||||
|
|
||||||
|
### E1. Composer toolbar touch targets (#7)
|
||||||
|
|
||||||
|
On a phone, open a room and the composer toolbar. Tap each button (attach, format, sticker, emoji, GIF, location, poll, schedule, send).
|
||||||
|
**Expected:** every button is comfortably tappable (≥44×44px), no mis-taps hitting the wrong icon.
|
||||||
|
|
||||||
|
### E2. Room Settings — no horizontal overflow (#8)
|
||||||
|
|
||||||
|
On a narrow phone screen, open **Room Settings**.
|
||||||
|
**Expected:** the settings nav panel fills the full width; **no** horizontal scrollbar / sideways scrolling anywhere in the panel.
|
||||||
|
|
||||||
|
### E3. Modals go fullscreen on mobile (#9)
|
||||||
|
|
||||||
|
On a phone, open several dialogs: Leave Room, Create Room, Create Space, Invite User, Report (room/user/message), Edit History, Forward Message, Remind Me, Schedule Message, Device Verification, Poll Creator.
|
||||||
|
**Expected:** each opens **fullscreen** (no floating box, no rounded corners / max-width margins). On desktop the same modals should still be the normal centered boxes.
|
||||||
|
|
||||||
|
### E4. Composer not hidden by the keyboard (#10) — iOS Safari especially
|
||||||
|
|
||||||
|
On a phone (priority: **iOS Safari**), tap into the composer so the on-screen keyboard appears.
|
||||||
|
**Expected:** the composer input stays **visible above** the keyboard; the layout shrinks rather than the composer sliding under the keyboard.
|
||||||
|
|
||||||
|
### E5. Mobile "Saved Messages" access (Mobile Bookmarks)
|
||||||
|
|
||||||
|
On a phone, **inside a room**, open the room header **··· More Options** menu.
|
||||||
|
**Expected:** a **"Saved Messages"** item is present; tapping it opens the bookmarks panel. (This was the only in-room access point missing on mobile.)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## F. Visual / theming
|
||||||
|
|
||||||
|
### F1. Animated chat background — no flicker (#2)
|
||||||
|
|
||||||
|
Settings → set an **animated** chat background (e.g. anim-rain / anim-aurora / anim-stars). Watch the message text and composer while it animates.
|
||||||
|
**Expected:** smooth animation, **no flickering / shimmering** on message text or the composer, especially after scrolling. Note your GPU/browser if you see artifacts.
|
||||||
|
|
||||||
|
### F2. Background vs. Seasonal theme are mutually exclusive (#6)
|
||||||
|
|
||||||
|
In Settings → Appearance:
|
||||||
|
|
||||||
|
1. Pick a **chat background** → confirm any **seasonal theme** auto-switches off.
|
||||||
|
2. Pick a **seasonal theme** → confirm the **chat background** auto-clears to none.
|
||||||
|
3. (Edge) If you have old data with both set, after reload only one should visibly apply (no double-overlay clutter).
|
||||||
|
|
||||||
|
### F3. Background / seasonal picker grid layout (N81)
|
||||||
|
|
||||||
|
In Settings → Appearance, look at the **Chat Background** and **Seasonal Theme** swatch grids; resize the window narrow→wide.
|
||||||
|
**Expected:** swatches reflow to fill each row evenly (responsive grid), with no lopsided/orphaned last row at any width.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## G. Calls — additional unverified (👥 2 people)
|
||||||
|
|
||||||
|
### G1. PiP mute badges point at the right person (#12)
|
||||||
|
|
||||||
|
In a call with at least one other person, pop out the **Picture-in-Picture** mini window.
|
||||||
|
|
||||||
|
- **You** mute your own mic → a **"You"/muted badge appears bottom-left** (your status).
|
||||||
|
- A **remote** participant (or all of them) mutes → an **"All muted"** badge appears **top-right** (clearly about other people).
|
||||||
|
**Expected:** the bottom-left badge is **never** triggered by someone else muting — that was the original bug (it looked like your own mic was muted when it wasn't).
|
||||||
|
|
||||||
|
### G2. Full-screen camera broadcasts
|
||||||
|
|
||||||
|
1. In a **camera-only** call (no screenshare), confirm the **Fullscreen** button is available (previously only showed during screenshare).
|
||||||
|
2. Use **MemberGlance → Focus camera** to full-screen/spotlight a specific person's camera. (Overlaps **A5**; if you've done A5 you can skip.)
|
||||||
|
|
||||||
|
### G3. PTT badge renders on all themes (N53)
|
||||||
|
|
||||||
|
Enable **Push-to-talk** (Settings → Calls) and join a call. Hold the PTT key.
|
||||||
|
**Expected:** the floating PTT badge above the controls shows "PTT — Hold KEY" when idle and "● Live" (green) while held — on **both** a default theme and Lotus Terminal (it's now a single folds Chip; the old terminal-only variant was removed).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## H. Media / performance (needs a room with many images)
|
||||||
|
|
||||||
|
### H1. Lazy image decryption (P5-5 / MediaGallery)
|
||||||
|
|
||||||
|
Open a room / media gallery with **many images** (ideally encrypted). Scroll down through them.
|
||||||
|
**Expected:** images decrypt/load as they **approach the viewport**, not all at once on open; scrolling stays smooth and memory doesn't balloon. Off-screen images shouldn't all decode up front.
|
||||||
|
|
||||||
|
### H2. Thumbnail framing (P5-6)
|
||||||
|
|
||||||
|
Look at **tall portrait** images in the timeline and in the media gallery.
|
||||||
|
**Expected:** thumbnails are framed **center-top** (so faces/subjects at the top aren't cropped out); no awkward stretching. Opening the full-size viewer still shows the **whole** image (contain, not cropped).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## I. Accessibility (needs a screen reader: VoiceOver / NVDA / TalkBack)
|
||||||
|
|
||||||
|
With a screen reader on, navigate message hover-actions and content and confirm each control **announces a meaningful label** (not "button" / blank):
|
||||||
|
|
||||||
|
- [ ] **Reaction** buttons announce the emoji + count (e.g. "thumbsup reaction, 3 people").
|
||||||
|
- [ ] **Edit history** button announces "View edit history".
|
||||||
|
- [ ] **Thread indicator** announces "View thread".
|
||||||
|
- [ ] **Reply** (jump to original) announces "Jump to original message".
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## J. Desktop / Tauri build only
|
||||||
|
|
||||||
|
### J1. Proactive update notifications (P5-40)
|
||||||
|
|
||||||
|
In the **desktop (Tauri)** build, with an update available, launch the app (and/or leave it running ~12h).
|
||||||
|
**Expected:** an in-app toast/badge alerts you that an update is available, without manually checking Settings. (Needs an actual newer release to point at.)
|
||||||
|
|
||||||
|
### J2. DTLN noise suppression sanity
|
||||||
|
|
||||||
|
In Settings → Calls, enable **ML noise suppression** with the **DTLN** model, then join a call.
|
||||||
|
**Expected:** your mic audio still flows (no silence/robotic dropouts) and background noise is reduced. Confirmed working earlier but flagged for a final real-call check; verify on **both** web and desktop.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## K. Features — end-to-end unverified
|
||||||
|
|
||||||
|
### K1. Remind Me Later
|
||||||
|
|
||||||
|
On a message, **··· → Remind Me**, pick a short preset (the 20-min one, or wait one out).
|
||||||
|
**Expected:** when due, a Lotus toast fires linking to that message; the reminder then clears itself. Survives a reload while pending (stored in account data).
|
||||||
|
|
||||||
|
### K2. Advanced search filters (P4-9)
|
||||||
|
|
||||||
|
In message search: use the **sender picker** (instead of typing `from:@user`), the **date-range** quick presets (Today / Last week / Last month / Last year), and the **Has link** toggle.
|
||||||
|
**Expected:** each narrows results correctly and reflects in the search.
|
||||||
|
|
||||||
|
### K3. Notification content + click target (P5-20 partial)
|
||||||
|
|
||||||
|
Trigger a desktop/browser notification for a new message.
|
||||||
|
**Expected:** it shows the **real message body** (`username: message`, not "New inbox notification from…"); **clicking it** brings the window to front and navigates **directly to that message** (not just the inbox).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## L. Fixed — verify
|
||||||
|
|
||||||
|
### L1. AFK auto-mute releases the OS microphone indicator on mute (N95) — 👥 live call
|
||||||
|
|
||||||
|
**Context (now FIXED):** `useAfkAutoMute.ts` opened its own `getUserMedia` level-monitor capture for the whole call, so the OS recording indicator (green dot on macOS, mic icon on Windows/Linux) stayed lit even when muted. The capture is now gated on the reactive mic-on state — it runs only while unmuted, so muting releases the stream.
|
||||||
|
|
||||||
|
**To verify:**
|
||||||
|
|
||||||
|
1. Enable **AFK auto-mute** in Settings → Calls and **join a call**.
|
||||||
|
2. Manually **mute your mic** using the call controls → the **OS recording indicator should clear** within ~a second.
|
||||||
|
3. **Unmute** → the indicator should re-appear (capture re-acquired).
|
||||||
|
4. Also confirm AFK still works end-to-end: stay unmuted and silent past the configured timeout → mic auto-mutes with the "muted after inactivity" toast, and the indicator clears.
|
||||||
|
|
||||||
|
### L2. Maskable PWA icon (N108) — Android install
|
||||||
|
|
||||||
|
1. On **Android Chrome**, install Lotus Chat as a PWA (Add to Home Screen).
|
||||||
|
2. Look at the **home-screen icon**.
|
||||||
|
|
||||||
|
**Expected:** the icon fills the adaptive-icon shape cleanly (the logo centered with safe-zone padding on the dark background), **not** clipped at the corners or floating in an odd box. Also worth a quick check in Chrome DevTools → Application → Manifest that the two `purpose: maskable` icons load without a 404 (this also validates the manifest's icon paths resolve in production — a pre-existing path convention I couldn't verify statically).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## M. New features (this round)
|
||||||
|
|
||||||
|
### M1. Search: `has:image` / `has:file` / `has:video` filters
|
||||||
|
|
||||||
|
1. Open message search (in a room with shared images/files/videos in history).
|
||||||
|
2. Run a broad search, then toggle the **Images**, **Files**, **Video** chips (in the filter bar, next to "Has link").
|
||||||
|
|
||||||
|
**Expected:**
|
||||||
|
|
||||||
|
- Each chip narrows the visible results to that message type; multiple active chips = union (any of them).
|
||||||
|
- Toggling them off restores the full results. The existing room/sender/date/has-link filters still work alongside.
|
||||||
|
- **Known limitation (by design):** filtering is client-side over already-fetched results, so the visible count can be lower than the server's total for that query — paginating/loading more pulls in more to filter. Confirm this reads acceptably.
|
||||||
|
|
||||||
|
### M2. Search: recent searches
|
||||||
|
|
||||||
|
1. Run a few different searches, then **clear the search box** and focus it.
|
||||||
|
|
||||||
|
**Expected:** your last (up to 10) distinct searches appear as clickable chips; clicking one re-runs it. A **Clear** affordance wipes the list. The list **persists across a page refresh** (localStorage).
|
||||||
|
|
||||||
|
### M3. Custom accent color (non-TDS themes) — ⚠️ needs your visual judgment
|
||||||
|
|
||||||
|
1. Make sure **Lotus Terminal (TDS)** is **off**. Settings → Appearance → **Custom Accent Color** → pick a color.
|
||||||
|
|
||||||
|
**Expected:**
|
||||||
|
|
||||||
|
- The app's accent (buttons, selected/active states, links, primary chips) recolors to your choice **live**.
|
||||||
|
- **Look critically at quality** (this is the part I can't verify): button **text legibility** (OnMain contrast) on the accent buttons; **hover/active** shades; and **selected-row / chip** backgrounds (the translucent "Container" tints). Try a **light** color and a **dark** color and a **saturated** one.
|
||||||
|
- If a dark accent makes selected-row text (OnContainer) hard to read, tell me — that's the one spot in the auto-derived palette most likely to need tuning.
|
||||||
|
- **Reset** clears it back to the theme default.
|
||||||
|
- Turn **Lotus Terminal ON** → the custom accent should be **ignored** (TDS fixed palette wins) and the picker shows a "non-TDS only" note; turn it back off → custom accent returns.
|
||||||
|
- Reload → the chosen accent **persists**.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### M4. Search: "Pinned only" filter
|
||||||
|
|
||||||
|
In message search, toggle the **Pinned** chip.
|
||||||
|
**Expected:** results narrow to messages currently pinned in their room; composes with the Images/Files/Video chips and room/sender/date filters; toggling off restores results. It also narrows the **encrypted/local-cache** results section (not just server results). Needs a room with actually pinned messages.
|
||||||
|
|
||||||
|
### M5. New theme presets (Cyberpunk / Ocean / Blood Red / Classic Matrix / Midnight) — ⚠️ visual judgment
|
||||||
|
|
||||||
|
Settings → Appearance → theme picker → try each of the 5 new themes.
|
||||||
|
**Expected:** each applies a complete, legible dark palette. Code review computed WCAG contrast and all pass AA, but **eyeball these specifically**: **Midnight** (lowest-contrast accent `#6b7ca8` — selected/focus states), **Classic Matrix** (green accents, light-green body text on near-black), **Blood Red** (white-ish text on bright-red buttons). Confirm Success/Warning/Critical (save/leave/delete) still look correctly green/amber/red, not recolored. Switching back to a stock theme should fully revert.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Priority if you're short on time
|
||||||
|
|
||||||
|
1. **A4** (in-call banner) + **A3** (ringtone) — newest, most logic, hardest to reproduce.
|
||||||
|
2. **B1–B3** (polls on a default theme) — the confirmed visual bug.
|
||||||
|
3. **D** (EC 0.20.1 control sweep) — guards against the upstream merge breaking calls.
|
||||||
|
4. **A7** false-positive check (normal joins don't show the error overlay).
|
||||||
|
5. Everything else.
|
||||||
@@ -0,0 +1,698 @@
|
|||||||
|
# Lotus Chat — Work Backlog
|
||||||
|
|
||||||
|
**Repo:** `lotus` branch at `https://code.lotusguild.org/LotusGuild/cinny`
|
||||||
|
**Deploy:** push to `lotus` → CI → auto-deploy to `chat.lotusguild.org` (~11 min)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ⚠️ TDS DESIGN LAW — READ BEFORE TOUCHING ANY UI
|
||||||
|
|
||||||
|
> **ALL Lotus Terminal Design System (TDS) styling — colors, animations, glows, borders, fonts, spacing — MUST come exclusively from `/root/code/web_template/base.css` CSS variables.**
|
||||||
|
> Do NOT hardcode hex values. Do NOT invent new variable names. Do NOT deviate from the design tokens defined in that file.
|
||||||
|
> The canonical variable reference: `--lt-accent-orange`, `--lt-accent-cyan`, `--lt-accent-green`, `--lt-glow-orange`, `--lt-box-glow-*`, `--lt-border-color`, etc.
|
||||||
|
> Reference implementation for code patterns: `/root/code/tinker_tickets/` (markdown.js, base.js, ticket.css)
|
||||||
|
> This rule applies to EVERY task in this file without exception.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🧩 NATIVE-CINNY LAW — EVERY FEATURE MUST FEEL LIKE STOCK CINNY
|
||||||
|
|
||||||
|
> **Every feature we implement must feel native to the upstream Cinny app — indistinguishable from something the Cinny team would have shipped.** Reference: <https://github.com/cinnyapp/cinny>.
|
||||||
|
>
|
||||||
|
> Concretely this means:
|
||||||
|
>
|
||||||
|
> - **Use the `folds` design system, not bespoke UI.** Build with folds primitives (`Button`, `Chip`, `IconButton`, `Menu`, `MenuItem`, `Dialog`, `Modal`, `Input`, `Switch`, `Badge`, `SettingTile`, `SequenceCard`, etc.) and folds tokens (`color.*`, `config.space.*`, `config.radii.*`, `config.borderWidth.*`). No hardcoded hex/`rgba()` for UI chrome, no invented/undefined CSS variables.
|
||||||
|
> - **Match Cinny's existing patterns.** Before adding UI, find the closest existing Cinny component/flow and mirror it (e.g. a new dropdown uses `Button`+`PopOut`+`Menu`+`MenuItem` like the rest; a new modal has a `Header` with a close `IconButton`; a new setting is a `SettingTile` inside a `SequenceCard`). Consistency with stock Cinny beats personal style.
|
||||||
|
> - **Lotus-custom additions should be unobtrusive** and fit Cinny's visual language, spacing, and interaction conventions — a stranger using Cinny should not be able to tell which features are ours.
|
||||||
|
>
|
||||||
|
> **The ONE exception:** explicit **Lotus Terminal Design System (TDS)** features, which intentionally have their own distinct look and follow the **TDS Design Law** above. TDS styling is opt-in (only active in Lotus Terminal mode); everything else must look and feel like native Cinny.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Completed features are documented in [LOTUS_FEATURES.md](./LOTUS_FEATURES.md).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ✅ Done — Awaiting Verification
|
||||||
|
|
||||||
|
Built and gate-green; verify per [LOTUS_TESTING.md](./LOTUS_TESTING.md), then they graduate to LOTUS_FEATURES.md. (Bug-side fixes awaiting verification live in LOTUS_BUGS.md.)
|
||||||
|
|
||||||
|
| Feature | Test guide |
|
||||||
|
| :-------------------------------------------------------------------------------- | :---------------- |
|
||||||
|
| Full-Screen Camera Broadcasts (per-participant focus) | A5 / G2 |
|
||||||
|
| Advanced search filters (sender/date/has-link/has:image·file·video/pinned/recent) | K2 / M1 / M2 / M4 |
|
||||||
|
| Custom Accent Color Picker (non-TDS themes) | M3 |
|
||||||
|
| 5 Color Theme Presets (Cyberpunk/Ocean/Blood Red/Matrix/Midnight) | M5 |
|
||||||
|
| Intersection-based lazy media loading | H1 |
|
||||||
|
| Context-aware thumbnail previews | H2 |
|
||||||
|
| Desktop — proactive update notifications (Tauri) | J1 |
|
||||||
|
| Remind Me Later | K1 |
|
||||||
|
| Mobile Bookmarks access | E5 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Legend:
|
||||||
|
|
||||||
|
- `[AUDIT REQUIRED]` — at least one assumption needs code/server verification before implementing
|
||||||
|
- `[SERVER CHECK]` — depends on a Synapse feature or MSC; verify on `matrix.lotusguild.org`
|
||||||
|
- `[LOW PRIORITY]` — implement after all higher-priority items
|
||||||
|
- `[EXTREME COMPLEXITY]` — multi-sprint, plan separately before touching
|
||||||
|
- `[BLOCKED]` — cannot build until a server upgrade, upstream MSC, or dependency resolves
|
||||||
|
- `[IMPROVE]` — feature exists in upstream Cinny; this task enhances it for Lotus Chat
|
||||||
|
|
||||||
|
Status: `[ ]` pending · `[~]` in progress · `[x]` completed
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Server Capabilities (as of June 2026)
|
||||||
|
|
||||||
|
- **Homeserver:** `matrix.lotusguild.org`
|
||||||
|
- **Synapse version:** `1.155.0` (2026-06-18) — fully up to date; last version for Debian 12 (LXC 151 already on Debian 13 Trixie)
|
||||||
|
- **Matrix spec:** up to `v1.12` formally; newer MSC features via `unstable_features`
|
||||||
|
|
||||||
|
### Confirmed facts
|
||||||
|
|
||||||
|
| Finding | Impact |
|
||||||
|
| --------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------ |
|
||||||
|
| **MSC flags ON:** `msc4140` · `msc3771` · `msc3440.stable` · `msc4133.stable` · `simplified_msc3575` · `msc4222` · `msc3266` · `msc3401_matrix_rtc` | All safe to use now |
|
||||||
|
| **MSC flags OFF:** `msc4306` (thread subscriptions) · `msc3882` · `msc3912` · `msc4155` | These features are BLOCKED |
|
||||||
|
| **MSC3266** room summary: flag `msc3266_enabled: true` set but `GET /v1/rooms/{id}/summary` still returns 404 (M_UNRECOGNIZED) | Room Preview BLOCKED — endpoint not implemented in Synapse 1.155 |
|
||||||
|
| **MSC3892** relation redaction: not in flags | Reaction Redaction feature BLOCKED |
|
||||||
|
| **MSC4260** report user: `POST /_matrix/client/v3/users/{userId}/report` returns **200** ✅ | **Report User UNBLOCKED** — endpoint live since Synapse 1.133; ready to build |
|
||||||
|
| **MSC4151** report room: HTTP 405 on GET = endpoint exists (POST only) | Report Room live ✅ |
|
||||||
|
| `folds AvatarImage` does NOT accept children | Add frame/overlay inside `UserAvatar.tsx` itself — optional `frameName` prop |
|
||||||
|
| No in-app toast system exists (was) | Built `ToastProvider` + Jotai queue; at `App.tsx:65` |
|
||||||
|
| `useUnverifiedDeviceCount()` hook exists | `src/app/hooks/useDeviceVerificationStatus.ts:65-106` |
|
||||||
|
| Voice player: `AudioContent.tsx:44-223` | Playback rate on hidden `<audio>` at line 217 |
|
||||||
|
| `CallControl.setMicrophone(bool)` at `CallControl.ts:206-212` | For AFK auto-mute |
|
||||||
|
| `CallControl.toggleSound()` at `CallControl.ts:230-251` | Push-to-deafen — just wire a hotkey to this |
|
||||||
|
| matrix-js-sdk has NO arbitrary profile field methods | Use `mx.http.authedRequest()` for MSC4133 |
|
||||||
|
| Sanitizer (`sanitize.ts`) allows table, div, span, a, code, hr | LFG HTML card is safe locally; test on Element/FluffyChat |
|
||||||
|
| Sanitizer STRIPS `<math>`/MathML tags | Math/LaTeX task must also modify sanitizer |
|
||||||
|
| Service worker EXISTS at `src/sw.ts` | Quick-reply task: add `notificationclick` handler |
|
||||||
|
| `knockSupported()` utility exists at `matrix.ts:376-391` | Knock UX: only need "Request to Join" in `RoomIntro.tsx` |
|
||||||
|
| `KeywordMessages.tsx` already has custom keyword push rules | Full push rule editor: only non-keyword rule types need new UI |
|
||||||
|
| `getMatrixToRoom()` in `matrix-to.ts` generates invite URLs | Invite link: just add QR code to room settings |
|
||||||
|
| Cindy CANNOT inject audio into EC call stream | In-call soundboard must be redesigned as local-only |
|
||||||
|
| Folds uses vanilla-extract in non-TDS, NOT CSS custom properties | Custom accent color: must create new vanilla-extract theme variant dynamically |
|
||||||
|
| Theme presets need ~50 CSS custom properties each | Significant design work before coding |
|
||||||
|
| `useCallSpeakers.ts` CSS MutationObserver polling | Visual speaking indicator: TDS ring animation on top of existing data |
|
||||||
|
| MSC3489/3672 live location: BOTH false on server | Live Location BLOCKED |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Key File Reference
|
||||||
|
|
||||||
|
| What you need | File | Lines |
|
||||||
|
| -------------------------------- | ------------------------------------------------------------- | ------------------- |
|
||||||
|
| Global keydown hook | `src/app/hooks/useKeyDown.ts` | whole file |
|
||||||
|
| Room navigation | `src/app/hooks/useRoomNavigate.ts` | 19-72 |
|
||||||
|
| All room IDs atom | `src/app/state/room-list/roomList.ts` | `allRoomsAtom` |
|
||||||
|
| Room unread counts | `src/app/state/room/roomToUnread.ts` | `roomToUnreadAtom` |
|
||||||
|
| Overlay portal provider | `src/app/pages/App.tsx` | 65 |
|
||||||
|
| Portal container div | `index.html` | 101 |
|
||||||
|
| Room settings tabs | `src/app/features/room-settings/RoomSettings.tsx` | 27-56 |
|
||||||
|
| State event read/write pattern | `src/app/features/common-settings/general/RoomEncryption.tsx` | 42-52 |
|
||||||
|
| Power level checker | `src/app/hooks/usePowerLevels.ts` | whole file |
|
||||||
|
| Slash command registration | `src/app/hooks/useCommands.ts` | 140-537 |
|
||||||
|
| Chat background picker | `src/app/features/settings/general/General.tsx` | 945-981 |
|
||||||
|
| Chat backgrounds definition | `src/app/features/lotus/chatBackground.ts` | whole file |
|
||||||
|
| Matrix.to URL builder | `src/app/plugins/matrix-to.ts` | `getMatrixToRoom()` |
|
||||||
|
| Media event content types | `src/app/types/matrix/common.ts` | 46-91 |
|
||||||
|
| Media URL conversion | `src/app/utils/matrix.ts` | `mxcUrlToHttp()` |
|
||||||
|
| Message pagination (search) | `src/app/features/message-search/useMessageSearch.ts` | 74-121 |
|
||||||
|
| Infinite pagination pattern | `src/app/features/message-search/MessageSearch.tsx` | 234-365 |
|
||||||
|
| Poll event format | `src/app/components/message/content/PollContent.tsx` | 1-320 |
|
||||||
|
| Theme class application | `src/app/hooks/useTheme.ts` | 25-60 |
|
||||||
|
| Animations file | `src/app/styles/Animations.css.ts` | whole file |
|
||||||
|
| Message status (EventStatus) | `src/app/features/room/message/Message.tsx` | 84-142 |
|
||||||
|
| Call member change events | `src/app/hooks/useCall.ts` | 37-52 |
|
||||||
|
| Mic control in calls | `src/app/plugins/call/CallControl.ts` | 206-212 |
|
||||||
|
| Device verification hook | `src/app/hooks/useDeviceVerificationStatus.ts` | 65-106 |
|
||||||
|
| Knock room support check | `src/app/utils/matrix.ts` | 376-391 |
|
||||||
|
| Room join button location | `src/app/components/room-intro/RoomIntro.tsx` | 25-119 |
|
||||||
|
| Notification mute via push rules | `src/app/hooks/useRoomsNotificationPreferences.ts` | 110-150 |
|
||||||
|
| Message text body CSS | `src/app/components/message/layout/layout.css.ts` | 182-205 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Priority 3 — Higher complexity / lower daily frequency
|
||||||
|
|
||||||
|
### [ ] P3-4 · Accessibility Improvements (WCAG 2.1 AA)
|
||||||
|
|
||||||
|
**What:** Comprehensive audit and fix pass targeting the critical user paths:
|
||||||
|
|
||||||
|
- Room list navigation (keyboard-only)
|
||||||
|
- Reading messages in the timeline (screen reader announces new messages)
|
||||||
|
- Composing and sending a reply
|
||||||
|
- Opening and closing modals (focus trap, return focus)
|
||||||
|
- ARIA labels on all icon-only buttons
|
||||||
|
|
||||||
|
**Scope:** Do NOT attempt to make every corner of the app AA-compliant in one pass — focus on the golden path (open app → find room → read → reply → send).
|
||||||
|
**[AUDIT REQUIRED]** — Run an automated audit first: `npx axe-core` or browser DevTools accessibility tree. Document every violation before writing a single line of code. Prioritize by severity (critical > serious > moderate).
|
||||||
|
|
||||||
|
**Investigation Findings:**
|
||||||
|
|
||||||
|
- **Root Cause:** Inconsistent focus management, missing `aria-live` regions for dynamic timeline updates, and sparse global keyboard shortcuts.
|
||||||
|
- **Approach:** Standardize `focus-trap-react` usage (reference `RoomNavItem.tsx`). Add `aria-live` regions to the timeline. Expand `useKeyDown.ts` for section navigation shortcuts.
|
||||||
|
- **Complexity:** Medium-High (audit is the main work).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### [ ] P3-8 · Thread Panel (full side drawer)
|
||||||
|
|
||||||
|
**⚠️ LARGEST FEATURE — requires its own planning session before implementation.**
|
||||||
|
**What:** A right-side drawer for threaded conversations. Currently "Reply in Thread" exists but there is no panel to read or write thread replies.
|
||||||
|
|
||||||
|
Features:
|
||||||
|
|
||||||
|
- Click "Reply in Thread" → opens thread drawer on the right
|
||||||
|
- Thread root event shown at the top of the panel
|
||||||
|
- Full message rendering for all in-thread replies (reuse timeline components)
|
||||||
|
- Reply input at the bottom (full composer with formatting, emoji, etc.)
|
||||||
|
- Unread count badge on the thread button in the main timeline
|
||||||
|
- Keyboard shortcut to close thread panel
|
||||||
|
|
||||||
|
**Architecture:**
|
||||||
|
|
||||||
|
- New Jotai atom: `activeThreadEventId: string | null`
|
||||||
|
- New component: `src/app/features/room/thread/ThreadPanel.tsx`
|
||||||
|
- Rendered alongside `RoomView` as a conditional right panel (mirror the members drawer pattern)
|
||||||
|
- Filter events in timeline to `m.thread` relation for the active root event ID
|
||||||
|
- Shares the same `mx` client and room reference as the main timeline
|
||||||
|
|
||||||
|
**[AUDIT REQUIRED]** — Deeply audit how `m.thread` relation events are currently stored and retrieved in the matrix-js-sdk. Understand the thread aggregation API: `GET /rooms/{roomId}/relations/{eventId}/m.thread`. Check if `RoomTimeline.tsx` currently filters out thread replies from the main timeline (it should — confirm).
|
||||||
|
|
||||||
|
**Investigation Findings:**
|
||||||
|
|
||||||
|
- **Root Cause:** Current `m.thread` events are treated as standard `m.room.message` events and rendered in the main timeline.
|
||||||
|
- **Approach:** Introduce new Jotai atom `activeThreadEventId`. Create `ThreadPanel.tsx`. Update `RoomTimeline.tsx` to filter out thread relations (`m.relates_to`). Implement aggregation fetch using `GET /rooms/{roomId}/relations/{eventId}/m.thread`. Use `thread.timelineSet` directly for the most accurate thread view.
|
||||||
|
- **Complexity:** High.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Priority 4 — Specialized, high complexity, or low priority
|
||||||
|
|
||||||
|
### [ ] P4-7 · Virtualized Infinite Scroll for Search Results
|
||||||
|
|
||||||
|
**What:** Replace the manual "load more" button with an automated, virtualized infinite scroll for search results.
|
||||||
|
**Approach:** Utilize `@tanstack/react-virtual` in `MessageSearch.tsx` to handle the `nextToken` automatically as the user scrolls.
|
||||||
|
|
||||||
|
### [ ] P4-8 · Encrypted Message Search Indexing & Caching
|
||||||
|
|
||||||
|
**What:** Implement a persistent local cache for search results, optimized for encrypted rooms.
|
||||||
|
**Approach:** Use `IndexedDB` to store search metadata (event IDs, timestamps) to prevent redundant server-side decryption/fetching.
|
||||||
|
|
||||||
|
### [ ] P4-1 · Thread Notification Mode Per-Thread (MSC3771)
|
||||||
|
|
||||||
|
**Spec:** MSC3771 (stable). Depends on Thread Panel (#P3-8).
|
||||||
|
**What:** Per-thread notification toggle: "All messages" vs "Mentions only". Accessible from the thread panel header. Tracks unread counts separately per thread.
|
||||||
|
**[AUDIT REQUIRED]** — Implement after Thread Panel. Requires understanding how the SDK tracks per-thread unread counts.
|
||||||
|
**Complexity:** Medium (after thread panel exists).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### [ ] P4-2 · Thread Subscriptions (MSC4306) [BLOCKED]
|
||||||
|
|
||||||
|
**Spec:** MSC4306 (Synapse experimental). Depends on Thread Panel (#P3-8).
|
||||||
|
**What:** "Follow thread" button to receive notifications for a thread you haven't posted in. Uses MSC4306 subscription endpoint.
|
||||||
|
**[SERVER CHECK]** — `org.matrix.msc4306 = false` on `matrix.lotusguild.org` — BLOCKED until server enables it.
|
||||||
|
**Complexity:** Medium (after thread panel exists).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### [ ] P4-4 · Math / LaTeX Rendering in Messages (LOW PRIORITY)
|
||||||
|
|
||||||
|
**Spec:** CS-API §11.5 (stable) — `formatted_body` can contain LaTeX.
|
||||||
|
**What:** Render `$...$` or `$$...$$` LaTeX expressions in message bodies. Use KaTeX (lightweight, ~100KB, renders server-side-compatible CSS). Must gracefully fall back to raw LaTeX text if KaTeX fails.
|
||||||
|
**Note:** This is LOW PRIORITY — only useful for academic/technical communities. Implement last.
|
||||||
|
**[AUDIT REQUIRED]** — Confirm KaTeX bundle size impact on the Vite bundle. Check if matrix-js-sdk's HTML sanitizer strips LaTeX before it reaches the renderer. The formatted_body sanitization pipeline is the main risk here. (Confirmed: sanitizer STRIPS `<math>` tags — must be patched alongside the renderer.)
|
||||||
|
**Complexity:** Low-Medium.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### [ ] P4-5 · Live Location Sharing (MSC3489 + MSC3672) (LOW PRIORITY, HIGH COMPLEXITY) [BLOCKED]
|
||||||
|
|
||||||
|
**Spec:** MSC3489 + MSC3672. Implemented in Element Web.
|
||||||
|
**Note:** Static location sharing is already implemented. This adds live/real-time GPS beacons. Very low priority per user preference.
|
||||||
|
**What:** Start sharing live location → creates `m.beacon_info` state event → client posts `m.beacon` events on a timer → other users see your position update live on a map.
|
||||||
|
**[SERVER CHECK]** — `org.matrix.msc3489 = false` AND `org.matrix.msc3672 = false` on `matrix.lotusguild.org` — BLOCKED.
|
||||||
|
**Complexity:** High. Requires background geolocation API + live map rendering.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### [ ] P4-6 · OIDC / SSO Next-Gen Auth (MSC3861) (EXTREME COMPLEXITY, LOW PRIORITY)
|
||||||
|
|
||||||
|
**Spec:** MSC3861, merged Matrix spec v1.15. Uses Matrix Authentication Service (MAS).
|
||||||
|
**Context:** ~80% of homeserver users have LLDAP/Authelia/SSO accounts. SSO is currently enabled on `matrix.lotusguild.org` but accounts are not yet linked. This would allow users to log in via their SSO credentials.
|
||||||
|
**What:** OAuth 2.0 / OIDC login flow, token refresh, account management page linking Matrix identity to SSO identity.
|
||||||
|
**EXTREME COMPLEXITY** — requires: MAS deployment/configuration on the homeserver, significant auth flow changes in the client, token refresh handling, session management overhaul.
|
||||||
|
**[SERVER CHECK]** — Before any client work, audit whether MAS is already deployed on `compute-storage-01`. Check: `pct exec 151 -- systemctl status matrix-authentication-service` or similar.
|
||||||
|
**Complexity:** Extreme. Multi-sprint project. Plan separately.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Priority 5 — Gamer / Aesthetic / Customization
|
||||||
|
|
||||||
|
### [MOVED] P5-9 · LFG (Looking for Group) Command → LotusBot
|
||||||
|
|
||||||
|
**Decision:** Implemented as `!lfg` in LotusBot rather than a client slash command. Bot-side rendering works consistently across all Matrix clients; client-side enhanced cards would only be visible to Lotus Chat users and require sanitizer auditing. The bot can also support richer flows (list active LFGs, DM interested players, auto-expire posts).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### [ ] P5-15 · In-Call Soundboard
|
||||||
|
|
||||||
|
**What:** Grid of short audio clips playable into the call audio stream via Web Audio API (AudioBufferSourceNode → MediaStreamDestinationNode → mixed with mic). Built-in clips + user-uploadable custom clips (stored as mxc://). Accessible from call controls bar.
|
||||||
|
**[AUDIT REQUIRED]** Verify the Element Call integration exposes the mic MediaStream for mixing. This is the highest-risk part of this feature.
|
||||||
|
**🔱 [EC-FORK]** Owning the EC source (see [`HANDOFF_ELEMENT_CALL_FORK.md`](./HANDOFF_ELEMENT_CALL_FORK.md)) would unblock real audio-injection — a proper soundboard mixed into the call — which is impossible against the prebuilt bundle today.
|
||||||
|
**Complexity:** High.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### [~] P5-20 · Quick Reply from Browser Notification
|
||||||
|
|
||||||
|
**What:** Inline reply field in browser notification toasts via Notification Actions API. Reply sends as threaded reply to the triggering message.
|
||||||
|
**[AUDIT REQUIRED]** (1) Verify browser Notification Actions API support in target browsers. (2) Confirmed: service worker EXISTS at `src/sw.ts` — add `notificationclick` handler there.
|
||||||
|
**Complexity:** Medium-High.
|
||||||
|
**Partial Fix Applied ⚠️ UNTESTED:** Notifications now (a) show the real message body (`username: message` instead of "New inbox notification from..."), (b) click navigates directly to the room at the specific event (not the inbox), (c) `window.focus()` called on click so the tab comes to front, (d) reminder toasts also link to the specific event. Full inline-reply via Notification Actions API still needs the SW `push`+`notificationclick` pipeline (requires switching from `new Notification()` to `showNotification()` through the SW).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### [x] P5-30 · Advanced ML Noise Suppression (Krisp-style)
|
||||||
|
|
||||||
|
**What:** High-end background noise cancellation using a pre-trained ML model (RNNoise) running in the browser. Removes dogs, fans, and keyboard clicks from the mic stream.
|
||||||
|
**Shipped:** 3-tier setting (Off / Browser-native / ML) in Settings → General → Calls. ML tier injects a same-origin pre-init shim into the vendored Element Call `index.html` that monkeypatches `getUserMedia` and routes the captured mic through an RNNoise `AudioWorklet` before LiveKit publishes — no EC fork required. See LOTUS_FEATURES.md → "Noise Suppression (Advanced Multi-Tier)".
|
||||||
|
**🔱 [EC-FORK]** Once we own the EC source (see [`HANDOFF_ELEMENT_CALL_FORK.md`](./HANDOFF_ELEMENT_CALL_FORK.md)), denoise should become a first-class audio stage **inside** EC instead of an `index.html` getUserMedia monkeypatch — more robust, survives reconnects (fixes the A7 mic-after-reconnect bug), and removes the build-time injection hack.
|
||||||
|
**Key decision:** LiveKit's Krisp filter is LiveKit-Cloud-only (we self-host the SFU); EC's own RNNoise PR #3892 is unmerged. The shim is the same post-capture pipeline #3892 uses, executed from the realm we control, so it survives EC version bumps.
|
||||||
|
**AEC note (resolved-as-accepted):** WebAudio capture routing can weaken browser AEC — same tradeoff as EC's upstream feature; mitigated by keeping `echoCancellation`/`autoGainControl` on the raw capture and labeling the tier "beta".
|
||||||
|
|
||||||
|
**Model Roadmap (priority order):**
|
||||||
|
|
||||||
|
- [ ] **Verify DTLN** (16 kHz narrowband fix) in a real call before investing further — wired but unverified.
|
||||||
|
- [ ] **DeepFilterNet 3** — best self-hostable upgrade: Rust→WASM, CPU real-time, 48 kHz fullband. Effort: self-host `df_bg.wasm` + DFN3 ONNX model, wire a 48 kHz worklet.
|
||||||
|
- [ ] **Desktop-only / HW-gated:** FRCRN or NVIDIA Maxine (RTX/Tensor only) — impossible in-browser; would run in Tauri Rust backend + bridge a virtual mic into the webview. Must detect capability and only offer on supported hardware; web falls back to RNNoise.
|
||||||
|
- **Excluded:** Krisp (LiveKit Cloud only); FRCRN/Maxine on web (GPU/server-bound).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### [ ] P5-31 · Granular Voice & Screenshare Quality Controls (Discord-style)
|
||||||
|
|
||||||
|
**What:** Let users (or room admins via room settings) adjust audio bitrates (e.g., 64kbps to 512kbps) and screenshare quality (resolution: 720p/1080p/Source, framerate: 15/30/60fps).
|
||||||
|
**Note:** Requires tight integration with the LiveKit SFU and custom state events for per-room quality caps.
|
||||||
|
**[AUDIT REQUIRED]** Must verify if current `lk-jwt-service` can be extended with custom bitrate/resolution claims or if a new sidecar (similar to `voice-limit-guard`) is needed for server-side enforcement.
|
||||||
|
**Complexity:** Extreme.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### [ ] P5-35 · Desktop — Notification Click Opens Room (DEFERRED)
|
||||||
|
|
||||||
|
**What:** Clicking a system tray notification navigates to the relevant room. Quick-reply from the notification toast would send the reply without opening the window.
|
||||||
|
**Status:** Deferred — `tauri-plugin-notification` has no Rust click/action callback API. Quick-reply would need a custom WinRT toast activator + COM registration, which can't be compile-tested without a Windows build environment.
|
||||||
|
**Note:** Tray icon and `matrix:` deep links already bring the window forward on most interactions. Revisit when tauri-plugin-notification gains click handler support upstream.
|
||||||
|
**Complexity:** High (platform-specific native code required).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### [ ] P5-36 · Desktop — Windows Jump List (DEFERRED)
|
||||||
|
|
||||||
|
**What:** Right-clicking the taskbar icon shows a jump list with recent/favorite rooms for quick navigation.
|
||||||
|
**Status:** Deferred — implementing the Windows COM jump list API in Tauri requires iterating on C++/COM code that can only be compile-checked on Windows, making blind CI iteration impractical.
|
||||||
|
**Action when unblocked:** Revisit when a Tauri plugin abstracts the Windows Shell `ICustomDestinationList` interface, or when a Windows build environment is available for local iteration.
|
||||||
|
**Complexity:** High (Windows-only native COM).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### [ ] P5-41 · Desktop — Native WinRT Toast Notifications
|
||||||
|
|
||||||
|
**What:** Replace emulated notifications with native WinRT Toast notifications.
|
||||||
|
**Approach:** Implement native WinRT Toast integration using `windows-rs` to enable full Action Center integration, including native Quick Reply functionality.
|
||||||
|
|
||||||
|
### [ ] P5-42 · Desktop — Persistent Background Sync
|
||||||
|
|
||||||
|
**What:** Maintain light connection to homeserver when WebView2 is suspended.
|
||||||
|
**Approach:** Implement a headless Rust sidecar to fetch unread counts/notifications while the webview is suspended to ensure instant notification delivery.
|
||||||
|
|
||||||
|
### [ ] P5-43 · Desktop — System Media Transport Controls (SMTC)
|
||||||
|
|
||||||
|
**What:** Integrate with Windows SMTC for volume flyout call/media control.
|
||||||
|
**Approach:** Use Windows SMTC API to expose call status, mic mute/unmute, and media controls to the Windows volume flyout/media overlay.
|
||||||
|
|
||||||
|
### [ ] P5-44 · Desktop — Taskbar Thumbnail Toolbar
|
||||||
|
|
||||||
|
**What:** Add persistent call controls to the taskbar preview.
|
||||||
|
**Approach:** Implement a COM thumbnail toolbar in the application preview window, featuring Mute/Deafen/End Call buttons.
|
||||||
|
|
||||||
|
### [ ] P5-46 · Desktop — System Power Management (Call Continuity)
|
||||||
|
|
||||||
|
**What:** Prevent system sleep/hibernate during active calls.
|
||||||
|
**Approach:** Use Tauri/Rust `power-manager` or platform-specific APIs to block system power saving states while a voice/video session is active.
|
||||||
|
|
||||||
|
### [ ] P5-47 · Desktop — TDS-Styled Native Window Chrome
|
||||||
|
|
||||||
|
**What:** Replace system titlebar with custom Lotus TDS chrome.
|
||||||
|
**Approach:** Configure Tauri window (`decorations: false`) and implement custom, TDS-token compliant titlebar controls (Close/Max/Min) for a cohesive UI.
|
||||||
|
|
||||||
|
### [ ] P5-48 · Desktop — Native File System Drag-and-Drop Improvements
|
||||||
|
|
||||||
|
**What:** Enhance drag-and-drop support for Windows.
|
||||||
|
**Approach:** Improve handling for Windows file shortcuts, recursive folder uploads, and shell-integrated "Send To" context menu actions.
|
||||||
|
|
||||||
|
### [ ] P5-49 · Desktop — Network Awareness (NCSI Integration)
|
||||||
|
|
||||||
|
**What:** Proactively detect Windows network connectivity changes.
|
||||||
|
**Approach:** Integrate with the Windows Network Connectivity Status Indicator (NCSI) API to improve offline mode transition latency and network recovery.
|
||||||
|
|
||||||
|
### [ ] P5-50 · Desktop — Windows Hardware-Accelerated Media Pipeline
|
||||||
|
|
||||||
|
**What:** Replace standard browser decoding with native Windows Media Foundation.
|
||||||
|
**Approach:** Leverage DirectShow/Media Foundation to offload video/audio decoding from the CPU to the GPU, significantly reducing power consumption and latency during calls.
|
||||||
|
|
||||||
|
### [ ] P5-51 · Desktop — Federated "Identity Contexts" (Isolation Manager)
|
||||||
|
|
||||||
|
**What:** Compartmentalize sessions, local databases, and caches into isolated "Contexts."
|
||||||
|
**Approach:** Implement a zero-leak boundary for personas (e.g., Work vs. Personal) by isolating `IndexedDB`, filesystem caches, and session persistence per context.
|
||||||
|
**Priority:** Extreme Low (Multi-sprint/Architectural).
|
||||||
|
|
||||||
|
### [~] P5-52 · Desktop — Room-Level Sync Governor (Performance Control) [STILL_CONSIDERING]
|
||||||
|
|
||||||
|
**What:** Granular sync tuning for individual rooms.
|
||||||
|
**Approach:** Allow per-room overrides for sync frequency and event type filtering (e.g., disable read receipts/typing in heavy rooms) to optimize performance. Implementation requires careful UX to prevent complexity fatigue.
|
||||||
|
|
||||||
|
### [ ] P5-53 · Desktop — Local-Only "Scripting" Plugin System (Tampermonkey-like)
|
||||||
|
|
||||||
|
**What:** A sandboxed environment for local execution of user scripts on Matrix events.
|
||||||
|
**Approach:** Implement a WASM-based execution engine that allows users to write local-only, client-side scripts to interact with incoming Matrix events, trigger sounds/notifications, or inject custom UI elements based on event payload rules. Designed for privacy — all logic runs exclusively on the local machine.
|
||||||
|
|
||||||
|
### [ ] P5-55 · Desktop — Composer Toolbar Drag-and-Drop Reordering
|
||||||
|
|
||||||
|
**What:** Allow users to reorder toolbar icons via drag-and-drop.
|
||||||
|
**Approach:** Extend the current settings-based toolbar toggle system to include a drag-and-drop UI mode in the composer settings, allowing users to personalize their icon order.
|
||||||
|
|
||||||
|
### [ ] P5-56 · Desktop — Windows "Focus Assist" (DND) Sync
|
||||||
|
|
||||||
|
**What:** Automatically toggle notification state based on Windows Focus Assist.
|
||||||
|
**Approach:** Integrate with the Windows `NotificationCenter` / `Focus` state via Tauri/Rust to automatically enable/disable Lotus Chat's internal notification suppression mode when Windows Focus Assist is toggled.
|
||||||
|
|
||||||
|
### [ ] P5-57 · Desktop — Visual Draft Persistence Indicator
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚀 Features to Add
|
||||||
|
|
||||||
|
- [ ] **Mobile Audit:** Comprehensive audit of all features in LOTUS_FEATURES.md for mobile PWA usability and layout responsiveness.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Blocked Features
|
||||||
|
|
||||||
|
These features are confirmed desirable but cannot be built until the listed dependency is resolved.
|
||||||
|
Check back after each Synapse upgrade — re-run `/matrix/client/versions` and `unstable_features` to see if they've become available.
|
||||||
|
|
||||||
|
### [BLOCKED] · Live Location Sharing (MSC3489 + MSC3672)
|
||||||
|
|
||||||
|
**Blocked by:** `org.matrix.msc3489 = false` AND `org.matrix.msc3672 = false` on `matrix.lotusguild.org` (confirmed from unstable_features).
|
||||||
|
**What it would do:** Real-time GPS beacon streaming upgrading the existing static location share.
|
||||||
|
**Action when unblocked:** Both MSCs must be enabled on the homeserver before any client work.
|
||||||
|
|
||||||
|
### [BLOCKED] · Reaction / Relation Redaction (MSC3892)
|
||||||
|
|
||||||
|
**Blocked by:** `org.matrix.msc3892` = false on `matrix.lotusguild.org`
|
||||||
|
**What it would do:** Cleanly remove a reaction without redacting the parent message.
|
||||||
|
**Current behavior:** Full event redaction — acceptable fallback, no user-facing issue.
|
||||||
|
**Action when unblocked:** Find `onReactionToggle` redaction call site; swap in MSC3892 endpoint with fallback.
|
||||||
|
|
||||||
|
### [BLOCKED] · Room Preview Before Joining (MSC3266)
|
||||||
|
|
||||||
|
**Blocked by:** `GET /_matrix/client/v1/rooms/{roomId}/summary` returns `M_UNRECOGNIZED` 404 — endpoint not implemented in Synapse 1.155. Config flag `msc3266_enabled: true` is set but has no effect; Synapse appears not to have shipped a stable implementation at the v1 path. Verified 2026-06-18.
|
||||||
|
**What it would do:** Show room name, topic, avatar, member count before joining.
|
||||||
|
**Action when unblocked:** Re-test after each future Synapse upgrade.
|
||||||
|
|
||||||
|
### [BLOCKED] · Thread Subscriptions (MSC4306)
|
||||||
|
|
||||||
|
**Blocked by:** `org.matrix.msc4306` = false on `matrix.lotusguild.org`
|
||||||
|
**What it would do:** Follow a thread without posting; get notifications for replies.
|
||||||
|
**Action when unblocked:** Add "Follow thread" button in the thread panel header (depends on #P3-8 Thread Panel).
|
||||||
|
|
||||||
|
### [DONE] · Report User (MSC4260) ✅
|
||||||
|
|
||||||
|
**Previously blocked by:** Server spec v1.12, but `POST /_matrix/client/v3/users/{userId}/report` was confirmed **200** on 2026-06-18 (live since Synapse 1.133.0).
|
||||||
|
**What it does:** Reports a specific user to homeserver admins (separate from reporting a message).
|
||||||
|
**Note:** Report Message already exists in upstream Cinny. This adds Report User to the profile panel.
|
||||||
|
**Implemented 2026-06-18:** `ReportUserModal.tsx` added at `src/app/features/room/ReportUserModal.tsx`. Button wired into `UserRoomProfile.tsx` between UserModeration and UserDeviceSessions (hidden for own profile). Category dropdown + reason text, inline success/error feedback, auto-close 1500ms after success.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Pending Audits
|
||||||
|
|
||||||
|
### [ ] Audit-3 · Profile banner image — Matrix protocol support
|
||||||
|
|
||||||
|
Research whether Matrix spec or MSC4133 (v1.16) defines a standard profile banner field. `uk.tcpip.msc4133.stable = true` on our server — check if a `banner_url` or similar field is defined. If no cross-client standard exists, do not implement.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📚 Implementation Reference
|
||||||
|
|
||||||
|
Exhaustive, low-level implementation details for backlog items. Follow these patterns to ensure code is "Lotus-perfect" (idiomatic, performant, and TDS-compliant).
|
||||||
|
|
||||||
|
### P3-8 · Thread Panel (Full Side Drawer)
|
||||||
|
|
||||||
|
**Architecture:** Mirror the `MembersDrawer` pattern but with a specialized timeline.
|
||||||
|
|
||||||
|
- **State (`src/app/state/room/thread.ts`):**
|
||||||
|
```typescript
|
||||||
|
export const activeThreadIdAtom = atom<string | null>(null);
|
||||||
|
```
|
||||||
|
- **Layout (`src/app/features/room/Room.tsx`):** Insert `ThreadPanel` conditionally alongside `RoomTimeline`:
|
||||||
|
```tsx
|
||||||
|
{
|
||||||
|
activeThreadId && (
|
||||||
|
<>
|
||||||
|
<Line variant="Background" direction="Vertical" size="300" />
|
||||||
|
<ThreadPanel roomId={roomId} threadId={activeThreadId} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
- **Component (`src/app/features/room/thread/ThreadPanel.tsx`):** Use `room.getThread(threadId)` from the SDK. Render a `Header` with a "Close" button that sets `activeThreadIdAtom` to `null`. Reuse `RoomTimeline` but pass a filtered `EventTimelineSet`. Use `thread.timelineSet` directly for the most accurate thread view.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### P4-4 · Math / LaTeX Rendering
|
||||||
|
|
||||||
|
**Mechanism:** KaTeX injection into the HTML parser.
|
||||||
|
|
||||||
|
- **Sanitizer (`src/app/utils/sanitize.ts`):** Allow KaTeX-specific tags and classes (e.g., `span`, `annotation`, `math`). Use a specialized allowed list for math blocks.
|
||||||
|
> [Gemini_Found] `sanitize.ts` uses **`sanitize-html`** (not DOMPurify) with an explicit allowlist (`allowedTags`) and `disallowedTagsMode: 'discard'`. All MathML tags are currently absent from the allowlist and are silently stripped. Update `permittedHtmlTags` to include: `<math>`, `<mi>`, `<mo>`, `<mn>`, `<ms>`, `<mtext>`, `<mspace>`, `<mrow>`, `<mfrac>`, `<msqrt>`, `<mroot>`, `<mstyle>`, `<merror>`, `<mpadded>`, `<mphantom>`, `<mfenced>`, `<menclose>`, `<msub>`, `<msup>`, `<msubsup>`, `<munder>`, `<mover>`, `<munderover>`, `<mmultiscripts>`, `<mtable>`, `<mtr>`, `<mtd>`, `<maligngroup>`, `<malignmark>`, and `annotation`. Also add the required MathML attributes (e.g. `xmlns`, `display`, `mathvariant`) to `permittedTagToAttributes`.
|
||||||
|
- **Parser (`src/app/plugins/react-custom-html-parser.tsx`):** Detect `$ ... $` and `$$ ... $$` patterns in text nodes:
|
||||||
|
```tsx
|
||||||
|
if (node.type === 'text') {
|
||||||
|
const parts = node.data.split(/(\$\$.*?\$\$|\$.*?\$)/g);
|
||||||
|
return parts.map((p) => {
|
||||||
|
if (p.startsWith('$')) return <KaTeX math={p.replace(/\$/g, '')} />;
|
||||||
|
return p;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
```
|
||||||
|
- **CSS (`src/app/styles/CustomHtml.css.ts`):** Import `katex/dist/katex.min.css` only when a math block is rendered to save initial bundle size.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### P4-6 · OIDC / SSO Next-Gen Auth (MSC3861)
|
||||||
|
|
||||||
|
**Mechanism:** Matrix Authentication Service (MAS) Integration.
|
||||||
|
|
||||||
|
- **Architecture:** Shift from password-based `/login` to OAuth2 `authorization_code` flow.
|
||||||
|
- **Key Files:** `src/app/pages/auth/Login.tsx` and `src/app/hooks/useAuth.ts`.
|
||||||
|
- **Implementation:** Use `oidc-client-ts` or a similar lightweight OIDC library. Check for `m.authentication` in `/.well-known/matrix/client`. Redirect to the MAS authorization endpoint. Handle the callback in a new `OidcCallback` route and store the OIDC `refresh_token`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### P5-1 · Custom Accent Color Picker (Non-TDS only)
|
||||||
|
|
||||||
|
**Mechanism:** Dynamic CSS variable injection.
|
||||||
|
|
||||||
|
- **Setting (`src/app/state/settings.ts`):** Add `customAccentColor: string` (hex).
|
||||||
|
- **Manager (`src/app/pages/ThemeManager.tsx`):** Inside the `useEffect` that monitors theme changes:
|
||||||
|
```typescript
|
||||||
|
if (!lotusTerminal && customAccentColor) {
|
||||||
|
document.documentElement.style.setProperty('--lt-accent-orange', customAccentColor);
|
||||||
|
document.documentElement.style.setProperty('--lt-accent-orange-glow', `${customAccentColor}80`);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
- **UI (`src/app/features/settings/general/General.tsx`):** Use `<Input type="color">`. Hide this section if `lotusTerminal` is `true`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### P5-15 · In-Call Soundboard
|
||||||
|
|
||||||
|
**Mechanism:** Local-to-Global Audio Bridge via Web Audio API.
|
||||||
|
|
||||||
|
- Create an `AudioContext` and a `MediaStreamDestinationNode`.
|
||||||
|
- Create an `AudioBufferSourceNode` for each clip.
|
||||||
|
- Route the mic `MediaStream` and the clip source to the destination node.
|
||||||
|
- Pass the destination's `.stream` to the call bridge.
|
||||||
|
|
||||||
|
> ⚠️ **[Gemini_Found — CORRECTED]** Gemini originally suggested using LiveKit's `LocalAudioTrack.replaceTrack()` to mix audio into the call stream. This is **not possible** from Lotus Chat's realm: Element Call runs in a **cross-origin iframe** controlled via `matrix-widget-api` (postMessage). LiveKit's JS SDK and its `LocalAudioTrack` live inside EC's sandboxed context — inaccessible from our code. This directly contradicts the confirmed constraint already listed in the Server Capabilities table: _"Cindy CANNOT inject audio into EC call stream — In-call soundboard must be redesigned as local-only."_ The soundboard must be a local-playback-only feature (output through the user's speakers, not mixed into the call audio stream).
|
||||||
|
>
|
||||||
|
> 🔱 **[EC-FORK — partial correction]** The "cross-origin" claim above is **outdated**: EC is now **same-origin** / self-hosted (`iframe.sandbox` has `allow-same-origin`; we read `contentDocument`). The _practical_ blocker still holds — LiveKit's `LocalAudioTrack` lives in EC's **module scope** (not on `window`), so it's unreachable from cinny even same-origin. **Owning the EC source** (see [`HANDOFF_ELEMENT_CALL_FORK.md`](./HANDOFF_ELEMENT_CALL_FORK.md)) is the path to a real call-audio-inject API, which would unblock a true in-call soundboard.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### P5-20 · Quick Reply from Browser Notification
|
||||||
|
|
||||||
|
**Mechanism:** Service Worker `notificationclick` Action.
|
||||||
|
|
||||||
|
> [Gemini_Found] Implementation detail: `serviceWorkerRegistration.showNotification()` should be used instead of `new Notification()` so that the service worker can listen to the `notificationclick` event. `new Notification()` creates notifications that are bound to the client page, not the SW.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// src/sw.ts
|
||||||
|
self.addEventListener('notificationclick', (event) => {
|
||||||
|
if (event.action === 'reply' && event.reply) {
|
||||||
|
const { roomId, threadId } = event.notification.data;
|
||||||
|
const session = sessions.get(event.clientId);
|
||||||
|
fetch(`${session.baseUrl}/_matrix/client/v3/rooms/${roomId}/send/m.room.message`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { Authorization: `Bearer ${session.accessToken}` },
|
||||||
|
body: JSON.stringify({
|
||||||
|
msgtype: 'm.text',
|
||||||
|
body: event.reply,
|
||||||
|
'm.relates_to': threadId ? { rel_type: 'm.thread', event_id: threadId } : undefined,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### P5-30 · Advanced ML Noise Suppression — Model Roadmap
|
||||||
|
|
||||||
|
See shipped implementation in LOTUS_FEATURES.md → "Noise Suppression (Advanced Multi-Tier)".
|
||||||
|
|
||||||
|
**Models status:**
|
||||||
|
|
||||||
|
- **RNNoise** (sapphi, 48 kHz) — ✅ working, default fallback. Keep — runs on any hardware.
|
||||||
|
- **Speex** (sapphi, 48 kHz) — ✅ working, low value; candidate to drop.
|
||||||
|
- **DTLN** (@workadventure, 16 kHz) — 🟡 wired; sample-rate fix applied (was robotic at 48 kHz). **TODO: verify in a real call.** Narrowband (16 kHz) = slightly telephone-y even when correct.
|
||||||
|
|
||||||
|
**Constraints:** client-side AudioWorklet, fully self-hosted, no GPU, self-hosted SFU (no LiveKit Cloud).
|
||||||
|
|
||||||
|
**Roadmap:**
|
||||||
|
|
||||||
|
- [ ] Verify DTLN 16 kHz fix in a real call.
|
||||||
|
- [ ] **DeepFilterNet 3** — best self-hostable upgrade: Rust→WASM, CPU real-time, 48 kHz fullband. Self-host `df_bg.wasm` + DFN3 ONNX model; wire a 48 kHz worklet. Audio quality unverifiable without a real-call test.
|
||||||
|
- [ ] **Desktop-only / HW-gated:** FRCRN (Alibaba) or NVIDIA Maxine (RTX/Tensor only). Runs in Tauri Rust backend + bridges a virtual mic into the webview. Must detect capability; web + weak HW falls back to RNNoise/DTLN.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### P5-31 · Granular Voice & Screenshare Quality Controls
|
||||||
|
|
||||||
|
**Mechanism:** WebRTC Encoding Parameters + Backend Quality Guard.
|
||||||
|
|
||||||
|
- **State Event:** `io.lotus.room_quality` (state key `""`) containing:
|
||||||
|
```json
|
||||||
|
{ "audio_bitrate": 128000, "screen_max_res": "1080p", "screen_max_fps": 60 }
|
||||||
|
```
|
||||||
|
- **Screenshare:** In `src/app/plugins/call/CallControl.ts`, map the "Quality" setting to `getDisplayMedia` constraints.
|
||||||
|
- **Audio Bitrate:** After the call joins, find the `RTCRtpSender` for the audio track:
|
||||||
|
```typescript
|
||||||
|
const sender = peerConnection.getSenders().find((s) => s.track?.kind === 'audio');
|
||||||
|
const params = sender.getParameters();
|
||||||
|
params.encodings[0].maxBitrate = roomBitrate || 128000;
|
||||||
|
await sender.setParameters(params);
|
||||||
|
```
|
||||||
|
- **Backend Sidecar:** Extend `voice-limit-guard.py` (LXC 151) to fetch `io.lotus.room_quality` and inject limits into the LiveKit JWT or return them as an authorized config packet.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### P5-40 · Desktop — Proactive Update Notifications (Tauri)
|
||||||
|
|
||||||
|
**Key Files:** `src/app/hooks/useTauriUpdater.ts`, `src/app/pages/client/ClientNonUIFeatures.tsx`, `src/app/features/toast/LotusToastContainer.tsx`.
|
||||||
|
|
||||||
|
1. Create a `TauriUpdateFeature` component. Use `useTauriUpdater()` to get the `check` function and `status`.
|
||||||
|
2. In a `useEffect`, call `check()` on mount and then on a `setInterval` (every 12 hours).
|
||||||
|
3. When status transitions to `{ state: 'available', version: '...' }`, fire a Lotus Toast: "Lotus Chat v[version] is available!" with an "Update" button that calls `install()`.
|
||||||
|
4. Store `lastCheck` timestamp in `localStorage` to prevent redundant checks on refresh.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Mobile Bookmarks Visibility Fix
|
||||||
|
|
||||||
|
**Issue:** `ClientLayout.tsx` explicitly restricts `BookmarksPanel` to `ScreenSize.Desktop` (lines 51-56).
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
// ClientLayout.tsx
|
||||||
|
{
|
||||||
|
bookmarksOpen && (
|
||||||
|
<BookmarksPanel
|
||||||
|
onClose={() => setBookmarksOpen(false)}
|
||||||
|
isMobile={screenSize !== ScreenSize.Desktop}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`BookmarksPanel.tsx` already supports the `isMobile` prop (line 127) to enable full-screen absolute positioning. No other changes required.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Remind Me Later (Slack-style)
|
||||||
|
|
||||||
|
**Mechanism:** Account Data + Timer/Service Worker.
|
||||||
|
|
||||||
|
- **Storage (`src/app/hooks/useReminders.ts`):** Store in account data `io.lotus.reminders` as `Array<{ id: string, roomId: string, eventId: string, timestamp: number }>`.
|
||||||
|
- **Context Menu (`src/app/features/room/message/MessageContextMenu.tsx`):** Add "Remind me" option → opens date/time picker modal (reuse `JumpToTime.tsx` logic).
|
||||||
|
- **Trigger (foreground):** `setTimeout` in a hook inside `ReminderMonitor` in `ClientNonUIFeatures.tsx` → pushes to `toastQueueAtom` in `state/toast.ts` when due.
|
||||||
|
- **Trigger (background):** Use Service Worker — `setTimeout` in the main thread will not fire when the PWA is suspended.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Mobile Usability Audit — Methodology
|
||||||
|
|
||||||
|
1. **Viewport & Touch:** All interactive elements must have at least `44px × 44px` touch targets. Audit for horizontal overflow (horizontal scrolling must be disabled).
|
||||||
|
2. **Modal Responsiveness:** All modals (Settings, Profile, etc.) MUST cover the full screen on mobile, not float as overlays.
|
||||||
|
3. **Sidebar / Panels:** On mobile, sidebar panels (Members, Bookmarks, Media) must become full-screen overlays (using a `Drawer` or `Modal` pattern) rather than side-by-side flexbox panels.
|
||||||
|
4. **Input & Composer:** Ensure the composer doesn't get obscured by the mobile keyboard. Test focus trap and blur behaviors.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Implementation Notes
|
||||||
|
|
||||||
|
### ⚠️ TDS DESIGN LAW (repeated here for emphasis)
|
||||||
|
|
||||||
|
> Every TDS color, animation, glow, border, shadow, and font value MUST come from `/root/code/web_template/base.css`.
|
||||||
|
> Never hardcode hex values. Never invent CSS variable names.
|
||||||
|
> Key variables: `--lt-accent-orange` · `--lt-accent-cyan` · `--lt-accent-green` · `--lt-glow-*` · `--lt-box-glow-*` · `--lt-border-color` · `--lt-font-mono`
|
||||||
|
> Reference implementation: `/root/code/tinker_tickets/` (markdown.js, base.js, ticket.css)
|
||||||
|
> This applies without exception to every task marked `[IMPROVE]`, `[Build]`, or any UI change.
|
||||||
|
|
||||||
|
### Design Rules
|
||||||
|
|
||||||
|
- All new components must respect both TDS dark (`LotusTerminalTheme`) and TDS light (`LotusTerminalLightTheme`) modes
|
||||||
|
- Non-TDS theme work (custom accent color, theme presets) uses vanilla-extract theme files — match the pattern in `src/lotus-terminal.css.ts`
|
||||||
|
- Code syntax highlighting token classes: `.tok-kw .tok-str .tok-num .tok-cmt .tok-fn` (defined in `web_template/base.css`)
|
||||||
|
- `folds AvatarImage` does NOT accept children — wrap Avatar components externally for overlays/frames/borders
|
||||||
|
|
||||||
|
### CI/CD Pipeline
|
||||||
|
|
||||||
|
```
|
||||||
|
edit → commit → git push origin lotus
|
||||||
|
→ Gitea Actions: tsc --noEmit, eslint, prettier (~3 min)
|
||||||
|
→ Webhook: lotus_deploy.sh on LXC 106 polls CI, then npm ci && npm run build → rsync
|
||||||
|
→ Live at chat.lotusguild.org (~11 min total)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Per-Feature Checklist (before marking complete)
|
||||||
|
|
||||||
|
- [ ] `npx tsc --noEmit` — zero TypeScript errors
|
||||||
|
- [ ] `npx eslint src/` — zero new errors (warnings OK if pre-existing)
|
||||||
|
- [ ] `npx prettier --check src/` — formatting passes
|
||||||
|
- [ ] `README.md` updated (Lotus-custom features only — not upstream Cinny features)
|
||||||
|
- [ ] `landing/index.html` updated if the feature appears in the comparison table
|
||||||
|
- [ ] Visually tested at `chat.lotusguild.org` after CI deploys
|
||||||
|
|
||||||
|
### Homeserver Access (for server audits)
|
||||||
|
|
||||||
|
- **Synapse (Matrix):** LXC 151 on `compute-storage-01` — `pct exec 151 -- bash`
|
||||||
|
- **Config:** `/etc/matrix-synapse/homeserver.yaml`
|
||||||
|
- **Version check:** `curl -s https://matrix.lotusguild.org/_matrix/client/versions`
|
||||||
@@ -1,115 +1,180 @@
|
|||||||
# Cinny
|
# Lotus Chat
|
||||||
<p>
|
|
||||||
<a href="https://github.com/ajbura/cinny/releases">
|
|
||||||
<img alt="GitHub release downloads" src="https://img.shields.io/github/downloads/ajbura/cinny/total?logo=github&style=social"></a>
|
|
||||||
<a href="https://hub.docker.com/r/ajbura/cinny">
|
|
||||||
<img alt="DockerHub downloads" src="https://img.shields.io/docker/pulls/ajbura/cinny?logo=docker&style=social"></a>
|
|
||||||
<a href="https://fosstodon.org/@cinnyapp">
|
|
||||||
<img alt="Follow on Mastodon" src="https://img.shields.io/mastodon/follow/106845779685925461?domain=https%3A%2F%2Ffosstodon.org&logo=mastodon&style=social"></a>
|
|
||||||
<a href="https://twitter.com/intent/follow?screen_name=cinnyapp">
|
|
||||||
<img alt="Follow on Twitter" src="https://img.shields.io/twitter/follow/cinnyapp?logo=twitter&style=social"></a>
|
|
||||||
<a href="https://cinny.in/#sponsor">
|
|
||||||
<img alt="Sponsor Cinny" src="https://img.shields.io/opencollective/all/cinny?logo=opencollective&style=social"></a>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
A Matrix client focusing primarily on simple, elegant and secure interface. The main goal is to have an instant messaging application that is easy on people and has a modern touch.
|
A Matrix chat client built for Lotus Guild — fast, private, and packed with the features you actually want.
|
||||||
- [Roadmap](https://github.com/orgs/cinnyapp/projects/1)
|
|
||||||
- [Contributing](./CONTRIBUTING.md)
|
|
||||||
|
|
||||||
> [!IMPORTANT]
|
**Deployed at [chat.lotusguild.org](https://chat.lotusguild.org)** | Forked from [Cinny](https://github.com/cinnyapp/cinny), synced through v4.12.3
|
||||||
We are currently in the [process of replacing the matrix-js-sdk](https://github.com/cinnyapp/cinny/issues/257#issuecomment-3714406704) with our own SDK. As a result, we will not be accepting any pull requests until further notice.
|
|
||||||
Thank you for your understanding.
|
|
||||||
|
|
||||||
<img align="center" src="https://raw.githubusercontent.com/cinnyapp/cinny-site/main/assets/preview2-light.png" height="380">
|
---
|
||||||
|
|
||||||
## Getting started
|
## Licensing & Attribution
|
||||||
The web app is available at [app.cinny.in](https://app.cinny.in/) and gets updated on each new release. The `dev` branch is continuously deployed at [dev.cinny.in](https://dev.cinny.in) but keep in mind that it could have things broken.
|
|
||||||
|
|
||||||
You can also download our desktop app from the [cinny-desktop repository](https://github.com/cinnyapp/cinny-desktop).
|
The source code is licensed under [AGPLv3](LICENSE), the same license as the upstream Cinny project. The source for this fork is public at [code.lotusguild.org/LotusGuild/cinny](https://code.lotusguild.org/LotusGuild/cinny).
|
||||||
|
|
||||||
## Self-hosting
|
The Lotus Chat logo (`public/res/Lotus.png`) is a derivative work based on the original Cinny logo by Ajay Bura and contributors, used under [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/). The modified logo is © Lotus Guild and is also made available under CC BY 4.0.
|
||||||
To host Cinny on your own, simply download the tarball from [GitHub releases](https://github.com/cinnyapp/cinny/releases/latest), and serve the files from `dist/` using your preferred webserver. Alternatively, you can just pull the docker image from [DockerHub](https://hub.docker.com/r/ajbura/cinny) or [GitHub Container Registry](https://github.com/cinnyapp/cinny/pkgs/container/cinny).
|
|
||||||
|
|
||||||
* The default homeservers and explore pages are defined in [`config.json`](config.json).
|
---
|
||||||
|
|
||||||
* You need to set up redirects to serve the assests. Example configurations; [netlify](netlify.toml), [nginx](contrib/nginx/cinny.domain.tld.conf), [caddy](contrib/caddy/caddyfile).
|
## Features
|
||||||
* If you have trouble configuring redirects you can [enable hash routing](config.json#L35) — the url in the browser will have a `/#/` between the domain and open channel (ie. `app.cinny.in/#/home/` instead of `app.cinny.in/home/`) but you won't have to configure your webserver.
|
|
||||||
|
|
||||||
* To deploy on subdirectory, you need to rebuild the app youself after updating the `base` path in [`build.config.ts`](build.config.ts).
|
### Messaging
|
||||||
* For example, if you want to deploy on `https://cinny.in/app`, then set `base: '/app'`.
|
|
||||||
|
|
||||||
<details><summary><b>PGP Public Key to verify tarball</b></summary>
|
- See who has read each message, and track delivery status (sending / sent / failed)
|
||||||
|
- Bookmark any message and revisit saved messages from the sidebar
|
||||||
|
- Schedule messages to send at a specific time
|
||||||
|
- Click "edited" on any message to see the full edit history
|
||||||
|
- Drafts are saved automatically and survive page reloads
|
||||||
|
- Long messages collapse automatically — click "Read more" to expand
|
||||||
|
- Forward messages to other rooms
|
||||||
|
- Create and view polls directly in chat
|
||||||
|
- Share your location with an inline map embed
|
||||||
|
- Add captions to image and video uploads
|
||||||
|
- Optionally compress images before uploading — shows before/after file sizes
|
||||||
|
- GIF links from Giphy and Tenor auto-preview inline
|
||||||
|
- Search for and send GIFs from a built-in GIF picker
|
||||||
|
- Control voice message playback speed: 0.75× / 1× / 1.5× / 2×
|
||||||
|
- Search messages with a date range filter
|
||||||
|
- Room topics support rich formatting (bold, links, italics)
|
||||||
|
- Deleted messages show a placeholder instead of disappearing
|
||||||
|
- Code blocks highlight syntax for JS/TS, Python, and Rust
|
||||||
|
- Rich link preview cards for YouTube, GitHub, Twitter/X, Reddit, Spotify, Twitch, Steam, Wikipedia, Discord, npm, Stack Overflow, and IMDb
|
||||||
|
|
||||||
```
|
### Calls & Voice
|
||||||
-----BEGIN PGP PUBLIC KEY BLOCK-----
|
|
||||||
|
|
||||||
mQGNBGJw/g0BDAC8qQeLqDMzYzfPyOmRlHVEoguVTo+eo1aVdQH2X7OELdjjBlyj
|
- Push to Talk with a configurable keybind (default: Space)
|
||||||
6d6c1adv/uF2g83NNMoQY7GEeHjRnXE4m8kYSaarb840pxrYUagDc0dAbJOGaCBY
|
- Push to Deafen with the M key
|
||||||
FKTo7U1Kvg0vdiaRuus0pvc1NVdXSxRNQbFXBSwduD+zn66TI3HfcEHNN62FG1cE
|
- Camera starts turned off by default when joining a call
|
||||||
K1jWDwLAU0P3kKmj8+CAc3h9ZklPu0k/+t5bf/LJkvdBJAUzGZpehbPL5f3u3BZ0
|
- Screenshare requires confirmation before going live
|
||||||
leZLIrR8uV7PiV5jKFahxlKR5KQHld8qQm+qVhYbUzpuMBGmh419I6UvTzxuRcvU
|
- Toggle noise suppression on or off
|
||||||
Frn9ttCEzV55Y+so4X2e4ZnB+5gOnNw+ecifGVdj/+UyWnqvqqDvLrEjjK890nLb
|
- Calls float in a draggable picture-in-picture window when you navigate away
|
||||||
Pil4siecNMEpiwAN6WSmKpWaCwQAHEGDVeZCc/kT0iYfj5FBcsTVqWiO6eaxkUlm
|
- Your chat background shows through the call view
|
||||||
jnulqWqRrlB8CJQQvih/g//uSEBdzIibo+ro+3Jpe120U/XVUH62i9HoRQEm6ADG
|
- Dark/light mode inside calls matches your Lotus Chat theme
|
||||||
4zS5hIq4xyA8fL8AEQEAAbQdQ2lubnlBcHAgPGNpbm55YXBwQGdtYWlsLmNvbT6J
|
- Calls are available in DMs and private groups only — no accidental mass rings
|
||||||
AdQEEwEIAD4CGwMFCwkIBwIGFQoJCAsCBBYCAwECHgECF4AWIQSRri2MHidaaZv+
|
- AFK auto-mute: mic is automatically silenced after a configurable idle timeout (1–30 min); a toast confirms the action
|
||||||
vvuUMwx6UK/M8wUCZqEDwAUJFvwIswAKCRCUMwx6UK/M877qC/4lxXOQIoWnLLkK
|
- Voice channel user limit: admins can cap how many people can be in a room's call — enforced server-side for every Matrix client (not just Lotus Chat); others see "Channel Full" until a spot opens
|
||||||
YiRCTkGsH6NdxgeYr6wpXT4xuQ45ZxCytwHpOGQmO/5up5961TxWW8D1frRIJHjj
|
- Custom join/leave sound effects when someone enters or leaves your call — choose Chime, Soft, Retro, or off
|
||||||
AZGoRCL3EKEuY8nt3D99fpf3DvZrs1uoVAhiyn737hRlZAg+QsJheeGCmdSJ0hX5
|
|
||||||
Yud8SE+9zxLS1+CEjMrsUd/RGre/phme+wNXfaHfREAC9ewolgVChPIbMxG2f+vs
|
|
||||||
K8Xv52BFng7ta9fgsl1XuOjpuaSbQv6g+4ONk/lxKF0SmnhEGM3dmIYPONxW47Yf
|
|
||||||
atnIjRra/YhPTNwrNBGMmG4IFKaOsMbjW/eakjWTWOVKKJNBMoDdRcYYWIMCpLy8
|
|
||||||
AQUrMtQEsHSnqCwrw818S5A6rrhcfVGk36RGm0nOy6LS5g5jmqaYsvbCcBGY9B2c
|
|
||||||
SUAVNm17oo7TtEajk8hcSXoZod1t++pyjcVKEmSn3nFK7v5m3V+cPhNTxZMK459P
|
|
||||||
3x1Ucqj/kTqrxKw6s2Uknuk0ajmw0ljV+BQwgL6maguo9BKgCNW5AY0EYnD+DQEM
|
|
||||||
ANOu/d6ZMF8bW+Df9RDCUQKytbaZfa+ZbIHBus7whCD/SQMOhPKntv3HX7SmMCs+
|
|
||||||
5i27kJMu4YN623JCS7hdCoXVO1R5kXCEcneW/rPBMDutaM472YvIWMIqK9Wwl5+0
|
|
||||||
Piu2N+uTkKhe9uS2u7eN+Khef3d7xfjGRxoppM+xI9dZO+jhYiy8LuC0oBohTjJq
|
|
||||||
QPqfGDpowBwRkkOsGz/XVcesJ1Pzg4bKivTS9kZjZSyT9RRSY8As0sVUN57AwYul
|
|
||||||
s1+eh00n/tVpi2Jj9pCm7S0csSXvXj8v2OTdK1jt4YjpzR0/rwh4+/xlOjDjZEqH
|
|
||||||
vMPhpzpbgnwkxZ3X8BFne9dJ3maC5zQ3LAeCP5m1W0hXzagYhfyjo74slJgD1O8c
|
|
||||||
LDf2Oxc5MyM8Y/UK497zfqSPfgT3NhQmhHzk83DjXw3I6Z3A3U+Jp61w0eBRI1nx
|
|
||||||
H1UIG+gldcAKUTcfwL0lghoT3nmi9JAbvek0Smhz00Bbo8/dx8vwQRxDUxlt7Exx
|
|
||||||
NwARAQABiQG8BBgBCAAmAhsMFiEEka4tjB4nWmmb/r77lDMMelCvzPMFAmahA9IF
|
|
||||||
CRb8CMUACgkQlDMMelCvzPPQgQv/d5/z+fxgKqgfhQX+V49X4WgTVxZ/CzztDoJ1
|
|
||||||
XAq1dzTNEy8AFguXIo6eVXPSpMxec7ZreN3+UPQBnCf3eR5YxWNYOYKmk0G4E8D2
|
|
||||||
KGUJept7TSA42/8N2ov6tToXFg4CgzKZj0fYLwgutly7K8eiWmSU6ptaO8aEQBHB
|
|
||||||
gTGIOO3h6vJMGVycmoeRnHjv4wV84YWSVFSoJ7cY0he4Z9UznJBbE/KHZjrkXsPo
|
|
||||||
N+Gg5lDuOP5xjKzM5SogV9lhxBAhMWAg3URUF15yruZBiA8uV1FOK8sal/9C1G7V
|
|
||||||
M6ygA6uOZqXlZtcdA94RoSsW2pZ9eLVPsxz2B3Zko7tu11MpNP/wYmfGTI3KxZBj
|
|
||||||
n/eodvwjJSgHpGOFSmbNzvPJo3to5nNlp7wH1KxIMc6Uuu9hgfDfwkFZgV2bnFIa
|
|
||||||
Q6gyF548Ub48z7Dz83+WwLgbX19ve4oZx+dqSdczP6ILHRQomtrzrkkP2LU52oI5
|
|
||||||
mxFo+ioe/ABCufSmyqFye0psX3Sp
|
|
||||||
=WtqZ
|
|
||||||
-----END PGP PUBLIC KEY BLOCK-----
|
|
||||||
```
|
|
||||||
</details>
|
|
||||||
|
|
||||||
## Local development
|
### Customization & Appearance
|
||||||
> [!TIP]
|
|
||||||
> We recommend using a version manager as versions change very quickly. You will likely need to switch between multiple Node.js versions based on the needs of different projects you're working on. [NVM on windows](https://github.com/coreybutler/nvm-windows#installation--upgrades) on Windows and [nvm](https://github.com/nvm-sh/nvm) on Linux/macOS are pretty good choices. Recommended nodejs version is Krypton LTS (v24.13.1).
|
|
||||||
|
|
||||||
Execute the following commands to start a development server:
|
- LotusGuild Terminal Design System (TDS) — a CRT terminal-inspired dark theme
|
||||||
```sh
|
- TDS light mode variant for daytime use
|
||||||
npm ci # Installs all dependencies
|
- 20+ static chat background patterns
|
||||||
npm start # Serve a development version
|
- 5 animated chat backgrounds: Digital Rain, Star Drift, Grid Pulse, Aurora Flow, Fireflies (with improved per-layer looping, phosphor-flicker rain, fluid aurora sweep, and organic firefly bioluminescence)
|
||||||
|
- 11 seasonal & holiday theme overlays — Halloween, Christmas, New Year, Autumn, Valentine's Day, St. Patrick's Day, Earth Day, Lunar New Year, April Fools', Deep Space, and Retro Arcade; auto-selected by date with a manual override in Settings → Appearance
|
||||||
|
- Avatar decorations — 99 animated APNG overlays (Gaming, Cyber, Space, Fantasy, Nature, Spooky, Cozy, and more) that frame your avatar across the timeline, members list, and @mention autocomplete; visible to all Lotus Chat users; select in Settings → Account → Avatar Decoration
|
||||||
|
- Toggle to pause background animations
|
||||||
|
- Glassmorphism sidebar — frosted glass effect that lets the background show through
|
||||||
|
- Night Light / blue light filter with an adjustable intensity slider
|
||||||
|
- Emoji prefixes on room names render larger in the sidebar (e.g. 🎮 general)
|
||||||
|
- Rename any room for yourself only — other members see the original name
|
||||||
|
- Emoji picker on all room name inputs
|
||||||
|
|
||||||
|
### Presence & Profile
|
||||||
|
|
||||||
|
- Discord-style presence selector: Online, Idle, Do Not Disturb, Invisible, or Auto
|
||||||
|
- Custom status message with emoji and an optional auto-clear timer (changing your status is never silently overwritten by activity events)
|
||||||
|
- Colored presence ring on member avatars (green / yellow / red)
|
||||||
|
- Profile fields for pronouns and timezone
|
||||||
|
- When a user's timezone is set, their current local time appears in their profile
|
||||||
|
- Private notes on any user's profile — freeform text visible only to you, auto-saves and syncs across devices
|
||||||
|
- Unread count shown in the browser tab title
|
||||||
|
|
||||||
|
### Moderation & Privacy
|
||||||
|
|
||||||
|
- Report any room to homeserver admins from the room menu
|
||||||
|
- View policy lists and ban lists (Draupnir-compatible, read-only)
|
||||||
|
- Toggle private read receipts so others can't see when you've read messages
|
||||||
|
- Optional warning when an encrypted room contains unverified devices
|
||||||
|
- Full push rule editor in notification settings
|
||||||
|
- View and edit Server ACL rules in room settings
|
||||||
|
- Filterable room activity / mod log (joins, kicks, bans, power level changes, etc.)
|
||||||
|
- Room stats and insights panel (active members, top reactions, media breakdown, activity heatmap)
|
||||||
|
- Export room history as plain text, JSON, or HTML with optional date range filter
|
||||||
|
|
||||||
|
### Notifications
|
||||||
|
|
||||||
|
- In-app toast notifications appear bottom-right when the window is focused
|
||||||
|
- Custom notification sounds per category (messages, invites)
|
||||||
|
- Quiet hours — suppress notifications during a configured time window
|
||||||
|
- Click a toast to jump directly to the room or DM
|
||||||
|
|
||||||
|
### UX
|
||||||
|
|
||||||
|
- Filter and search rooms in the sidebar
|
||||||
|
- Favorite rooms sync across devices and appear in a pinned section
|
||||||
|
- Sort rooms by recent activity, alphabetical, or unread first
|
||||||
|
- DM rows show a message preview and relative timestamp
|
||||||
|
- Right-click a room for a context menu: mute with duration, copy link, mark as read
|
||||||
|
- Quick emoji reactions appear on message hover — one click to react
|
||||||
|
- Knock-to-join: request access to a room; admins approve or deny from the members list
|
||||||
|
- Media gallery drawer: browse all images, videos, and files shared in a room
|
||||||
|
- Invite link and QR code in room settings
|
||||||
|
- Pending knock requests shown in the members list for room admins with a live badge count on the Members button
|
||||||
|
- Homeserver support contact displayed in Help & About (MSC1929)
|
||||||
|
- Server notice rooms are visually distinct from regular DMs
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Desktop App
|
||||||
|
|
||||||
|
Lotus Chat has a desktop app for Windows, macOS, and Linux. It wraps the same web client in a native window with automatic background updates — no need to reinstall for new versions.
|
||||||
|
|
||||||
|
### Download
|
||||||
|
|
||||||
|
Download the latest release from the [Releases page on code.lotusguild.org](https://code.lotusguild.org).
|
||||||
|
|
||||||
|
### SmartScreen Warning (Windows)
|
||||||
|
|
||||||
|
When you first run the installer on Windows, you may see a popup that says **"Windows protected your PC"** with the app listed as an unknown publisher. This is normal.
|
||||||
|
|
||||||
|
**Why it happens:** Windows SmartScreen flags any app that does not have an expensive commercial code-signing certificate from a major CA. Lotus Chat is signed with its own key for update verification, but that key is not in Microsoft's pre-approved list.
|
||||||
|
|
||||||
|
**How to install anyway:**
|
||||||
|
|
||||||
|
1. Click **"More info"** in the SmartScreen dialog.
|
||||||
|
2. A **"Run anyway"** button will appear.
|
||||||
|
3. Click it to proceed with installation.
|
||||||
|
|
||||||
|
After the first install, automatic in-app updates handle all future versions — you will not see this prompt again for updates.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## For Developers
|
||||||
|
|
||||||
|
The source code lives in `/root/code/cinny`. All changes should be made on the `lotus` branch. Push to `origin/lotus` and CI will automatically build and deploy to [chat.lotusguild.org](https://chat.lotusguild.org) in approximately 11 minutes — no manual build or deploy steps required.
|
||||||
|
|
||||||
|
See [LOTUS_FEATURES.md](LOTUS_FEATURES.md) for the full feature changelog and [LOTUS_TODO.md](LOTUS_TODO.md) for the work backlog.
|
||||||
|
|
||||||
|
### 🔱 Planned: Element Call fork ("Lotus Call")
|
||||||
|
|
||||||
|
Voice/video channels embed **Element Call**. Today it's a **pre-built npm bundle**
|
||||||
|
(`@element-hq/element-call-embedded` 0.20.1) copied to `public/element-call/` and
|
||||||
|
served same-origin; we steer it via the `matrix-widget-api` plus fragile DOM
|
||||||
|
hacks. Because we don't own its compiled source, several in-call issues (avatar
|
||||||
|
decorations on tiles, camera focus/fullscreen during screenshare, mic recovery
|
||||||
|
after reconnect, native theming, real call-audio injection) are unfixable from
|
||||||
|
outside.
|
||||||
|
|
||||||
|
**The plan is to fork `element-hq/element-call` into a new `LotusGuild/element-call`
|
||||||
|
repo, build it from source, and host our own build** for true ownership. The full
|
||||||
|
self-contained plan and integration map — written for a fresh session with no
|
||||||
|
prior context — is in **[`HANDOFF_ELEMENT_CALL_FORK.md`](HANDOFF_ELEMENT_CALL_FORK.md)**.
|
||||||
|
Infra/hosting notes also live in the `LotusGuild/matrix` repo README. Search the
|
||||||
|
docs for the **`[EC-FORK]`** tag to find every related note.
|
||||||
|
|
||||||
|
### Build
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm ci && npm run build # outputs to dist/
|
||||||
```
|
```
|
||||||
|
|
||||||
To build the app:
|
If the build is killed due to out-of-memory:
|
||||||
```sh
|
|
||||||
npm run build # Compiles the app into the dist/ directory
|
```bash
|
||||||
|
NODE_OPTIONS=--max_old_space_size=6144 npm run build
|
||||||
```
|
```
|
||||||
|
|
||||||
### Running with Docker
|
### CI/CD
|
||||||
This repository includes a Dockerfile, which builds the application from source and serves it with Nginx on port 80. To
|
|
||||||
use this locally, you can build the container like so:
|
|
||||||
```
|
|
||||||
docker build -t cinny:latest .
|
|
||||||
```
|
|
||||||
|
|
||||||
You can then run the container you've built with a command similar to this:
|
|
||||||
```
|
```
|
||||||
docker run -p 8080:80 cinny:latest
|
edit → commit → git push → ~11 min → live at chat.lotusguild.org
|
||||||
```
|
```
|
||||||
|
|
||||||
This will forward your `localhost` port 8080 to the container's port 80. You can visit the app in your browser by navigating to `http://localhost:8080`.
|
|
||||||
|
|||||||
@@ -0,0 +1,357 @@
|
|||||||
|
/*
|
||||||
|
* Lotus Chat — client-side ML noise suppression shim for Element Call.
|
||||||
|
*
|
||||||
|
* Element Call runs as a same-origin iframe widget that captures the mic
|
||||||
|
* internally (via livekit-client -> getUserMedia) and publishes it to LiveKit.
|
||||||
|
* We can't reach that track from the host. Instead this classic <script> is
|
||||||
|
* injected (by the vite `lotus-denoise` plugin) into EC's index.html BEFORE its
|
||||||
|
* deferred module entry, so it runs first and monkeypatches getUserMedia. When
|
||||||
|
* the "ml" tier is selected (lotusDenoise=ml in the widget URL) we route the
|
||||||
|
* captured mic through an RNNoise AudioWorklet (@sapphi-red/web-noise-suppressor)
|
||||||
|
* and hand the processed track back to EC/LiveKit.
|
||||||
|
*
|
||||||
|
* RNNoise REQUIRES mono, 48 kHz float audio. Feeding it anything else (stereo,
|
||||||
|
* or 44.1 kHz data the model treats as 48 kHz) produces loud static. So we:
|
||||||
|
* - run a 48 kHz AudioContext (which handles resampling from the hardware),
|
||||||
|
* - use the SIMD build if supported for better performance,
|
||||||
|
* - keep browser-native stationary suppression ON so the fans are removed
|
||||||
|
* before RNNoise focuses on transient noises (keyboard, dogs, etc.).
|
||||||
|
*
|
||||||
|
* Any failure falls back to the unprocessed mic so calls never break.
|
||||||
|
*/
|
||||||
|
(function () {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
var params;
|
||||||
|
try {
|
||||||
|
params = new URLSearchParams(window.location.search);
|
||||||
|
if (params.get('lotusDenoise') !== 'ml') return;
|
||||||
|
} catch (e) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Derive the parent origin for postMessage targetOrigin from the parentUrl
|
||||||
|
// widget param (a full URL) so denoise-status messages aren't broadcast with
|
||||||
|
// '*'. Fall back to this frame's own origin if parentUrl is missing/malformed.
|
||||||
|
var targetOrigin;
|
||||||
|
try {
|
||||||
|
var parentUrl = params.get('parentUrl');
|
||||||
|
targetOrigin = parentUrl ? new URL(parentUrl).origin : window.location.origin;
|
||||||
|
} catch (e) {
|
||||||
|
targetOrigin = window.location.origin;
|
||||||
|
}
|
||||||
|
|
||||||
|
var md = navigator.mediaDevices;
|
||||||
|
if (!md || typeof md.getUserMedia !== 'function') return;
|
||||||
|
if (typeof AudioWorkletNode === 'undefined' || typeof AudioContext === 'undefined') return;
|
||||||
|
|
||||||
|
var ASSET_BASE = './denoise/';
|
||||||
|
|
||||||
|
var MODEL = params.get('lotusModel') || 'rnnoise';
|
||||||
|
// DTLN (@workadventure) targets 16 kHz and does not resample internally, so
|
||||||
|
// its whole graph runs in a 16 kHz context; RNNoise/Speex (sapphi) and
|
||||||
|
// DeepFilterNet 3 are 48 kHz fullband. The processed MediaStreamTrack is
|
||||||
|
// published to LiveKit either way (WebRTC/Opus resamples as needed).
|
||||||
|
var SAMPLE_RATE = MODEL === 'dtln' ? 16000 : 48000;
|
||||||
|
var USE_NATIVE_NS = params.get('lotusNativeNS') === 'true';
|
||||||
|
var USE_GATE = params.get('lotusGate') === 'true';
|
||||||
|
var GATE_THRESHOLD = parseFloat(params.get('lotusGateThreshold') || '-45');
|
||||||
|
|
||||||
|
var PROCESSORS = {
|
||||||
|
rnnoise: {
|
||||||
|
name: '@sapphi-red/web-noise-suppressor/rnnoise',
|
||||||
|
script: 'rnnoiseWorklet.js',
|
||||||
|
wasm: 'rnnoise.wasm',
|
||||||
|
simdWasm: 'rnnoise_simd.wasm',
|
||||||
|
},
|
||||||
|
speex: {
|
||||||
|
name: '@sapphi-red/web-noise-suppressor/speex',
|
||||||
|
script: 'speexWorklet.js',
|
||||||
|
wasm: 'speex.wasm',
|
||||||
|
},
|
||||||
|
dtln: {
|
||||||
|
// @workadventure/noise-suppression is a self-contained ES module that
|
||||||
|
// resolves its own AudioWorklet processor + LiteRT WASM + TFLite models
|
||||||
|
// via import.meta.url. We dynamic-import this helper and let it build the
|
||||||
|
// node, rather than addModule-ing a flat worklet ourselves.
|
||||||
|
helper: 'workadventure/audio-worklet.js',
|
||||||
|
},
|
||||||
|
deepfilternet: {
|
||||||
|
// deepfilternet3-noise-filter ships an ESM whose AudioWorklet processor +
|
||||||
|
// wasm-bindgen glue are INLINED as a string (loaded via a Blob URL — no
|
||||||
|
// CDN for the worklet). The only assets it fetches are its single-threaded
|
||||||
|
// df_bg.wasm + ONNX model, which we vendor + self-host under
|
||||||
|
// deepfilternet/v2/... We dynamic-import the ESM, build a DeepFilterNet3Core
|
||||||
|
// pointed at the self-hosted base, and let it create the worklet node.
|
||||||
|
esm: 'deepfilternet/index.esm.js',
|
||||||
|
},
|
||||||
|
gate: {
|
||||||
|
name: '@sapphi-red/web-noise-suppressor/noise-gate',
|
||||||
|
script: 'noiseGateWorklet.js',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
var origGetUserMedia = md.getUserMedia.bind(md);
|
||||||
|
var wasmPromises = {};
|
||||||
|
var ctxPromise = null;
|
||||||
|
|
||||||
|
function checkSimd() {
|
||||||
|
try {
|
||||||
|
return WebAssembly.validate(
|
||||||
|
new Uint8Array([
|
||||||
|
0, 97, 115, 109, 1, 0, 0, 0, 1, 5, 1, 96, 0, 1, 123, 3, 2, 1, 0, 10, 10, 1, 8, 0, 65, 0,
|
||||||
|
253, 15, 253, 98, 11,
|
||||||
|
]),
|
||||||
|
)
|
||||||
|
? Promise.resolve(true)
|
||||||
|
: Promise.resolve(false);
|
||||||
|
} catch (e) {
|
||||||
|
return Promise.resolve(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadWasm(modelId) {
|
||||||
|
if (wasmPromises[modelId]) return wasmPromises[modelId];
|
||||||
|
var p = PROCESSORS[modelId];
|
||||||
|
if (!p || !p.wasm) return Promise.resolve(null);
|
||||||
|
|
||||||
|
wasmPromises[modelId] = (modelId === 'rnnoise' ? checkSimd() : Promise.resolve(false)).then(
|
||||||
|
function (simd) {
|
||||||
|
var file = simd && p.simdWasm ? p.simdWasm : p.wasm;
|
||||||
|
return fetch(ASSET_BASE + file).then(function (r) {
|
||||||
|
if (!r.ok) {
|
||||||
|
if (simd && p.simdWasm)
|
||||||
|
return fetch(ASSET_BASE + p.wasm).then(function (r2) {
|
||||||
|
if (!r2.ok) throw new Error(modelId + ' wasm failed');
|
||||||
|
return r2.arrayBuffer();
|
||||||
|
});
|
||||||
|
throw new Error(modelId + ' wasm failed');
|
||||||
|
}
|
||||||
|
return r.arrayBuffer();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
return wasmPromises[modelId];
|
||||||
|
}
|
||||||
|
|
||||||
|
function getContext() {
|
||||||
|
if (!ctxPromise) {
|
||||||
|
ctxPromise = (function () {
|
||||||
|
var ctx = new AudioContext({ sampleRate: SAMPLE_RATE });
|
||||||
|
if (ctx.sampleRate !== SAMPLE_RATE) {
|
||||||
|
try {
|
||||||
|
ctx.close();
|
||||||
|
} catch (e) {}
|
||||||
|
return Promise.reject(new Error('SampleRate mismatch: ' + ctx.sampleRate));
|
||||||
|
}
|
||||||
|
// Load worklet modules. DTLN registers its own processor via the
|
||||||
|
// dynamic-imported helper (see buildMlNode), so it needs nothing here.
|
||||||
|
var scripts = [];
|
||||||
|
if (MODEL === 'rnnoise' || MODEL === 'speex') scripts.push(PROCESSORS[MODEL].script);
|
||||||
|
if (USE_GATE) scripts.push(PROCESSORS.gate.script);
|
||||||
|
|
||||||
|
return Promise.all(
|
||||||
|
scripts.map(function (s) {
|
||||||
|
return ctx.audioWorklet.addModule(ASSET_BASE + s);
|
||||||
|
}),
|
||||||
|
).then(function () {
|
||||||
|
return ctx.state === 'suspended'
|
||||||
|
? ctx.resume().then(function () {
|
||||||
|
return ctx;
|
||||||
|
})
|
||||||
|
: ctx;
|
||||||
|
});
|
||||||
|
})();
|
||||||
|
ctxPromise.catch(function () {
|
||||||
|
ctxPromise = null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return ctxPromise;
|
||||||
|
}
|
||||||
|
|
||||||
|
var hasNotifiedActive = false;
|
||||||
|
|
||||||
|
// Build the ML denoise AudioWorkletNode. RNNoise/Speex are flat sapphi
|
||||||
|
// worklets we instantiate directly with the fetched WASM binary. DTLN comes
|
||||||
|
// from @workadventure's self-contained helper, which we dynamic-import; it
|
||||||
|
// resolves its own processor + LiteRT WASM + TFLite models internally and
|
||||||
|
// returns the node. Resolves to { node, ready, dispose }.
|
||||||
|
function buildMlNode(ctx, wasmBinary) {
|
||||||
|
if (MODEL === 'dtln') {
|
||||||
|
return import(ASSET_BASE + PROCESSORS.dtln.helper).then(function (mod) {
|
||||||
|
// bypassUntilReady: pass raw audio through until the model is loaded so
|
||||||
|
// the call never has a silent/missing track during init.
|
||||||
|
return mod.createNoiseSuppressionAudioWorklet(ctx, { bypassUntilReady: true });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (MODEL === 'deepfilternet') {
|
||||||
|
// Resolve an absolute self-hosted base so the package's cdnUrl override
|
||||||
|
// fetches our vendored df_bg.wasm + ONNX model (never the upstream CDN).
|
||||||
|
var dfnBase = new URL(ASSET_BASE + 'deepfilternet', window.location.href).href;
|
||||||
|
return import(ASSET_BASE + PROCESSORS.deepfilternet.esm).then(function (mod) {
|
||||||
|
var core = new mod.DeepFilterNet3Core({
|
||||||
|
sampleRate: SAMPLE_RATE,
|
||||||
|
noiseReductionLevel: 80,
|
||||||
|
assetConfig: { cdnUrl: dfnBase },
|
||||||
|
});
|
||||||
|
// initialize() fetches + compiles the wasm and loads the model on the
|
||||||
|
// main thread; the worklet node only exists once that resolves, so the
|
||||||
|
// graph is connected with a ready model (no half-initialised passthrough).
|
||||||
|
return core.initialize().then(function () {
|
||||||
|
return core.createAudioWorkletNode(ctx).then(function (node) {
|
||||||
|
return {
|
||||||
|
node: node,
|
||||||
|
ready: Promise.resolve(),
|
||||||
|
dispose: function () {
|
||||||
|
try {
|
||||||
|
core.destroy();
|
||||||
|
} catch (e) {}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
var node = new AudioWorkletNode(ctx, PROCESSORS[MODEL].name, {
|
||||||
|
channelCount: 1,
|
||||||
|
numberOfInputs: 1,
|
||||||
|
numberOfOutputs: 1,
|
||||||
|
processorOptions: { maxChannels: 1, wasmBinary: wasmBinary },
|
||||||
|
});
|
||||||
|
return Promise.resolve({
|
||||||
|
node: node,
|
||||||
|
ready: Promise.resolve(),
|
||||||
|
dispose: function () {
|
||||||
|
try {
|
||||||
|
node.port.postMessage('destroy');
|
||||||
|
} catch (e) {}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function processStream(stream) {
|
||||||
|
var audioTracks = stream.getAudioTracks();
|
||||||
|
if (audioTracks.length === 0) return Promise.resolve(stream);
|
||||||
|
|
||||||
|
return Promise.all([loadWasm(MODEL), getContext()])
|
||||||
|
.then(function (res) {
|
||||||
|
var wasmBinary = res[0];
|
||||||
|
var ctx = res[1];
|
||||||
|
|
||||||
|
var source = ctx.createMediaStreamSource(stream);
|
||||||
|
var dest = ctx.createMediaStreamDestination();
|
||||||
|
var head = source;
|
||||||
|
|
||||||
|
// 1. Optional Noise Gate
|
||||||
|
if (USE_GATE) {
|
||||||
|
var gateNode = new AudioWorkletNode(ctx, PROCESSORS.gate.name, {
|
||||||
|
processorOptions: {
|
||||||
|
openThreshold: GATE_THRESHOLD,
|
||||||
|
closeThreshold: GATE_THRESHOLD - 5,
|
||||||
|
holdMs: 150,
|
||||||
|
maxChannels: 1,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
head.connect(gateNode);
|
||||||
|
head = gateNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. ML Processor
|
||||||
|
return buildMlNode(ctx, wasmBinary).then(function (ml) {
|
||||||
|
var mlNode = ml.node;
|
||||||
|
head.connect(mlNode);
|
||||||
|
mlNode.connect(dest);
|
||||||
|
|
||||||
|
// Surface async init failures (e.g. DTLN model load) without blocking
|
||||||
|
// the track handoff — audio flows via bypassUntilReady meanwhile.
|
||||||
|
if (ml.ready && typeof ml.ready.then === 'function') {
|
||||||
|
ml.ready.catch(function (err) {
|
||||||
|
var m = err instanceof Error ? err.message : String(err);
|
||||||
|
console.error('[lotus-denoise] ' + MODEL + ' init failed:', m);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
var origTrack = audioTracks[0];
|
||||||
|
var processedTrack = dest.stream.getAudioTracks()[0];
|
||||||
|
|
||||||
|
var torndown = false;
|
||||||
|
function cleanup() {
|
||||||
|
if (torndown) return;
|
||||||
|
torndown = true;
|
||||||
|
try {
|
||||||
|
ml.dispose();
|
||||||
|
} catch (e) {}
|
||||||
|
try {
|
||||||
|
source.disconnect();
|
||||||
|
mlNode.disconnect();
|
||||||
|
} catch (e) {}
|
||||||
|
try {
|
||||||
|
if (gateNode) gateNode.disconnect();
|
||||||
|
} catch (e) {}
|
||||||
|
try {
|
||||||
|
origTrack.stop();
|
||||||
|
} catch (e) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
var rawStop = processedTrack.stop.bind(processedTrack);
|
||||||
|
processedTrack.stop = function () {
|
||||||
|
cleanup();
|
||||||
|
rawStop();
|
||||||
|
};
|
||||||
|
origTrack.addEventListener('ended', function () {
|
||||||
|
try {
|
||||||
|
rawStop();
|
||||||
|
} catch (e) {}
|
||||||
|
cleanup();
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!hasNotifiedActive) {
|
||||||
|
hasNotifiedActive = true;
|
||||||
|
window.parent.postMessage(
|
||||||
|
{
|
||||||
|
type: 'lotus-denoise-status',
|
||||||
|
active: true,
|
||||||
|
model: MODEL,
|
||||||
|
nativeNS: USE_NATIVE_NS,
|
||||||
|
gate: USE_GATE,
|
||||||
|
},
|
||||||
|
targetOrigin,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
var out = new MediaStream();
|
||||||
|
out.addTrack(processedTrack);
|
||||||
|
stream.getVideoTracks().forEach(function (t) {
|
||||||
|
out.addTrack(t);
|
||||||
|
});
|
||||||
|
return out;
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch(function (e) {
|
||||||
|
var msg = e instanceof Error ? e.message : String(e);
|
||||||
|
console.error('[lotus-denoise] Setup failed:', msg);
|
||||||
|
window.parent.postMessage(
|
||||||
|
{ type: 'lotus-denoise-status', active: false, error: msg },
|
||||||
|
targetOrigin,
|
||||||
|
);
|
||||||
|
return stream;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
navigator.mediaDevices.getUserMedia = function (constraints) {
|
||||||
|
var wantsAudio = !!(constraints && constraints.audio);
|
||||||
|
var effective = constraints;
|
||||||
|
if (wantsAudio) {
|
||||||
|
var audioC =
|
||||||
|
typeof constraints.audio === 'object' ? Object.assign({}, constraints.audio) : {};
|
||||||
|
audioC.noiseSuppression = USE_NATIVE_NS;
|
||||||
|
audioC.channelCount = 1;
|
||||||
|
if (audioC.echoCancellation === undefined) audioC.echoCancellation = true;
|
||||||
|
if (audioC.autoGainControl === undefined) audioC.autoGainControl = true;
|
||||||
|
effective = Object.assign({}, constraints, { audio: audioC });
|
||||||
|
}
|
||||||
|
return origGetUserMedia(effective).then(function (stream) {
|
||||||
|
return wantsAudio ? processStream(stream) : stream;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
})();
|
||||||
@@ -1,31 +1,16 @@
|
|||||||
{
|
{
|
||||||
"defaultHomeserver": 1,
|
"defaultHomeserver": 0,
|
||||||
"homeserverList": ["converser.eu", "matrix.org", "mozilla.org", "unredacted.org", "xmr.se"],
|
"homeserverList": ["matrix.lotusguild.org", "matrix.org", "mozilla.org"],
|
||||||
"allowCustomHomeservers": true,
|
"allowCustomHomeservers": true,
|
||||||
|
|
||||||
"featuredCommunities": {
|
"featuredCommunities": {
|
||||||
"openAsDefault": false,
|
"openAsDefault": false,
|
||||||
"spaces": [
|
"spaces": [],
|
||||||
"#cinny:matrix.org",
|
"rooms": [],
|
||||||
"#community:matrix.org",
|
"servers": []
|
||||||
"#space:unredacted.org",
|
|
||||||
"#librewolf-community:matrix.org",
|
|
||||||
"#stickers-and-emojis:tastytea.de",
|
|
||||||
"#videogames:waywardinn.com",
|
|
||||||
"#science-space:matrix.org",
|
|
||||||
"#libregaming-games:tchncs.de",
|
|
||||||
"#mathematics-on:matrix.org"
|
|
||||||
],
|
|
||||||
"rooms": [
|
|
||||||
"#tuwunel:grin.hu",
|
|
||||||
"#freesoftware:matrix.org",
|
|
||||||
"#gentoo:matrix.org"
|
|
||||||
],
|
|
||||||
"servers": ["matrixrooms.info", "matrix.org", "mozilla.org", "unredacted.org"]
|
|
||||||
},
|
},
|
||||||
|
|
||||||
"hashRouter": {
|
"hashRouter": {
|
||||||
"enabled": false,
|
"enabled": false,
|
||||||
"basename": "/"
|
"basename": "/"
|
||||||
}
|
},
|
||||||
|
"gifApiKey": ""
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,126 @@
|
|||||||
|
import { FlatCompat } from '@eslint/eslintrc';
|
||||||
|
import js from '@eslint/js';
|
||||||
|
import tsPlugin from '@typescript-eslint/eslint-plugin';
|
||||||
|
import tsParser from '@typescript-eslint/parser';
|
||||||
|
import reactPlugin from 'eslint-plugin-react';
|
||||||
|
import reactHooksPlugin from 'eslint-plugin-react-hooks';
|
||||||
|
import jsxA11yPlugin from 'eslint-plugin-jsx-a11y';
|
||||||
|
import eslintConfigPrettier from 'eslint-config-prettier';
|
||||||
|
import globals from 'globals';
|
||||||
|
import path from 'path';
|
||||||
|
import { fileURLToPath } from 'url';
|
||||||
|
|
||||||
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
|
const __dirname = path.dirname(__filename);
|
||||||
|
|
||||||
|
const compat = new FlatCompat({
|
||||||
|
baseDirectory: __dirname,
|
||||||
|
recommendedConfig: js.configs.recommended,
|
||||||
|
allConfig: js.configs.all,
|
||||||
|
});
|
||||||
|
|
||||||
|
export default [
|
||||||
|
{ ignores: ['node_modules/**', 'dist/**', 'experiment/**'] },
|
||||||
|
js.configs.recommended,
|
||||||
|
tsPlugin.configs['flat/eslint-recommended'],
|
||||||
|
...tsPlugin.configs['flat/recommended'],
|
||||||
|
reactPlugin.configs.flat.recommended,
|
||||||
|
reactHooksPlugin.configs.flat['recommended'],
|
||||||
|
// Register jsx-a11y plugin (rules selectively enabled below)
|
||||||
|
{ plugins: { 'jsx-a11y': jsxA11yPlugin } },
|
||||||
|
// airbnb-base via FlatCompat (JS/import rules; no React plugin, no getFilename issue)
|
||||||
|
...compat.extends('airbnb-base'),
|
||||||
|
eslintConfigPrettier,
|
||||||
|
{
|
||||||
|
languageOptions: {
|
||||||
|
parser: tsParser,
|
||||||
|
globals: {
|
||||||
|
...globals.browser,
|
||||||
|
...globals.es2021,
|
||||||
|
JSX: 'readonly',
|
||||||
|
},
|
||||||
|
parserOptions: {
|
||||||
|
ecmaFeatures: { jsx: true },
|
||||||
|
ecmaVersion: 'latest',
|
||||||
|
sourceType: 'module',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
settings: {
|
||||||
|
react: {
|
||||||
|
version: '18.2.0',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
'linebreak-style': 0,
|
||||||
|
'no-unused-vars': 'off', // handled by @typescript-eslint/no-unused-vars
|
||||||
|
'no-underscore-dangle': 0,
|
||||||
|
'no-shadow': 'off',
|
||||||
|
|
||||||
|
// Stylistic rules — off for this codebase
|
||||||
|
'no-console': 'off',
|
||||||
|
'no-continue': 'off',
|
||||||
|
'no-nested-ternary': 'off',
|
||||||
|
'no-plusplus': 'off',
|
||||||
|
'no-param-reassign': 'off',
|
||||||
|
'no-restricted-syntax': 'off',
|
||||||
|
'no-restricted-globals': 'off',
|
||||||
|
'no-constant-condition': 'off',
|
||||||
|
'prefer-destructuring': 'off',
|
||||||
|
'no-useless-assignment': 'off',
|
||||||
|
'preserve-caught-error': 'off',
|
||||||
|
'consistent-return': 'off',
|
||||||
|
'no-use-before-define': 'off',
|
||||||
|
|
||||||
|
'import/prefer-default-export': 'off',
|
||||||
|
'import/extensions': 'off',
|
||||||
|
'import/no-unresolved': 'off',
|
||||||
|
'import/no-extraneous-dependencies': [
|
||||||
|
'error',
|
||||||
|
{
|
||||||
|
devDependencies: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
|
||||||
|
'react/no-unstable-nested-components': ['error', { allowAsProps: true }],
|
||||||
|
'react/jsx-filename-extension': [
|
||||||
|
'error',
|
||||||
|
{
|
||||||
|
extensions: ['.tsx', '.jsx'],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
|
||||||
|
'react/display-name': 'off',
|
||||||
|
'react/require-default-props': 'off',
|
||||||
|
'react/jsx-props-no-spreading': 'off',
|
||||||
|
'react-hooks/rules-of-hooks': 'error',
|
||||||
|
'react-hooks/exhaustive-deps': 'error',
|
||||||
|
// React Compiler rules added in react-hooks v7 — disabled until React Compiler is adopted
|
||||||
|
'react-hooks/react-compiler': 'off',
|
||||||
|
'react-hooks/incompatible-library': 'off',
|
||||||
|
'react-hooks/refs': 'off',
|
||||||
|
'react-hooks/set-state-in-effect': 'off',
|
||||||
|
'react-hooks/set-state-in-render': 'off',
|
||||||
|
'react-hooks/immutability': 'off',
|
||||||
|
'react-hooks/purity': 'off',
|
||||||
|
'react-hooks/use-memo': 'off',
|
||||||
|
|
||||||
|
'@typescript-eslint/no-unused-vars': [
|
||||||
|
'error',
|
||||||
|
{ argsIgnorePattern: '^_', varsIgnorePattern: '^_', caughtErrorsIgnorePattern: '^_' },
|
||||||
|
],
|
||||||
|
'@typescript-eslint/no-shadow': 'error',
|
||||||
|
'@typescript-eslint/no-explicit-any': 'warn',
|
||||||
|
|
||||||
|
// jsx-a11y — media captions not required for this app
|
||||||
|
'jsx-a11y/media-has-caption': 'off',
|
||||||
|
'jsx-a11y/no-noninteractive-element-interactions': 'off',
|
||||||
|
'jsx-a11y/alt-text': 'off',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
files: ['**/*.ts', '**/*.tsx'],
|
||||||
|
rules: {
|
||||||
|
'no-undef': 'off',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
@@ -1,36 +1,42 @@
|
|||||||
<!DOCTYPE html>
|
<!doctype html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>Cinny</title>
|
<title>Lotus Chat</title>
|
||||||
<meta name="name" content="Cinny" />
|
<meta name="name" content="Lotus Chat" />
|
||||||
<meta name="author" content="Ajay Bura" />
|
<meta name="author" content="Lotus Guild" />
|
||||||
<meta
|
<meta
|
||||||
name="description"
|
name="description"
|
||||||
content="A Matrix client where you can enjoy the conversation using simple, elegant and secure interface protected by e2ee with the power of open source."
|
content="Lotus Chat — the Lotus Guild Matrix client. Secure, fast, and built for our community."
|
||||||
/>
|
|
||||||
<meta
|
|
||||||
name="keywords"
|
|
||||||
content="cinny, cinnyapp, cinnychat, matrix, matrix client, matrix.org, element"
|
|
||||||
/>
|
/>
|
||||||
|
<meta name="keywords" content="lotus chat, lotus guild, matrix, matrix client" />
|
||||||
|
|
||||||
<meta property="og:title" content="Cinny" />
|
<meta property="og:type" content="website" />
|
||||||
<meta property="og:url" content="https://cinny.in" />
|
<meta property="og:title" content="Lotus Chat" />
|
||||||
<meta property="og:image" content="https://cinny.in/assets/favicon-48x48.png" />
|
<meta property="og:url" content="https://chat.lotusguild.org" />
|
||||||
|
<meta
|
||||||
|
property="og:image"
|
||||||
|
content="https://chat.lotusguild.org/public/res/android/android-chrome-192x192.png"
|
||||||
|
/>
|
||||||
<meta
|
<meta
|
||||||
property="og:description"
|
property="og:description"
|
||||||
content="A Matrix client where you can enjoy the conversation using simple, elegant and secure interface protected by e2ee with the power of open source."
|
content="Lotus Chat — the Lotus Guild Matrix client. Secure, fast, and built for our community."
|
||||||
/>
|
/>
|
||||||
<meta name="theme-color" content="#000000" />
|
<meta name="theme-color" content="#000000" />
|
||||||
|
<meta name="color-scheme" content="dark light" />
|
||||||
|
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com" crossorigin />
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=VT323&display=swap" rel="stylesheet" />
|
||||||
|
<link rel="stylesheet" href="/fonts/custom-fonts.css" />
|
||||||
<link id="favicon" rel="shortcut icon" href="./public/favicon.ico" />
|
<link id="favicon" rel="shortcut icon" href="./public/favicon.ico" />
|
||||||
|
|
||||||
<link rel="manifest" href="/manifest.json" />
|
<link rel="manifest" href="/manifest.json" />
|
||||||
<meta name="mobile-web-app-capable" content="yes" />
|
<meta name="mobile-web-app-capable" content="yes" />
|
||||||
<meta name="application-name" content="Cinny" />
|
<meta name="application-name" content="Lotus Chat" />
|
||||||
<meta name="apple-mobile-web-app-title" content="Cinny" />
|
<meta name="apple-mobile-web-app-title" content="Lotus Chat" />
|
||||||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
|
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
|
||||||
|
|
||||||
|
|||||||
|
After Width: | Height: | Size: 851 KiB |
|
After Width: | Height: | Size: 944 KiB |
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "cinny",
|
"name": "lotus-chat",
|
||||||
"version": "4.12.2",
|
"version": "4.12.3-lotus",
|
||||||
"description": "Yet another matrix client",
|
"description": "Lotus Chat — Matrix client for Lotus Guild",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"engines": {
|
"engines": {
|
||||||
@@ -16,9 +16,11 @@
|
|||||||
"check:prettier": "prettier --check .",
|
"check:prettier": "prettier --check .",
|
||||||
"fix:prettier": "prettier --write .",
|
"fix:prettier": "prettier --write .",
|
||||||
"typecheck": "tsc --noEmit",
|
"typecheck": "tsc --noEmit",
|
||||||
"prepare": "husky install",
|
"test": "node --import tsx --test $(find src -name '*.test.ts')",
|
||||||
|
"prepare": "husky",
|
||||||
"commit": "git-cz",
|
"commit": "git-cz",
|
||||||
"bump": "node scripts/update-version.js"
|
"postinstall": "node scripts/patch-folds.mjs",
|
||||||
|
"sync:decorations": "node scripts/syncDecorations.mjs"
|
||||||
},
|
},
|
||||||
"lint-staged": {
|
"lint-staged": {
|
||||||
"*.{ts,tsx,js,jsx}": "eslint",
|
"*.{ts,tsx,js,jsx}": "eslint",
|
||||||
@@ -33,96 +35,114 @@
|
|||||||
"author": "Ajay Bura",
|
"author": "Ajay Bura",
|
||||||
"license": "AGPL-3.0-only",
|
"license": "AGPL-3.0-only",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@atlaskit/pragmatic-drag-and-drop": "1.1.6",
|
"@atlaskit/pragmatic-drag-and-drop": "1.8.1",
|
||||||
"@atlaskit/pragmatic-drag-and-drop-auto-scroll": "1.3.0",
|
"@atlaskit/pragmatic-drag-and-drop-auto-scroll": "2.1.5",
|
||||||
"@atlaskit/pragmatic-drag-and-drop-hitbox": "1.0.3",
|
"@atlaskit/pragmatic-drag-and-drop-hitbox": "1.1.0",
|
||||||
"@fontsource/inter": "4.5.14",
|
"@eslint/eslintrc": "3.3.5",
|
||||||
"@tanstack/react-query": "5.24.1",
|
"@eslint/js": "10.0.1",
|
||||||
"@tanstack/react-query-devtools": "5.24.1",
|
"@fontsource-variable/inter": "5.2.8",
|
||||||
"@tanstack/react-virtual": "3.2.0",
|
"@giphy/js-fetch-api": "5.8.0",
|
||||||
"@vanilla-extract/css": "1.9.3",
|
"@giphy/js-types": "5.1.0",
|
||||||
"@vanilla-extract/recipes": "0.3.0",
|
"@giphy/js-util": "5.2.0",
|
||||||
"@vanilla-extract/vite-plugin": "3.7.1",
|
"@giphy/react-components": "10.1.2",
|
||||||
|
"@sapphi-red/web-noise-suppressor": "0.3.5",
|
||||||
|
"@tanstack/react-query": "5.100.13",
|
||||||
|
"@tanstack/react-query-devtools": "5.100.13",
|
||||||
|
"@tanstack/react-virtual": "3.13.25",
|
||||||
|
"@types/dompurify": "3.2.0",
|
||||||
|
"@workadventure/noise-suppression": "0.0.4",
|
||||||
"await-to-js": "3.0.0",
|
"await-to-js": "3.0.0",
|
||||||
"badwords-list": "2.0.1-4",
|
"badwords-list": "2.0.1-4",
|
||||||
"blurhash": "2.0.4",
|
"blurhash": "2.0.5",
|
||||||
"browser-encrypt-attachment": "0.3.0",
|
"browser-encrypt-attachment": "0.3.0",
|
||||||
"chroma-js": "3.1.2",
|
"chroma-js": "3.2.0",
|
||||||
"classnames": "2.3.2",
|
"classnames": "2.5.1",
|
||||||
"dateformat": "5.0.3",
|
"dateformat": "5.0.3",
|
||||||
"dayjs": "1.11.10",
|
"dayjs": "1.11.20",
|
||||||
"domhandler": "5.0.3",
|
"deepfilternet3-noise-filter": "1.2.1",
|
||||||
"emojibase": "15.3.1",
|
"domhandler": "6.0.1",
|
||||||
"emojibase-data": "15.3.2",
|
"dompurify": "3.4.5",
|
||||||
|
"emojibase": "17.0.0",
|
||||||
|
"emojibase-data": "17.0.0",
|
||||||
"file-saver": "2.0.5",
|
"file-saver": "2.0.5",
|
||||||
"focus-trap-react": "10.0.2",
|
"focus-trap-react": "12.0.2",
|
||||||
"folds": "2.6.2",
|
"folds": "2.6.2",
|
||||||
"html-dom-parser": "4.0.0",
|
"globals": "17.6.0",
|
||||||
"html-react-parser": "4.2.0",
|
"html-dom-parser": "7.1.0",
|
||||||
"i18next": "23.12.2",
|
"html-react-parser": "6.1.2",
|
||||||
"i18next-browser-languagedetector": "8.0.0",
|
"i18next": "26.2.0",
|
||||||
"i18next-http-backend": "2.5.2",
|
"i18next-browser-languagedetector": "8.2.1",
|
||||||
"immer": "9.0.16",
|
"i18next-http-backend": "4.0.0",
|
||||||
|
"immer": "11.1.8",
|
||||||
"is-hotkey": "0.2.0",
|
"is-hotkey": "0.2.0",
|
||||||
"jotai": "2.6.0",
|
"jotai": "2.20.0",
|
||||||
"linkify-react": "4.3.2",
|
"linkify-react": "4.3.3",
|
||||||
"linkifyjs": "4.3.2",
|
"linkifyjs": "4.3.3",
|
||||||
"matrix-js-sdk": "41.5.0",
|
"matrix-js-sdk": "41.6.0-rc.0",
|
||||||
"matrix-widget-api": "1.16.1",
|
"matrix-widget-api": "1.17.0",
|
||||||
"millify": "6.1.0",
|
"millify": "6.1.0",
|
||||||
"pdfjs-dist": "4.2.67",
|
"pdfjs-dist": "5.7.284",
|
||||||
"prismjs": "1.30.0",
|
"prismjs": "1.30.0",
|
||||||
"react": "18.2.0",
|
"react": "19.2.6",
|
||||||
"react-aria": "3.29.1",
|
"react-aria": "3.48.0",
|
||||||
"react-blurhash": "0.2.0",
|
"react-blurhash": "0.3.0",
|
||||||
"react-colorful": "5.6.1",
|
"react-colorful": "5.7.0",
|
||||||
"react-dom": "18.2.0",
|
"react-dom": "19.2.6",
|
||||||
"react-error-boundary": "4.0.13",
|
"react-error-boundary": "6.1.1",
|
||||||
"react-google-recaptcha": "2.1.0",
|
"react-google-recaptcha": "3.1.0",
|
||||||
"react-i18next": "15.0.0",
|
"react-i18next": "17.0.8",
|
||||||
"react-range": "1.8.14",
|
"react-range": "1.10.0",
|
||||||
"react-router-dom": "6.30.3",
|
"react-router-dom": "7.15.1",
|
||||||
"sanitize-html": "2.17.4",
|
"sanitize-html": "2.17.4",
|
||||||
"slate": "0.123.0",
|
"slate": "0.124.1",
|
||||||
"slate-dom": "0.123.0",
|
"slate-dom": "0.124.1",
|
||||||
"slate-history": "0.113.1",
|
"slate-history": "0.113.1",
|
||||||
"slate-react": "0.123.0",
|
"slate-react": "0.124.2",
|
||||||
"ua-parser-js": "1.0.35"
|
"styled-components": "6.4.2",
|
||||||
|
"ua-parser-js": "2.0.10"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@element-hq/element-call-embedded": "0.19.1",
|
"@lotusguild/element-call-embedded": "0.20.1-lotus.1",
|
||||||
"@esbuild-plugins/node-globals-polyfill": "0.2.3",
|
"@rollup/plugin-inject": "5.0.5",
|
||||||
"@rollup/plugin-inject": "5.0.3",
|
"@rollup/plugin-wasm": "6.2.2",
|
||||||
"@rollup/plugin-wasm": "6.1.1",
|
"@types/chroma-js": "3.1.2",
|
||||||
"@types/chroma-js": "3.1.1",
|
"@types/file-saver": "2.0.7",
|
||||||
"@types/file-saver": "2.0.5",
|
|
||||||
"@types/is-hotkey": "0.1.10",
|
"@types/is-hotkey": "0.1.10",
|
||||||
"@types/node": "18.11.18",
|
"@types/node": "25.9.1",
|
||||||
"@types/prismjs": "1.26.0",
|
"@types/prismjs": "1.26.6",
|
||||||
"@types/react": "18.2.39",
|
"@types/react": "19.2.15",
|
||||||
"@types/react-dom": "18.2.17",
|
"@types/react-dom": "19.2.3",
|
||||||
"@types/react-google-recaptcha": "2.1.8",
|
"@types/react-google-recaptcha": "2.1.9",
|
||||||
"@types/sanitize-html": "2.16.1",
|
"@types/sanitize-html": "2.16.1",
|
||||||
"@types/ua-parser-js": "0.7.36",
|
"@types/ua-parser-js": "0.7.39",
|
||||||
"@typescript-eslint/eslint-plugin": "5.46.1",
|
"@typescript-eslint/eslint-plugin": "8.59.4",
|
||||||
"@typescript-eslint/parser": "5.46.1",
|
"@typescript-eslint/parser": "8.59.4",
|
||||||
"@vitejs/plugin-react": "4.2.0",
|
"@vanilla-extract/css": "1.20.1",
|
||||||
|
"@vanilla-extract/recipes": "0.5.7",
|
||||||
|
"@vanilla-extract/vite-plugin": "5.2.2",
|
||||||
|
"@vitejs/plugin-react": "6.0.2",
|
||||||
"buffer": "6.0.3",
|
"buffer": "6.0.3",
|
||||||
"cz-conventional-changelog": "3.3.0",
|
"cz-conventional-changelog": "3.3.0",
|
||||||
"eslint": "8.29.0",
|
"eslint": "9.39.4",
|
||||||
"eslint-config-airbnb": "19.0.4",
|
"eslint-config-airbnb": "19.0.4",
|
||||||
"eslint-config-prettier": "8.5.0",
|
"eslint-config-prettier": "10.1.8",
|
||||||
"eslint-plugin-import": "2.29.1",
|
"eslint-plugin-import": "2.32.0",
|
||||||
"eslint-plugin-jsx-a11y": "6.6.1",
|
"eslint-plugin-jsx-a11y": "6.10.2",
|
||||||
"eslint-plugin-react": "7.31.11",
|
"eslint-plugin-react": "7.37.5",
|
||||||
"eslint-plugin-react-hooks": "4.6.0",
|
"eslint-plugin-react-hooks": "7.1.1",
|
||||||
"husky": "9.1.7",
|
"husky": "9.1.7",
|
||||||
"lint-staged": "16.3.2",
|
"lint-staged": "17.0.5",
|
||||||
"prettier": "2.8.1",
|
"prettier": "3.8.3",
|
||||||
"typescript": "4.9.4",
|
"tsx": "4.22.4",
|
||||||
"vite": "5.4.19",
|
"typescript": "6.0.3",
|
||||||
"vite-plugin-pwa": "0.20.5",
|
"vite": "8.0.14",
|
||||||
"vite-plugin-static-copy": "1.0.4",
|
"vite-plugin-pwa": "1.3.0",
|
||||||
"vite-plugin-top-level-await": "1.4.4"
|
"vite-plugin-static-copy": "4.1.0"
|
||||||
|
},
|
||||||
|
"overrides": {
|
||||||
|
"@giphy/js-util": {
|
||||||
|
"dompurify": ">=3.3.4"
|
||||||
|
},
|
||||||
|
"js-cookie": ">=3.0.6"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"defaultHomeserver": 0,
|
||||||
|
"homeserverList": ["matrix.lotusguild.org", "matrix.org", "mozilla.org"],
|
||||||
|
"allowCustomHomeservers": true,
|
||||||
|
"featuredCommunities": {
|
||||||
|
"openAsDefault": false,
|
||||||
|
"spaces": [],
|
||||||
|
"rooms": [],
|
||||||
|
"servers": []
|
||||||
|
},
|
||||||
|
"hashRouter": {
|
||||||
|
"enabled": false,
|
||||||
|
"basename": "/"
|
||||||
|
},
|
||||||
|
"gifApiKey": ""
|
||||||
|
}
|
||||||
|
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 128 KiB |
@@ -0,0 +1,11 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang=en>
|
||||||
|
<meta charset=utf-8>
|
||||||
|
<meta name=viewport content="initial-scale=1, minimum-scale=1, width=device-width">
|
||||||
|
<title>Error 404 (Not Found)!!1</title>
|
||||||
|
<style>
|
||||||
|
*{margin:0;padding:0}html,code{font:15px/22px arial,sans-serif}html{background:#fff;color:#222;padding:15px}body{margin:7% auto 0;max-width:390px;min-height:180px;padding:30px 0 15px}* > body{background:url(//www.google.com/images/errors/robot.png) 100% 5px no-repeat;padding-right:205px}p{margin:11px 0 22px;overflow:hidden}ins{color:#777;text-decoration:none}a img{border:0}@media screen and (max-width:772px){body{background:none;margin-top:0;max-width:none;padding-right:0}}#logo{background:url(//www.google.com/images/branding/googlelogo/1x/googlelogo_color_150x54dp.png) no-repeat;margin-left:-5px}@media only screen and (min-resolution:192dpi){#logo{background:url(//www.google.com/images/branding/googlelogo/2x/googlelogo_color_150x54dp.png) no-repeat 0% 0%/100% 100%;-moz-border-image:url(//www.google.com/images/branding/googlelogo/2x/googlelogo_color_150x54dp.png) 0}}@media only screen and (-webkit-min-device-pixel-ratio:2){#logo{background:url(//www.google.com/images/branding/googlelogo/2x/googlelogo_color_150x54dp.png) no-repeat;-webkit-background-size:100% 100%}}#logo{display:inline-block;height:54px;width:150px}
|
||||||
|
</style>
|
||||||
|
<a href=//www.google.com/><span id=logo aria-label=Google></span></a>
|
||||||
|
<p><b>404.</b> <ins>That’s an error.</ins>
|
||||||
|
<p>The requested URL <code>/s/jetbrainsmono/v18/tDbY2o-flEEny0FZhsfKu5WU4xD-IQ.woff2</code> was not found on this server. <ins>That’s all we know.</ins>
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang=en>
|
||||||
|
<meta charset=utf-8>
|
||||||
|
<meta name=viewport content="initial-scale=1, minimum-scale=1, width=device-width">
|
||||||
|
<title>Error 404 (Not Found)!!1</title>
|
||||||
|
<style>
|
||||||
|
*{margin:0;padding:0}html,code{font:15px/22px arial,sans-serif}html{background:#fff;color:#222;padding:15px}body{margin:7% auto 0;max-width:390px;min-height:180px;padding:30px 0 15px}* > body{background:url(//www.google.com/images/errors/robot.png) 100% 5px no-repeat;padding-right:205px}p{margin:11px 0 22px;overflow:hidden}ins{color:#777;text-decoration:none}a img{border:0}@media screen and (max-width:772px){body{background:none;margin-top:0;max-width:none;padding-right:0}}#logo{background:url(//www.google.com/images/branding/googlelogo/1x/googlelogo_color_150x54dp.png) no-repeat;margin-left:-5px}@media only screen and (min-resolution:192dpi){#logo{background:url(//www.google.com/images/branding/googlelogo/2x/googlelogo_color_150x54dp.png) no-repeat 0% 0%/100% 100%;-moz-border-image:url(//www.google.com/images/branding/googlelogo/2x/googlelogo_color_150x54dp.png) 0}}@media only screen and (-webkit-min-device-pixel-ratio:2){#logo{background:url(//www.google.com/images/branding/googlelogo/2x/googlelogo_color_150x54dp.png) no-repeat;-webkit-background-size:100% 100%}}#logo{display:inline-block;height:54px;width:150px}
|
||||||
|
</style>
|
||||||
|
<a href=//www.google.com/><span id=logo aria-label=Google></span></a>
|
||||||
|
<p><b>404.</b> <ins>That’s an error.</ins>
|
||||||
|
<p>The requested URL <code>/s/jetbrainsmono/v18/tDbY2o-flEEny0FZhsfKu5WU4xD-IQ.woff2</code> was not found on this server. <ins>That’s all we know.</ins>
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang=en>
|
||||||
|
<meta charset=utf-8>
|
||||||
|
<meta name=viewport content="initial-scale=1, minimum-scale=1, width=device-width">
|
||||||
|
<title>Error 404 (Not Found)!!1</title>
|
||||||
|
<style>
|
||||||
|
*{margin:0;padding:0}html,code{font:15px/22px arial,sans-serif}html{background:#fff;color:#222;padding:15px}body{margin:7% auto 0;max-width:390px;min-height:180px;padding:30px 0 15px}* > body{background:url(//www.google.com/images/errors/robot.png) 100% 5px no-repeat;padding-right:205px}p{margin:11px 0 22px;overflow:hidden}ins{color:#777;text-decoration:none}a img{border:0}@media screen and (max-width:772px){body{background:none;margin-top:0;max-width:none;padding-right:0}}#logo{background:url(//www.google.com/images/branding/googlelogo/1x/googlelogo_color_150x54dp.png) no-repeat;margin-left:-5px}@media only screen and (min-resolution:192dpi){#logo{background:url(//www.google.com/images/branding/googlelogo/2x/googlelogo_color_150x54dp.png) no-repeat 0% 0%/100% 100%;-moz-border-image:url(//www.google.com/images/branding/googlelogo/2x/googlelogo_color_150x54dp.png) 0}}@media only screen and (-webkit-min-device-pixel-ratio:2){#logo{background:url(//www.google.com/images/branding/googlelogo/2x/googlelogo_color_150x54dp.png) no-repeat;-webkit-background-size:100% 100%}}#logo{display:inline-block;height:54px;width:150px}
|
||||||
|
</style>
|
||||||
|
<a href=//www.google.com/><span id=logo aria-label=Google></span></a>
|
||||||
|
<p><b>404.</b> <ins>That’s an error.</ins>
|
||||||
|
<p>The requested URL <code>/s/jetbrainsmono/v18/tDbY2o-flEEny0FZhsfKu5WU4xD-IQ.woff2</code> was not found on this server. <ins>That’s all we know.</ins>
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
/* Self-hosted fonts — avoids tracking prevention in desktop WebView2 */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'JetBrains Mono';
|
||||||
|
font-style: italic;
|
||||||
|
font-weight: 400;
|
||||||
|
font-display: swap;
|
||||||
|
src: url('/fonts/JetBrainsMono-italic-400.woff2') format('woff2');
|
||||||
|
unicode-range:
|
||||||
|
U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329,
|
||||||
|
U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||||
|
}
|
||||||
|
@font-face {
|
||||||
|
font-family: 'JetBrains Mono';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
font-display: swap;
|
||||||
|
src: url('/fonts/JetBrainsMono-normal-400.woff2') format('woff2');
|
||||||
|
unicode-range:
|
||||||
|
U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329,
|
||||||
|
U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||||
|
}
|
||||||
|
@font-face {
|
||||||
|
font-family: 'JetBrains Mono';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 700;
|
||||||
|
font-display: swap;
|
||||||
|
src: url('/fonts/JetBrainsMono-normal-700.woff2') format('woff2');
|
||||||
|
unicode-range:
|
||||||
|
U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329,
|
||||||
|
U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||||
|
}
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Fira Code';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
font-display: swap;
|
||||||
|
src: url('/fonts/FiraCode-400.woff2') format('woff2');
|
||||||
|
unicode-range:
|
||||||
|
U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329,
|
||||||
|
U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||||
|
}
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Fira Code';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 600;
|
||||||
|
font-display: swap;
|
||||||
|
src: url('/fonts/FiraCode-600.woff2') format('woff2');
|
||||||
|
unicode-range:
|
||||||
|
U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329,
|
||||||
|
U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||||
|
}
|
||||||
@@ -2,6 +2,57 @@
|
|||||||
"Organisms": {
|
"Organisms": {
|
||||||
"RoomCommon": {
|
"RoomCommon": {
|
||||||
"changed_room_name": " changed room name"
|
"changed_room_name": " changed room name"
|
||||||
|
},
|
||||||
|
"CreateRoom": {
|
||||||
|
"chat_room": "Chat Room",
|
||||||
|
"chat_room_desc": "Messages, photos, and videos.",
|
||||||
|
"voice_room": "Voice Room",
|
||||||
|
"voice_room_desc": "Live audio and video conversations."
|
||||||
|
},
|
||||||
|
"ImageViewer": {
|
||||||
|
"download": "Download"
|
||||||
|
},
|
||||||
|
"Message": {
|
||||||
|
"open_location": "Open Location",
|
||||||
|
"thread": "Thread"
|
||||||
|
},
|
||||||
|
"ImageContent": {
|
||||||
|
"view": "View",
|
||||||
|
"spoiler": "Spoiler",
|
||||||
|
"retry": "Retry"
|
||||||
|
},
|
||||||
|
"DeviceVerification": {
|
||||||
|
"close": "Close",
|
||||||
|
"accept": "Accept",
|
||||||
|
"they_match": "They Match",
|
||||||
|
"okay": "Okay",
|
||||||
|
"do_not_match": "Do not Match",
|
||||||
|
"please_accept": "Please accept the request from other device.",
|
||||||
|
"waiting_accept": "Waiting for request to be accepted...",
|
||||||
|
"click_accept": "Click accept to start the verification process.",
|
||||||
|
"request_accepted": "Verification request has been accepted.",
|
||||||
|
"waiting_response": "Waiting for the response from other device...",
|
||||||
|
"starting_emoji": "Starting verification using emoji comparison...",
|
||||||
|
"confirm_emoji": "Confirm the emoji below are displayed on both devices, in the same order:",
|
||||||
|
"device_verified": "Your device is verified.",
|
||||||
|
"verification_canceled": "Verification has been canceled."
|
||||||
|
},
|
||||||
|
"UrlPreview": {
|
||||||
|
"join_server": "Join Server"
|
||||||
|
},
|
||||||
|
"InviteUser": {
|
||||||
|
"invite": "Invite"
|
||||||
|
},
|
||||||
|
"UploadBoard": {
|
||||||
|
"files": "Files",
|
||||||
|
"send": "Send",
|
||||||
|
"upload_failed": "Upload Failed"
|
||||||
|
},
|
||||||
|
"PasswordStage": {
|
||||||
|
"account_password": "Account Password",
|
||||||
|
"password": "Password",
|
||||||
|
"invalid_password": "Invalid Password!",
|
||||||
|
"authenticate_prompt": "To perform this action you need to authenticate yourself by entering you account password."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,59 +1,86 @@
|
|||||||
{
|
{
|
||||||
"name": "Cinny",
|
"name": "Lotus Chat",
|
||||||
"short_name": "Cinny",
|
"short_name": "Lotus Chat",
|
||||||
"description": "Yet another matrix client",
|
"description": "Lotus Chat \u2014 the Lotus Guild Matrix client",
|
||||||
"dir": "auto",
|
"dir": "auto",
|
||||||
"lang": "en-US",
|
"lang": "en-US",
|
||||||
"display": "standalone",
|
"display": "standalone",
|
||||||
"orientation": "portrait",
|
"orientation": "portrait",
|
||||||
"start_url": "./",
|
"start_url": "./",
|
||||||
"background_color": "#fff",
|
"background_color": "#0a0a0a",
|
||||||
"theme_color": "#fff",
|
"theme_color": "#980000",
|
||||||
"icons": [
|
"icons": [
|
||||||
{
|
{
|
||||||
"src": "./public/android/android-chrome-36x36.png",
|
"src": "./res/android/android-chrome-36x36.png",
|
||||||
"sizes": "36x36",
|
"sizes": "36x36",
|
||||||
"type": "image/png"
|
"type": "image/png"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"src": "./public/android/android-chrome-48x48.png",
|
"src": "./res/android/android-chrome-48x48.png",
|
||||||
"sizes": "48x48",
|
"sizes": "48x48",
|
||||||
"type": "image/png"
|
"type": "image/png"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"src": "./public/android/android-chrome-72x72.png",
|
"src": "./res/android/android-chrome-72x72.png",
|
||||||
"sizes": "72x72",
|
"sizes": "72x72",
|
||||||
"type": "image/png"
|
"type": "image/png"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"src": "./public/android/android-chrome-96x96.png",
|
"src": "./res/android/android-chrome-96x96.png",
|
||||||
"sizes": "96x96",
|
"sizes": "96x96",
|
||||||
"type": "image/png"
|
"type": "image/png"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"src": "./public/android/android-chrome-144x144.png",
|
"src": "./res/android/android-chrome-144x144.png",
|
||||||
"sizes": "144x144",
|
"sizes": "144x144",
|
||||||
"type": "image/png"
|
"type": "image/png"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"src": "./public/android/android-chrome-192x192.png",
|
"src": "./res/android/android-chrome-192x192.png",
|
||||||
"sizes": "192x192",
|
"sizes": "192x192",
|
||||||
"type": "image/png"
|
"type": "image/png"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"src": "./public/android/android-chrome-256x256.png",
|
"src": "./res/android/android-chrome-256x256.png",
|
||||||
"sizes": "256x256",
|
"sizes": "256x256",
|
||||||
"type": "image/png"
|
"type": "image/png"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"src": "./public/android/android-chrome-384x384.png",
|
"src": "./res/android/android-chrome-384x384.png",
|
||||||
"sizes": "384x384",
|
"sizes": "384x384",
|
||||||
"type": "image/png"
|
"type": "image/png"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"src": "./public/android/android-chrome-512x512.png",
|
"src": "./res/android/android-chrome-512x512.png",
|
||||||
"sizes": "512x512",
|
"sizes": "512x512",
|
||||||
"type": "image/png"
|
"type": "image/png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "./res/android/maskable-192x192.png",
|
||||||
|
"sizes": "192x192",
|
||||||
|
"type": "image/png",
|
||||||
|
"purpose": "maskable"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "./res/android/maskable-512x512.png",
|
||||||
|
"sizes": "512x512",
|
||||||
|
"type": "image/png",
|
||||||
|
"purpose": "maskable"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"categories": ["social", "communication", "productivity"],
|
||||||
|
"shortcuts": [
|
||||||
|
{
|
||||||
|
"name": "New Message",
|
||||||
|
"short_name": "DM",
|
||||||
|
"description": "Open a new direct message",
|
||||||
|
"url": "/",
|
||||||
|
"icons": [
|
||||||
|
{
|
||||||
|
"src": "res/android/android-chrome-96x96.png",
|
||||||
|
"sizes": "96x96"
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
|
After Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 4.4 KiB After Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 6.1 KiB After Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 8.2 KiB After Width: | Height: | Size: 40 KiB |
|
Before Width: | Height: | Size: 1.0 KiB After Width: | Height: | Size: 2.0 KiB |
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 79 KiB |
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 3.1 KiB |
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 134 KiB |
|
Before Width: | Height: | Size: 2.2 KiB After Width: | Height: | Size: 5.8 KiB |
|
Before Width: | Height: | Size: 2.9 KiB After Width: | Height: | Size: 8.9 KiB |
|
After Width: | Height: | Size: 7.7 KiB |
|
After Width: | Height: | Size: 37 KiB |
|
Before Width: | Height: | Size: 4.8 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 5.0 KiB After Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 5.9 KiB After Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 6.5 KiB After Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 7.2 KiB After Width: | Height: | Size: 21 KiB |
|
Before Width: | Height: | Size: 7.7 KiB After Width: | Height: | Size: 23 KiB |
|
Before Width: | Height: | Size: 2.3 KiB After Width: | Height: | Size: 4.0 KiB |
|
Before Width: | Height: | Size: 2.4 KiB After Width: | Height: | Size: 4.3 KiB |
|
Before Width: | Height: | Size: 2.9 KiB After Width: | Height: | Size: 5.8 KiB |
|
Before Width: | Height: | Size: 3.1 KiB After Width: | Height: | Size: 6.2 KiB |
|
After Width: | Height: | Size: 2.0 KiB |
|
After Width: | Height: | Size: 2.0 KiB |
|
After Width: | Height: | Size: 2.0 KiB |
@@ -1,13 +1,14 @@
|
|||||||
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
|
<svg viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
<g clip-path="url(#clip0_2707_1961)">
|
<g fill="#980000" fill-opacity="0.88">
|
||||||
<path d="M10.5867 17.3522C10.0727 17.4492 9.54226 17.5 9 17.5C4.30558 17.5 0.5 13.6944 0.5 9C0.5 4.30558 4.30558 0.5 9 0.5C13.6944 0.5 17.5 4.30558 17.5 9C17.5 9.54226 17.4492 10.0727 17.3522 10.5867C16.6511 10.2123 15.8503 10 15 10C12.2386 10 10 12.2386 10 15C10 15.8503 10.2123 16.6511 10.5867 17.3522Z" fill="white"/>
|
<path d="M9,9 Q6.4,4.8 9,1.2 Q11.6,4.8 9,9Z" transform="rotate(0,9,9)"/>
|
||||||
<path d="M10 6.39999C10 6.67614 9.77614 6.89999 9.5 6.89999C9.22386 6.89999 9 6.67614 9 6.39999C9 6.12385 9.22386 5.89999 9.5 5.89999C9.77614 5.89999 10 6.12385 10 6.39999Z" fill="black"/>
|
<path d="M9,9 Q6.4,4.8 9,1.2 Q11.6,4.8 9,9Z" transform="rotate(45,9,9)"/>
|
||||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M9 0C4 0 0 4 0 9C0 14 4 18 9 18C9.63967 18 10.263 17.9345 10.8636 17.8099C10.3186 17.0091 10 16.0417 10 15C10 12.2386 12.2386 10 15 10C16.0417 10 17.0091 10.3186 17.8099 10.8636C17.9345 10.263 18 9.63967 18 9C18 4 14 0 9 0ZM1.2 10.8L4.7 8.5V8.2C4.7 6.4 6 5 7.8 4.8H8.2C9.4 4.8 10.5 5.4 11.1 6.4C11.4 6.3 11.7 6.3 12 6.3C12.4 6.3 12.8 6.3 13.2 6.4C13.9 6.6 14.6 6.9 15.2 7.3C14.6 7.1 14 7 13.3 7C12.1 7 11.1 7.4 10.4 8.4C9.7 9.3 9.3 10.4 9.3 11.6C9.3 13.1 8.9 14.5 8 15.8C7.93744 15.8834 7.87923 15.9625 7.82356 16.0381C7.6123 16.325 7.43739 16.5626 7.2 16.8C4.2 16.1 1.9 13.8 1.2 10.8Z" fill="black"/>
|
<path d="M9,9 Q6.4,4.8 9,1.2 Q11.6,4.8 9,9Z" transform="rotate(90,9,9)"/>
|
||||||
<path d="M18 15C18 16.6569 16.6569 18 15 18C13.3431 18 12 16.6569 12 15C12 13.3431 13.3431 12 15 12C16.6569 12 18 13.3431 18 15Z" fill="#45B83B"/>
|
<path d="M9,9 Q6.4,4.8 9,1.2 Q11.6,4.8 9,9Z" transform="rotate(135,9,9)"/>
|
||||||
</g>
|
<path d="M9,9 Q6.4,4.8 9,1.2 Q11.6,4.8 9,9Z" transform="rotate(180,9,9)"/>
|
||||||
<defs>
|
<path d="M9,9 Q6.4,4.8 9,1.2 Q11.6,4.8 9,9Z" transform="rotate(225,9,9)"/>
|
||||||
<clipPath id="clip0_2707_1961">
|
<path d="M9,9 Q6.4,4.8 9,1.2 Q11.6,4.8 9,9Z" transform="rotate(270,9,9)"/>
|
||||||
<rect width="18" height="18" fill="white"/>
|
<path d="M9,9 Q6.4,4.8 9,1.2 Q11.6,4.8 9,9Z" transform="rotate(315,9,9)"/>
|
||||||
</clipPath>
|
</g>
|
||||||
</defs>
|
<circle cx="9" cy="9" r="2.2" fill="#cc2000"/>
|
||||||
</svg>
|
<circle cx="14.5" cy="14.5" r="3" fill="#45B83B"/>
|
||||||
|
</svg>
|
||||||
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 841 B |
@@ -1,13 +1,14 @@
|
|||||||
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
|
<svg viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
<g clip-path="url(#clip0_2707_2015)">
|
<g fill="#980000" fill-opacity="0.88">
|
||||||
<path d="M10.5867 17.3522C10.0727 17.4492 9.54226 17.5 9 17.5C4.30558 17.5 0.5 13.6944 0.5 9C0.5 4.30558 4.30558 0.5 9 0.5C13.6944 0.5 17.5 4.30558 17.5 9C17.5 9.54226 17.4492 10.0727 17.3522 10.5867C16.6511 10.2123 15.8503 10 15 10C12.2386 10 10 12.2386 10 15C10 15.8503 10.2123 16.6511 10.5867 17.3522Z" fill="white"/>
|
<path d="M9,9 Q6.4,4.8 9,1.2 Q11.6,4.8 9,9Z" transform="rotate(0,9,9)"/>
|
||||||
<path d="M10 6.39999C10 6.67614 9.77614 6.89999 9.5 6.89999C9.22386 6.89999 9 6.67614 9 6.39999C9 6.12385 9.22386 5.89999 9.5 5.89999C9.77614 5.89999 10 6.12385 10 6.39999Z" fill="black"/>
|
<path d="M9,9 Q6.4,4.8 9,1.2 Q11.6,4.8 9,9Z" transform="rotate(45,9,9)"/>
|
||||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M9 0C4 0 0 4 0 9C0 14 4 18 9 18C9.63967 18 10.263 17.9345 10.8636 17.8099C10.3186 17.0091 10 16.0417 10 15C10 12.2386 12.2386 10 15 10C16.0417 10 17.0091 10.3186 17.8099 10.8636C17.9345 10.263 18 9.63967 18 9C18 4 14 0 9 0ZM1.2 10.8L4.7 8.5V8.2C4.7 6.4 6 5 7.8 4.8H8.2C9.4 4.8 10.5 5.4 11.1 6.4C11.4 6.3 11.7 6.3 12 6.3C12.4 6.3 12.8 6.3 13.2 6.4C13.9 6.6 14.6 6.9 15.2 7.3C14.6 7.1 14 7 13.3 7C12.1 7 11.1 7.4 10.4 8.4C9.7 9.3 9.3 10.4 9.3 11.6C9.3 13.1 8.9 14.5 8 15.8C7.93744 15.8834 7.87923 15.9625 7.82356 16.0381C7.6123 16.325 7.43739 16.5626 7.2 16.8C4.2 16.1 1.9 13.8 1.2 10.8Z" fill="black"/>
|
<path d="M9,9 Q6.4,4.8 9,1.2 Q11.6,4.8 9,9Z" transform="rotate(90,9,9)"/>
|
||||||
<path d="M18 15C18 16.6569 16.6569 18 15 18C13.3431 18 12 16.6569 12 15C12 13.3431 13.3431 12 15 12C16.6569 12 18 13.3431 18 15Z" fill="#989898"/>
|
<path d="M9,9 Q6.4,4.8 9,1.2 Q11.6,4.8 9,9Z" transform="rotate(135,9,9)"/>
|
||||||
</g>
|
<path d="M9,9 Q6.4,4.8 9,1.2 Q11.6,4.8 9,9Z" transform="rotate(180,9,9)"/>
|
||||||
<defs>
|
<path d="M9,9 Q6.4,4.8 9,1.2 Q11.6,4.8 9,9Z" transform="rotate(225,9,9)"/>
|
||||||
<clipPath id="clip0_2707_2015">
|
<path d="M9,9 Q6.4,4.8 9,1.2 Q11.6,4.8 9,9Z" transform="rotate(270,9,9)"/>
|
||||||
<rect width="18" height="18" fill="white"/>
|
<path d="M9,9 Q6.4,4.8 9,1.2 Q11.6,4.8 9,9Z" transform="rotate(315,9,9)"/>
|
||||||
</clipPath>
|
</g>
|
||||||
</defs>
|
<circle cx="9" cy="9" r="2.2" fill="#cc2000"/>
|
||||||
</svg>
|
<circle cx="14.5" cy="14.5" r="3" fill="#989898"/>
|
||||||
|
</svg>
|
||||||
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 841 B |
@@ -1,19 +1,13 @@
|
|||||||
<!-- Generator: Adobe Illustrator 18.0.0, SVG Export Plug-In -->
|
<svg viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
<svg version="1.1"
|
<g fill="#980000" fill-opacity="0.88">
|
||||||
xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:a="http://ns.adobe.com/AdobeSVGViewerExtensions/3.0/"
|
<path d="M9,9 Q6.4,4.8 9,1.2 Q11.6,4.8 9,9Z" transform="rotate(0,9,9)"/>
|
||||||
x="0px" y="0px" width="18px" height="18px" viewBox="0 0 18 18" enable-background="new 0 0 18 18" xml:space="preserve">
|
<path d="M9,9 Q6.4,4.8 9,1.2 Q11.6,4.8 9,9Z" transform="rotate(45,9,9)"/>
|
||||||
<defs>
|
<path d="M9,9 Q6.4,4.8 9,1.2 Q11.6,4.8 9,9Z" transform="rotate(90,9,9)"/>
|
||||||
</defs>
|
<path d="M9,9 Q6.4,4.8 9,1.2 Q11.6,4.8 9,9Z" transform="rotate(135,9,9)"/>
|
||||||
<g>
|
<path d="M9,9 Q6.4,4.8 9,1.2 Q11.6,4.8 9,9Z" transform="rotate(180,9,9)"/>
|
||||||
<g>
|
<path d="M9,9 Q6.4,4.8 9,1.2 Q11.6,4.8 9,9Z" transform="rotate(225,9,9)"/>
|
||||||
<circle fill="#FFFFFF" cx="9" cy="9" r="8.5"/>
|
<path d="M9,9 Q6.4,4.8 9,1.2 Q11.6,4.8 9,9Z" transform="rotate(270,9,9)"/>
|
||||||
</g>
|
<path d="M9,9 Q6.4,4.8 9,1.2 Q11.6,4.8 9,9Z" transform="rotate(315,9,9)"/>
|
||||||
<g>
|
</g>
|
||||||
<path d="M9,0C4,0,0,4,0,9c0,5,4,9,9,9c5,0,9-4,9-9C18,4,14,0,9,0z M1.2,10.8l3.5-2.3c0-0.1,0-0.2,0-0.3c0-1.8,1.3-3.2,3.1-3.4
|
<circle cx="9" cy="9" r="2.2" fill="#cc2000"/>
|
||||||
c0.1,0,0.2,0,0.4,0c1.2,0,2.3,0.6,2.9,1.6c0.3-0.1,0.6-0.1,0.9-0.1c0.4,0,0.8,0,1.2,0.1c0.7,0.2,1.4,0.5,2,0.9
|
|
||||||
C14.6,7.1,14,7,13.3,7c-1.2,0-2.2,0.4-2.9,1.4c-0.7,0.9-1.1,2-1.1,3.2c0,1.5-0.4,2.9-1.3,4.2c-0.3,0.4-0.5,0.7-0.8,1
|
|
||||||
C4.2,16.1,1.9,13.8,1.2,10.8z"/>
|
|
||||||
<circle cx="9.5" cy="6.4" r="0.5"/>
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
</svg>
|
</svg>
|
||||||
|
Before Width: | Height: | Size: 871 B After Width: | Height: | Size: 788 B |
@@ -0,0 +1,35 @@
|
|||||||
|
import { readFileSync, writeFileSync } from 'fs';
|
||||||
|
import { fileURLToPath } from 'url';
|
||||||
|
import { join, dirname } from 'path';
|
||||||
|
|
||||||
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||||
|
const foldsPath = join(__dirname, '../node_modules/folds/dist/index.js');
|
||||||
|
|
||||||
|
try {
|
||||||
|
let content = readFileSync(foldsPath, 'utf8');
|
||||||
|
|
||||||
|
// Defensive guard: if src is not a function, render null instead of crashing
|
||||||
|
const original = 'children: src(filled)';
|
||||||
|
const patched = 'children: typeof src === "function" ? src(filled) : null';
|
||||||
|
|
||||||
|
if (content.includes(patched)) {
|
||||||
|
console.log('folds patch already applied.');
|
||||||
|
} else if (content.includes(original)) {
|
||||||
|
content = content.replace(original, patched);
|
||||||
|
writeFileSync(foldsPath, content, 'utf8');
|
||||||
|
console.log('Applied defensive Icon src guard to folds.');
|
||||||
|
} else {
|
||||||
|
// Genuine "patch could not be applied" case: the target string is gone
|
||||||
|
// (folds renamed/restructured it) AND it isn't already patched. Fail hard
|
||||||
|
// so the postinstall hook / CI breaks loudly instead of silently shipping
|
||||||
|
// an unpatched folds (which crashes at render with "src is not a function").
|
||||||
|
console.error(
|
||||||
|
'ERROR: folds Icon patch target not found - folds may have updated. ' +
|
||||||
|
'Update the patch target string in scripts/patch-folds.mjs before building.',
|
||||||
|
);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('ERROR: Could not patch folds:', e.message);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
@@ -0,0 +1,123 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
/**
|
||||||
|
* Syncs avatarDecorations.ts with what's actually available on the Nextcloud CDN.
|
||||||
|
*
|
||||||
|
* Usage:
|
||||||
|
* npm run sync:decorations
|
||||||
|
*
|
||||||
|
* Workflow after deleting files from Nextcloud:
|
||||||
|
* 1. Delete decoration files from your Nextcloud share.
|
||||||
|
* 2. Run: npm run sync:decorations
|
||||||
|
* 3. It probes each catalog slug via HTTP HEAD and removes entries
|
||||||
|
* whose files returned 404. Empty categories are dropped automatically.
|
||||||
|
* 4. Commit the updated avatarDecorations.ts.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { readFileSync, writeFileSync } from 'fs';
|
||||||
|
import { join, dirname } from 'path';
|
||||||
|
import { fileURLToPath } from 'url';
|
||||||
|
|
||||||
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||||
|
const root = join(__dirname, '..');
|
||||||
|
const catalogPath = join(root, 'src', 'app', 'features', 'lotus', 'avatarDecorations.ts');
|
||||||
|
|
||||||
|
// Single source of truth: the CDN base URL lives in avatarDecorations.ts as
|
||||||
|
// `export const DECORATION_CDN`. We extract it from there at runtime rather than
|
||||||
|
// re-declaring it here, so the build script and the app can never drift. This
|
||||||
|
// .mjs script can't cleanly import the browser-side .ts module (it's outside the
|
||||||
|
// Vite/TS app graph), so we parse the constant out of the file text instead.
|
||||||
|
// If you migrate the CDN, change it ONLY in avatarDecorations.ts.
|
||||||
|
const catalog = readFileSync(catalogPath, 'utf8');
|
||||||
|
|
||||||
|
const cdnMatch = catalog.match(/export const DECORATION_CDN\s*=\s*['"]([^'"]+)['"]/);
|
||||||
|
if (!cdnMatch) {
|
||||||
|
console.error(
|
||||||
|
'Could not find `export const DECORATION_CDN` in avatarDecorations.ts — ' +
|
||||||
|
'the constant may have been renamed. Update scripts/syncDecorations.mjs.',
|
||||||
|
);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
const CDN = cdnMatch[1];
|
||||||
|
|
||||||
|
// Extract all slugs from the catalog file
|
||||||
|
const slugMatches = [...catalog.matchAll(/slug: '([^']+)'/g)].map((m) => m[1]);
|
||||||
|
|
||||||
|
if (slugMatches.length === 0) {
|
||||||
|
console.error('No slugs found in catalog — check the file path.');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`Checking ${slugMatches.length} decorations against ${CDN} …`);
|
||||||
|
console.log('(This makes one HEAD request per decoration)\n');
|
||||||
|
|
||||||
|
// Probe all slugs in parallel batches of 16
|
||||||
|
async function headCheck(slug) {
|
||||||
|
try {
|
||||||
|
const res = await fetch(`${CDN}/${slug}.png`, { method: 'HEAD' });
|
||||||
|
return { slug, ok: res.ok, status: res.status };
|
||||||
|
} catch {
|
||||||
|
// Network/DNS/TLS failure — NOT a confirmation the file is gone.
|
||||||
|
return { slug, ok: false, status: 0, networkError: true };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const BATCH = 16;
|
||||||
|
const results = [];
|
||||||
|
for (let i = 0; i < slugMatches.length; i += BATCH) {
|
||||||
|
const batch = slugMatches.slice(i, i + BATCH);
|
||||||
|
const batchResults = await Promise.all(batch.map(headCheck));
|
||||||
|
results.push(...batchResults);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only a CONFIRMED HTTP 404 means the file is genuinely gone and safe to
|
||||||
|
// remove. A network error or any other non-ok status (5xx, 403, timeout) is
|
||||||
|
// ambiguous — the CDN may be unreachable — so refuse to remove anything and
|
||||||
|
// abort, otherwise a transient outage would wipe the whole catalog from source
|
||||||
|
// control (N119).
|
||||||
|
const transient = results.filter((r) => !r.ok && r.status !== 404);
|
||||||
|
if (transient.length > 0) {
|
||||||
|
console.error(
|
||||||
|
`Aborting: ${transient.length} decoration(s) returned a non-404 failure ` +
|
||||||
|
`(network error / server error). The CDN may be unreachable — refusing to ` +
|
||||||
|
`remove entries to avoid wiping the catalog.`,
|
||||||
|
);
|
||||||
|
transient
|
||||||
|
.slice(0, 8)
|
||||||
|
.forEach((r) =>
|
||||||
|
console.error(` ${r.slug}: ${r.networkError ? 'network error' : `HTTP ${r.status}`}`),
|
||||||
|
);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const missing = results.filter((r) => r.status === 404);
|
||||||
|
const found = results.filter((r) => r.ok);
|
||||||
|
|
||||||
|
if (missing.length === 0) {
|
||||||
|
console.log(`All ${found.length} decorations are available — catalog is up to date.`);
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`Found: ${found.length} Missing: ${missing.length}\n`);
|
||||||
|
missing.forEach((r) => console.log(` Removing (HTTP ${r.status}): ${r.slug}`));
|
||||||
|
|
||||||
|
const missingSet = new Set(missing.map((r) => r.slug));
|
||||||
|
|
||||||
|
// Remove individual entries for missing slugs
|
||||||
|
let updated = catalog.replace(/^[ \t]*\{ slug: '([^']+)', name: .+\},?\r?\n/gm, (match, slug) =>
|
||||||
|
missingSet.has(slug) ? '' : match,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Drop category blocks that now have an empty decorations array
|
||||||
|
updated = updated.replace(
|
||||||
|
/ \{\n id: '[^']+',\n label: '[^']+',\n decorations: \[\n?[ \t]*\],?\n \},?\n/g,
|
||||||
|
'',
|
||||||
|
);
|
||||||
|
|
||||||
|
// Clean up stray blank lines
|
||||||
|
updated = updated.replace(/\n{3,}/g, '\n\n');
|
||||||
|
|
||||||
|
writeFileSync(catalogPath, updated, 'utf8');
|
||||||
|
console.log(
|
||||||
|
`\nDone. Removed ${missing.length} entr${missing.length === 1 ? 'y' : 'ies'} from the catalog.`,
|
||||||
|
);
|
||||||
|
console.log('Review with: git diff src/app/features/lotus/avatarDecorations.ts');
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import fs from "fs";
|
import fs from 'fs';
|
||||||
import path from "path";
|
import path from 'path';
|
||||||
import { execSync } from "child_process";
|
import { execSync } from 'child_process';
|
||||||
import { fileURLToPath } from "url";
|
import { fileURLToPath } from 'url';
|
||||||
|
|
||||||
const __filename = fileURLToPath(import.meta.url);
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
const __dirname = path.dirname(__filename);
|
const __dirname = path.dirname(__filename);
|
||||||
@@ -9,26 +9,26 @@ const __dirname = path.dirname(__filename);
|
|||||||
const version = process.argv[2];
|
const version = process.argv[2];
|
||||||
|
|
||||||
if (!version) {
|
if (!version) {
|
||||||
console.error("Version argument missing");
|
console.error('Version argument missing');
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
const root = path.resolve(__dirname, "..");
|
const root = path.resolve(__dirname, '..');
|
||||||
const newVersionTag = `v${version}`;
|
const newVersionTag = `v${version}`;
|
||||||
|
|
||||||
// Update package.json + package-lock.json safely
|
// Update package.json + package-lock.json safely
|
||||||
execSync(`npm version ${version} --no-git-tag-version`, {
|
execSync(`npm version ${version} --no-git-tag-version`, {
|
||||||
cwd: root,
|
cwd: root,
|
||||||
stdio: "inherit",
|
stdio: 'inherit',
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log(`Updated package.json and package-lock.json → ${version}`);
|
console.log(`Updated package.json and package-lock.json → ${version}`);
|
||||||
|
|
||||||
// Update UI version references
|
// Update UI version references
|
||||||
const files = [
|
const files = [
|
||||||
"src/app/features/settings/about/About.tsx",
|
'src/app/features/settings/about/About.tsx',
|
||||||
"src/app/pages/auth/AuthFooter.tsx",
|
'src/app/pages/auth/AuthFooter.tsx',
|
||||||
"src/app/pages/client/WelcomePage.tsx",
|
'src/app/pages/client/WelcomePage.tsx',
|
||||||
];
|
];
|
||||||
|
|
||||||
files.forEach((filePath) => {
|
files.forEach((filePath) => {
|
||||||
@@ -39,10 +39,10 @@ files.forEach((filePath) => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const content = fs.readFileSync(absPath, "utf8");
|
const content = fs.readFileSync(absPath, 'utf8');
|
||||||
const updated = content.replace(/v\d+\.\d+\.\d+/g, newVersionTag);
|
const updated = content.replace(/v\d+\.\d+\.\d+/g, newVersionTag);
|
||||||
|
|
||||||
fs.writeFileSync(absPath, updated);
|
fs.writeFileSync(absPath, updated);
|
||||||
|
|
||||||
console.log(`Updated ${filePath} → ${newVersionTag}`);
|
console.log(`Updated ${filePath} → ${newVersionTag}`);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ function AccountDataEdit({
|
|||||||
|
|
||||||
const { handleKeyDown, operations, getTarget } = useTextAreaCodeEditor(
|
const { handleKeyDown, operations, getTarget } = useTextAreaCodeEditor(
|
||||||
textAreaRef,
|
textAreaRef,
|
||||||
EDITOR_INTENT_SPACE_COUNT
|
EDITOR_INTENT_SPACE_COUNT,
|
||||||
);
|
);
|
||||||
|
|
||||||
const [submitState, submit] = useAsyncCallback<void, MatrixError, [string, object]>(submitChange);
|
const [submitState, submit] = useAsyncCallback<void, MatrixError, [string, object]>(submitChange);
|
||||||
@@ -127,6 +127,7 @@ function AccountDataEdit({
|
|||||||
<Input
|
<Input
|
||||||
variant={type.length > 0 || submitting ? 'SurfaceVariant' : 'Background'}
|
variant={type.length > 0 || submitting ? 'SurfaceVariant' : 'Background'}
|
||||||
name="typeInput"
|
name="typeInput"
|
||||||
|
aria-label="Account data type"
|
||||||
size="400"
|
size="400"
|
||||||
radii="300"
|
radii="300"
|
||||||
readOnly={type.length > 0 || submitting}
|
readOnly={type.length > 0 || submitting}
|
||||||
@@ -170,6 +171,7 @@ function AccountDataEdit({
|
|||||||
<TextAreaComponent
|
<TextAreaComponent
|
||||||
ref={textAreaRef}
|
ref={textAreaRef}
|
||||||
name="contentTextArea"
|
name="contentTextArea"
|
||||||
|
aria-label="JSON content"
|
||||||
style={{
|
style={{
|
||||||
fontFamily: 'monospace',
|
fontFamily: 'monospace',
|
||||||
}}
|
}}
|
||||||
@@ -276,7 +278,7 @@ export function AccountDataEditor({
|
|||||||
|
|
||||||
const contentJSONStr = useMemo(
|
const contentJSONStr = useMemo(
|
||||||
() => JSON.stringify(data.content, null, EDITOR_INTENT_SPACE_COUNT),
|
() => JSON.stringify(data.content, null, EDITOR_INTENT_SPACE_COUNT),
|
||||||
[data.content]
|
[data.content],
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -294,7 +296,7 @@ export function AccountDataEditor({
|
|||||||
</Chip>
|
</Chip>
|
||||||
</Box>
|
</Box>
|
||||||
<Box shrink="No">
|
<Box shrink="No">
|
||||||
<IconButton onClick={requestClose} variant="Surface">
|
<IconButton onClick={requestClose} variant="Surface" aria-label="Close">
|
||||||
<Icon src={Icons.Cross} />
|
<Icon src={Icons.Cross} />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
@@ -45,11 +45,11 @@ export function AuthFlowsLoader({ fallback, error, children }: AuthFlowsLoaderPr
|
|||||||
};
|
};
|
||||||
|
|
||||||
return authFlows;
|
return authFlows;
|
||||||
}, [mx])
|
}, [mx]),
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
load();
|
load().catch(() => {});
|
||||||
}, [load]);
|
}, [load]);
|
||||||
|
|
||||||
if (state.status === AsyncStatus.Idle || state.status === AsyncStatus.Loading) {
|
if (state.status === AsyncStatus.Idle || state.status === AsyncStatus.Loading) {
|
||||||
|
|||||||
@@ -0,0 +1,71 @@
|
|||||||
|
import React, { useId } from 'react';
|
||||||
|
|
||||||
|
export function AuthSkeleton() {
|
||||||
|
const id = useId().replace(/:/g, '');
|
||||||
|
const shimmerKeyframes = `
|
||||||
|
@keyframes shimmer-${id} {
|
||||||
|
0% { background-position: -400px 0; }
|
||||||
|
100% { background-position: 400px 0; }
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const shimmer = {
|
||||||
|
background:
|
||||||
|
'linear-gradient(90deg, var(--skeleton-base) 25%, var(--skeleton-shine) 50%, var(--skeleton-base) 75%)',
|
||||||
|
backgroundSize: '800px 100%',
|
||||||
|
animation: `shimmer-${id} 1.6s ease-in-out infinite`,
|
||||||
|
borderRadius: '4px',
|
||||||
|
} as React.CSSProperties;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<style>{shimmerKeyframes}</style>
|
||||||
|
<div
|
||||||
|
style={
|
||||||
|
{
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
minHeight: '100dvh',
|
||||||
|
padding: '16px',
|
||||||
|
'--skeleton-base': 'color-mix(in srgb, currentColor 8%, transparent)',
|
||||||
|
'--skeleton-shine': 'color-mix(in srgb, currentColor 15%, transparent)',
|
||||||
|
} as React.CSSProperties
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{/* Card */}
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
width: '100%',
|
||||||
|
maxWidth: '360px',
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
gap: '24px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* Logo + app name */}
|
||||||
|
<div
|
||||||
|
style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: '12px' }}
|
||||||
|
>
|
||||||
|
<div style={{ ...shimmer, width: '64px', height: '64px', borderRadius: '50%' }} />
|
||||||
|
<div style={{ ...shimmer, width: '100px', height: '20px' }} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Server picker */}
|
||||||
|
<div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
|
||||||
|
<div style={{ ...shimmer, width: '80px', height: '12px' }} />
|
||||||
|
<div style={{ ...shimmer, width: '100%', height: '40px', borderRadius: '8px' }} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Form fields */}
|
||||||
|
<div style={{ display: 'flex', flexDirection: 'column', gap: '12px' }}>
|
||||||
|
<div style={{ ...shimmer, width: '100%', height: '40px', borderRadius: '8px' }} />
|
||||||
|
<div style={{ ...shimmer, width: '100%', height: '40px', borderRadius: '8px' }} />
|
||||||
|
<div style={{ ...shimmer, width: '100%', height: '40px', borderRadius: '8px' }} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -24,7 +24,7 @@ export function BackRouteHandler({ children }: BackRouteHandlerProps) {
|
|||||||
caseSensitive: true,
|
caseSensitive: true,
|
||||||
end: false,
|
end: false,
|
||||||
},
|
},
|
||||||
location.pathname
|
location.pathname,
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
navigate(getHomePath());
|
navigate(getHomePath());
|
||||||
@@ -37,7 +37,7 @@ export function BackRouteHandler({ children }: BackRouteHandlerProps) {
|
|||||||
caseSensitive: true,
|
caseSensitive: true,
|
||||||
end: false,
|
end: false,
|
||||||
},
|
},
|
||||||
location.pathname
|
location.pathname,
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
navigate(getDirectPath());
|
navigate(getDirectPath());
|
||||||
@@ -49,7 +49,7 @@ export function BackRouteHandler({ children }: BackRouteHandlerProps) {
|
|||||||
caseSensitive: true,
|
caseSensitive: true,
|
||||||
end: false,
|
end: false,
|
||||||
},
|
},
|
||||||
location.pathname
|
location.pathname,
|
||||||
);
|
);
|
||||||
const encodedSpaceIdOrAlias = spaceMatch?.params.spaceIdOrAlias;
|
const encodedSpaceIdOrAlias = spaceMatch?.params.spaceIdOrAlias;
|
||||||
const decodedSpaceIdOrAlias =
|
const decodedSpaceIdOrAlias =
|
||||||
@@ -66,7 +66,7 @@ export function BackRouteHandler({ children }: BackRouteHandlerProps) {
|
|||||||
caseSensitive: true,
|
caseSensitive: true,
|
||||||
end: false,
|
end: false,
|
||||||
},
|
},
|
||||||
location.pathname
|
location.pathname,
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
navigate(getExplorePath());
|
navigate(getExplorePath());
|
||||||
@@ -79,7 +79,7 @@ export function BackRouteHandler({ children }: BackRouteHandlerProps) {
|
|||||||
caseSensitive: true,
|
caseSensitive: true,
|
||||||
end: false,
|
end: false,
|
||||||
},
|
},
|
||||||
location.pathname
|
location.pathname,
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
navigate(getInboxPath());
|
navigate(getInboxPath());
|
||||||
|
|||||||
@@ -157,7 +157,7 @@ export function BackupRestoreTile({ crypto }: BackupRestoreTileProps) {
|
|||||||
setRestoreProgress(progress);
|
setRestoreProgress(progress);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}, [crypto, setRestoreProgress])
|
}, [crypto, setRestoreProgress]),
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleRestore = () => {
|
const handleRestore = () => {
|
||||||
@@ -178,6 +178,7 @@ export function BackupRestoreTile({ crypto }: BackupRestoreTileProps) {
|
|||||||
)}
|
)}
|
||||||
<IconButton
|
<IconButton
|
||||||
aria-pressed={!!menuCords}
|
aria-pressed={!!menuCords}
|
||||||
|
aria-label="Backup options"
|
||||||
size="300"
|
size="300"
|
||||||
variant="Surface"
|
variant="Surface"
|
||||||
radii="300"
|
radii="300"
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ export function CapabilitiesLoader({ children }: CapabilitiesLoaderProps) {
|
|||||||
const [state, load] = useAsyncCallback(useCallback(() => mx.getCapabilities(), [mx]));
|
const [state, load] = useAsyncCallback(useCallback(() => mx.getCapabilities(), [mx]));
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
load();
|
load().catch(() => {});
|
||||||
}, [load]);
|
}, [load]);
|
||||||
|
|
||||||
return children(state.status === AsyncStatus.Success ? state.data : undefined);
|
return children(state.status === AsyncStatus.Success ? state.data : undefined);
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ export function ClientConfigLoader({ fallback, error, children }: ClientConfigLo
|
|||||||
const ignoreCallback = useCallback(() => setIgnoreError(true), []);
|
const ignoreCallback = useCallback(() => setIgnoreError(true), []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
load();
|
load().catch(() => undefined);
|
||||||
}, [load]);
|
}, [load]);
|
||||||
|
|
||||||
if (state.status === AsyncStatus.Idle || state.status === AsyncStatus.Loading) {
|
if (state.status === AsyncStatus.Idle || state.status === AsyncStatus.Loading) {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { ReactNode, RefObject, useCallback, useRef, useState } from 'react';
|
import React, { ReactNode, RefObject, useCallback, useRef, useState } from 'react';
|
||||||
import { useDebounce } from '../hooks/useDebounce';
|
import { useDebounce } from '../hooks/useDebounce';
|
||||||
|
|
||||||
type ConfirmPasswordMatchProps = {
|
type ConfirmPasswordMatchProps = {
|
||||||
@@ -7,13 +7,13 @@ type ConfirmPasswordMatchProps = {
|
|||||||
match: boolean,
|
match: boolean,
|
||||||
doMatch: () => void,
|
doMatch: () => void,
|
||||||
passRef: RefObject<HTMLInputElement>,
|
passRef: RefObject<HTMLInputElement>,
|
||||||
confPassRef: RefObject<HTMLInputElement>
|
confPassRef: RefObject<HTMLInputElement>,
|
||||||
) => ReactNode;
|
) => ReactNode;
|
||||||
};
|
};
|
||||||
export function ConfirmPasswordMatch({ initialValue, children }: ConfirmPasswordMatchProps) {
|
export function ConfirmPasswordMatch({ initialValue, children }: ConfirmPasswordMatchProps) {
|
||||||
const [match, setMatch] = useState(initialValue);
|
const [match, setMatch] = useState(initialValue);
|
||||||
const passRef = useRef<HTMLInputElement>(null);
|
const passRef = useRef<HTMLInputElement>(null) as React.RefObject<HTMLInputElement>;
|
||||||
const confPassRef = useRef<HTMLInputElement>(null);
|
const confPassRef = useRef<HTMLInputElement>(null) as React.RefObject<HTMLInputElement>;
|
||||||
|
|
||||||
const doMatch = useDebounce(
|
const doMatch = useDebounce(
|
||||||
useCallback(() => {
|
useCallback(() => {
|
||||||
@@ -28,7 +28,7 @@ export function ConfirmPasswordMatch({ initialValue, children }: ConfirmPassword
|
|||||||
{
|
{
|
||||||
wait: 500,
|
wait: 500,
|
||||||
immediate: false,
|
immediate: false,
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
return children(match, doMatch, passRef, confPassRef);
|
return children(match, doMatch, passRef, confPassRef);
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import {
|
|||||||
Verifier,
|
Verifier,
|
||||||
} from 'matrix-js-sdk/lib/crypto-api';
|
} from 'matrix-js-sdk/lib/crypto-api';
|
||||||
import React, { CSSProperties, useCallback, useEffect, useState } from 'react';
|
import React, { CSSProperties, useCallback, useEffect, useState } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
import { VerificationMethod } from 'matrix-js-sdk/lib/types';
|
import { VerificationMethod } from 'matrix-js-sdk/lib/types';
|
||||||
import {
|
import {
|
||||||
Box,
|
Box,
|
||||||
@@ -30,6 +31,7 @@ import {
|
|||||||
} from '../hooks/useVerificationRequest';
|
} from '../hooks/useVerificationRequest';
|
||||||
import { AsyncStatus, useAsyncCallback } from '../hooks/useAsyncCallback';
|
import { AsyncStatus, useAsyncCallback } from '../hooks/useAsyncCallback';
|
||||||
import { ContainerColor } from '../styles/ContainerColor.css';
|
import { ContainerColor } from '../styles/ContainerColor.css';
|
||||||
|
import { useModalStyle } from '../hooks/useModalStyle';
|
||||||
|
|
||||||
const DialogHeaderStyles: CSSProperties = {
|
const DialogHeaderStyles: CSSProperties = {
|
||||||
padding: `0 ${config.space.S200} 0 ${config.space.S400}`,
|
padding: `0 ${config.space.S200} 0 ${config.space.S400}`,
|
||||||
@@ -50,21 +52,23 @@ function WaitingMessage({ message }: WaitingMessageProps) {
|
|||||||
|
|
||||||
type VerificationUnexpectedProps = { message: string; onClose: () => void };
|
type VerificationUnexpectedProps = { message: string; onClose: () => void };
|
||||||
function VerificationUnexpected({ message, onClose }: VerificationUnexpectedProps) {
|
function VerificationUnexpected({ message, onClose }: VerificationUnexpectedProps) {
|
||||||
|
const { t } = useTranslation();
|
||||||
return (
|
return (
|
||||||
<Box direction="Column" gap="400">
|
<Box direction="Column" gap="400">
|
||||||
<Text>{message}</Text>
|
<Text>{message}</Text>
|
||||||
<Button variant="Secondary" fill="Soft" onClick={onClose}>
|
<Button variant="Secondary" fill="Soft" onClick={onClose}>
|
||||||
<Text size="B400">Close</Text>
|
<Text size="B400">{t('Organisms.DeviceVerification.close')}</Text>
|
||||||
</Button>
|
</Button>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function VerificationWaitAccept() {
|
function VerificationWaitAccept() {
|
||||||
|
const { t } = useTranslation();
|
||||||
return (
|
return (
|
||||||
<Box direction="Column" gap="400">
|
<Box direction="Column" gap="400">
|
||||||
<Text>Please accept the request from other device.</Text>
|
<Text>{t('Organisms.DeviceVerification.please_accept')}</Text>
|
||||||
<WaitingMessage message="Waiting for request to be accepted..." />
|
<WaitingMessage message={t('Organisms.DeviceVerification.waiting_accept')} />
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -73,12 +77,13 @@ type VerificationAcceptProps = {
|
|||||||
onAccept: () => Promise<void>;
|
onAccept: () => Promise<void>;
|
||||||
};
|
};
|
||||||
function VerificationAccept({ onAccept }: VerificationAcceptProps) {
|
function VerificationAccept({ onAccept }: VerificationAcceptProps) {
|
||||||
|
const { t } = useTranslation();
|
||||||
const [acceptState, accept] = useAsyncCallback(onAccept);
|
const [acceptState, accept] = useAsyncCallback(onAccept);
|
||||||
|
|
||||||
const accepting = acceptState.status === AsyncStatus.Loading;
|
const accepting = acceptState.status === AsyncStatus.Loading;
|
||||||
return (
|
return (
|
||||||
<Box direction="Column" gap="400">
|
<Box direction="Column" gap="400">
|
||||||
<Text>Click accept to start the verification process.</Text>
|
<Text>{t('Organisms.DeviceVerification.click_accept')}</Text>
|
||||||
<Button
|
<Button
|
||||||
variant="Primary"
|
variant="Primary"
|
||||||
fill="Solid"
|
fill="Solid"
|
||||||
@@ -86,17 +91,18 @@ function VerificationAccept({ onAccept }: VerificationAcceptProps) {
|
|||||||
before={accepting && <Spinner size="100" variant="Primary" fill="Solid" />}
|
before={accepting && <Spinner size="100" variant="Primary" fill="Solid" />}
|
||||||
disabled={accepting}
|
disabled={accepting}
|
||||||
>
|
>
|
||||||
<Text size="B400">Accept</Text>
|
<Text size="B400">{t('Organisms.DeviceVerification.accept')}</Text>
|
||||||
</Button>
|
</Button>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function VerificationWaitStart() {
|
function VerificationWaitStart() {
|
||||||
|
const { t } = useTranslation();
|
||||||
return (
|
return (
|
||||||
<Box direction="Column" gap="400">
|
<Box direction="Column" gap="400">
|
||||||
<Text>Verification request has been accepted.</Text>
|
<Text>{t('Organisms.DeviceVerification.request_accepted')}</Text>
|
||||||
<WaitingMessage message="Waiting for the response from other device..." />
|
<WaitingMessage message={t('Organisms.DeviceVerification.waiting_response')} />
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -105,18 +111,20 @@ type VerificationStartProps = {
|
|||||||
onStart: () => Promise<void>;
|
onStart: () => Promise<void>;
|
||||||
};
|
};
|
||||||
function AutoVerificationStart({ onStart }: VerificationStartProps) {
|
function AutoVerificationStart({ onStart }: VerificationStartProps) {
|
||||||
|
const { t } = useTranslation();
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
onStart();
|
onStart();
|
||||||
}, [onStart]);
|
}, [onStart]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box direction="Column" gap="400">
|
<Box direction="Column" gap="400">
|
||||||
<WaitingMessage message="Starting verification using emoji comparison..." />
|
<WaitingMessage message={t('Organisms.DeviceVerification.starting_emoji')} />
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function CompareEmoji({ sasData }: { sasData: ShowSasCallbacks }) {
|
function CompareEmoji({ sasData }: { sasData: ShowSasCallbacks }) {
|
||||||
|
const { t } = useTranslation();
|
||||||
const [confirmState, confirm] = useAsyncCallback(useCallback(() => sasData.confirm(), [sasData]));
|
const [confirmState, confirm] = useAsyncCallback(useCallback(() => sasData.confirm(), [sasData]));
|
||||||
|
|
||||||
const confirming =
|
const confirming =
|
||||||
@@ -124,7 +132,7 @@ function CompareEmoji({ sasData }: { sasData: ShowSasCallbacks }) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Box direction="Column" gap="400">
|
<Box direction="Column" gap="400">
|
||||||
<Text>Confirm the emoji below are displayed on both devices, in the same order:</Text>
|
<Text>{t('Organisms.DeviceVerification.confirm_emoji')}</Text>
|
||||||
<Box
|
<Box
|
||||||
className={ContainerColor({ variant: 'SurfaceVariant' })}
|
className={ContainerColor({ variant: 'SurfaceVariant' })}
|
||||||
style={{
|
style={{
|
||||||
@@ -137,7 +145,6 @@ function CompareEmoji({ sasData }: { sasData: ShowSasCallbacks }) {
|
|||||||
>
|
>
|
||||||
{sasData.sas.emoji?.map(([emoji, name], index) => (
|
{sasData.sas.emoji?.map(([emoji, name], index) => (
|
||||||
<Box
|
<Box
|
||||||
// eslint-disable-next-line react/no-array-index-key
|
|
||||||
key={`${emoji}${name}${index}`}
|
key={`${emoji}${name}${index}`}
|
||||||
direction="Column"
|
direction="Column"
|
||||||
gap="100"
|
gap="100"
|
||||||
@@ -157,7 +164,7 @@ function CompareEmoji({ sasData }: { sasData: ShowSasCallbacks }) {
|
|||||||
disabled={confirming}
|
disabled={confirming}
|
||||||
before={confirming && <Spinner size="100" variant="Primary" />}
|
before={confirming && <Spinner size="100" variant="Primary" />}
|
||||||
>
|
>
|
||||||
<Text size="B400">They Match</Text>
|
<Text size="B400">{t('Organisms.DeviceVerification.they_match')}</Text>
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
variant="Primary"
|
variant="Primary"
|
||||||
@@ -165,7 +172,7 @@ function CompareEmoji({ sasData }: { sasData: ShowSasCallbacks }) {
|
|||||||
onClick={() => sasData.mismatch()}
|
onClick={() => sasData.mismatch()}
|
||||||
disabled={confirming}
|
disabled={confirming}
|
||||||
>
|
>
|
||||||
<Text size="B400">Do not Match</Text>
|
<Text size="B400">{t('Organisms.DeviceVerification.do_not_match')}</Text>
|
||||||
</Button>
|
</Button>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
@@ -177,6 +184,7 @@ type SasVerificationProps = {
|
|||||||
onCancel: () => void;
|
onCancel: () => void;
|
||||||
};
|
};
|
||||||
function SasVerification({ verifier, onCancel }: SasVerificationProps) {
|
function SasVerification({ verifier, onCancel }: SasVerificationProps) {
|
||||||
|
const { t } = useTranslation();
|
||||||
const [sasData, setSasData] = useState<ShowSasCallbacks>();
|
const [sasData, setSasData] = useState<ShowSasCallbacks>();
|
||||||
|
|
||||||
useVerifierShowSas(verifier, setSasData);
|
useVerifierShowSas(verifier, setSasData);
|
||||||
@@ -192,7 +200,7 @@ function SasVerification({ verifier, onCancel }: SasVerificationProps) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Box direction="Column" gap="400">
|
<Box direction="Column" gap="400">
|
||||||
<WaitingMessage message="Starting verification using emoji comparison..." />
|
<WaitingMessage message={t('Organisms.DeviceVerification.starting_emoji')} />
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -201,13 +209,14 @@ type VerificationDoneProps = {
|
|||||||
onExit: () => void;
|
onExit: () => void;
|
||||||
};
|
};
|
||||||
function VerificationDone({ onExit }: VerificationDoneProps) {
|
function VerificationDone({ onExit }: VerificationDoneProps) {
|
||||||
|
const { t } = useTranslation();
|
||||||
return (
|
return (
|
||||||
<Box direction="Column" gap="400">
|
<Box direction="Column" gap="400">
|
||||||
<div>
|
<div>
|
||||||
<Text>Your device is verified.</Text>
|
<Text>{t('Organisms.DeviceVerification.device_verified')}</Text>
|
||||||
</div>
|
</div>
|
||||||
<Button variant="Primary" fill="Solid" onClick={onExit}>
|
<Button variant="Primary" fill="Solid" onClick={onExit}>
|
||||||
<Text size="B400">Okay</Text>
|
<Text size="B400">{t('Organisms.DeviceVerification.okay')}</Text>
|
||||||
</Button>
|
</Button>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
@@ -217,11 +226,12 @@ type VerificationCanceledProps = {
|
|||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
};
|
};
|
||||||
function VerificationCanceled({ onClose }: VerificationCanceledProps) {
|
function VerificationCanceled({ onClose }: VerificationCanceledProps) {
|
||||||
|
const { t } = useTranslation();
|
||||||
return (
|
return (
|
||||||
<Box direction="Column" gap="400">
|
<Box direction="Column" gap="400">
|
||||||
<Text>Verification has been canceled.</Text>
|
<Text>{t('Organisms.DeviceVerification.verification_canceled')}</Text>
|
||||||
<Button variant="Secondary" fill="Soft" onClick={onClose}>
|
<Button variant="Secondary" fill="Soft" onClick={onClose}>
|
||||||
<Text size="B400">Close</Text>
|
<Text size="B400">{t('Organisms.DeviceVerification.close')}</Text>
|
||||||
</Button>
|
</Button>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
@@ -233,6 +243,7 @@ type DeviceVerificationProps = {
|
|||||||
};
|
};
|
||||||
export function DeviceVerification({ request, onExit }: DeviceVerificationProps) {
|
export function DeviceVerification({ request, onExit }: DeviceVerificationProps) {
|
||||||
const phase = useVerificationRequestPhase(request);
|
const phase = useVerificationRequestPhase(request);
|
||||||
|
const modalStyle = useModalStyle(480);
|
||||||
|
|
||||||
const handleCancel = useCallback(() => {
|
const handleCancel = useCallback(() => {
|
||||||
if (request.phase !== VerificationPhase.Done && request.phase !== VerificationPhase.Cancelled) {
|
if (request.phase !== VerificationPhase.Done && request.phase !== VerificationPhase.Cancelled) {
|
||||||
@@ -256,12 +267,19 @@ export function DeviceVerification({ request, onExit }: DeviceVerificationProps)
|
|||||||
escapeDeactivates: false,
|
escapeDeactivates: false,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Dialog variant="Surface">
|
<Dialog variant="Surface" style={modalStyle}>
|
||||||
<Header style={DialogHeaderStyles} variant="Surface" size="500">
|
<Header style={DialogHeaderStyles} variant="Surface" size="500">
|
||||||
<Box grow="Yes">
|
<Box grow="Yes">
|
||||||
<Text size="H4">Device Verification</Text>
|
<Text as="h2" size="H4">
|
||||||
|
Device Verification
|
||||||
|
</Text>
|
||||||
</Box>
|
</Box>
|
||||||
<IconButton size="300" radii="300" onClick={handleCancel}>
|
<IconButton
|
||||||
|
size="300"
|
||||||
|
radii="300"
|
||||||
|
onClick={handleCancel}
|
||||||
|
aria-label="Cancel verification"
|
||||||
|
>
|
||||||
<Icon src={Icons.Cross} />
|
<Icon src={Icons.Cross} />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</Header>
|
</Header>
|
||||||
@@ -310,9 +328,5 @@ export function ReceiveSelfDeviceVerification() {
|
|||||||
|
|
||||||
if (!request) return null;
|
if (!request) return null;
|
||||||
|
|
||||||
if (!request.isSelfVerification) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return <DeviceVerification request={request} onExit={handleExit} />;
|
return <DeviceVerification request={request} onExit={handleExit} />;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import {
|
|||||||
import FileSaver from 'file-saver';
|
import FileSaver from 'file-saver';
|
||||||
import to from 'await-to-js';
|
import to from 'await-to-js';
|
||||||
import { AuthDict, IAuthData, MatrixError, UIAuthCallback } from 'matrix-js-sdk';
|
import { AuthDict, IAuthData, MatrixError, UIAuthCallback } from 'matrix-js-sdk';
|
||||||
|
import { useModalStyle } from '../hooks/useModalStyle';
|
||||||
import { PasswordInput } from './password-input';
|
import { PasswordInput } from './password-input';
|
||||||
import { ContainerColor } from '../styles/ContainerColor.css';
|
import { ContainerColor } from '../styles/ContainerColor.css';
|
||||||
import { copyToClipboard } from '../utils/dom';
|
import { copyToClipboard } from '../utils/dom';
|
||||||
@@ -27,7 +28,7 @@ import { useAlive } from '../hooks/useAlive';
|
|||||||
import { UseStateProvider } from './UseStateProvider';
|
import { UseStateProvider } from './UseStateProvider';
|
||||||
|
|
||||||
type UIACallback<T> = (
|
type UIACallback<T> = (
|
||||||
authDict: AuthDict | null
|
authDict: AuthDict | null,
|
||||||
) => Promise<[IAuthData, undefined] | [undefined, T]>;
|
) => Promise<[IAuthData, undefined] | [undefined, T]>;
|
||||||
|
|
||||||
type PerformAction<T> = (authDict: AuthDict | null) => Promise<T>;
|
type PerformAction<T> = (authDict: AuthDict | null) => Promise<T>;
|
||||||
@@ -42,7 +43,7 @@ function makeUIAAction<T>(
|
|||||||
authData: IAuthData,
|
authData: IAuthData,
|
||||||
performAction: PerformAction<T>,
|
performAction: PerformAction<T>,
|
||||||
resolve: (data: T) => void,
|
resolve: (data: T) => void,
|
||||||
reject: (error?: any) => void
|
reject: (error?: any) => void,
|
||||||
): UIAAction<T> {
|
): UIAAction<T> {
|
||||||
const action: UIAAction<T> = {
|
const action: UIAAction<T> = {
|
||||||
authData,
|
authData,
|
||||||
@@ -91,7 +92,7 @@ function SetupVerification({ onComplete }: SetupVerificationProps) {
|
|||||||
setNextAuthData(authData);
|
setNextAuthData(authData);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[uiaAction, alive]
|
[uiaAction, alive],
|
||||||
);
|
);
|
||||||
|
|
||||||
const resetUIA = useCallback(() => {
|
const resetUIA = useCallback(() => {
|
||||||
@@ -118,7 +119,7 @@ function SetupVerification({ onComplete }: SetupVerificationProps) {
|
|||||||
(err) => {
|
(err) => {
|
||||||
resetUIA();
|
resetUIA();
|
||||||
reject(err);
|
reject(err);
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
if (alive()) {
|
if (alive()) {
|
||||||
setUIAAction(action);
|
setUIAAction(action);
|
||||||
@@ -130,7 +131,7 @@ function SetupVerification({ onComplete }: SetupVerificationProps) {
|
|||||||
reject(error);
|
reject(error);
|
||||||
});
|
});
|
||||||
}),
|
}),
|
||||||
[alive, resetUIA]
|
[alive, resetUIA],
|
||||||
);
|
);
|
||||||
|
|
||||||
const [setupState, setup] = useAsyncCallback<void, Error, [string | undefined]>(
|
const [setupState, setup] = useAsyncCallback<void, Error, [string | undefined]>(
|
||||||
@@ -159,8 +160,8 @@ function SetupVerification({ onComplete }: SetupVerificationProps) {
|
|||||||
|
|
||||||
onComplete(recoveryKeyData.encodedPrivateKey);
|
onComplete(recoveryKeyData.encodedPrivateKey);
|
||||||
},
|
},
|
||||||
[mx, onComplete, authUploadDeviceSigningKeys]
|
[mx, onComplete, authUploadDeviceSigningKeys],
|
||||||
)
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
const loading = setupState.status === AsyncStatus.Loading;
|
const loading = setupState.status === AsyncStatus.Loading;
|
||||||
@@ -287,9 +288,10 @@ type DeviceVerificationSetupProps = {
|
|||||||
export const DeviceVerificationSetup = forwardRef<HTMLDivElement, DeviceVerificationSetupProps>(
|
export const DeviceVerificationSetup = forwardRef<HTMLDivElement, DeviceVerificationSetupProps>(
|
||||||
({ onCancel }, ref) => {
|
({ onCancel }, ref) => {
|
||||||
const [recoveryKey, setRecoveryKey] = useState<string>();
|
const [recoveryKey, setRecoveryKey] = useState<string>();
|
||||||
|
const modalStyle = useModalStyle(480);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog ref={ref}>
|
<Dialog ref={ref} style={modalStyle}>
|
||||||
<Header
|
<Header
|
||||||
style={{
|
style={{
|
||||||
padding: `0 ${config.space.S200} 0 ${config.space.S400}`,
|
padding: `0 ${config.space.S200} 0 ${config.space.S400}`,
|
||||||
@@ -299,9 +301,11 @@ export const DeviceVerificationSetup = forwardRef<HTMLDivElement, DeviceVerifica
|
|||||||
size="500"
|
size="500"
|
||||||
>
|
>
|
||||||
<Box grow="Yes">
|
<Box grow="Yes">
|
||||||
<Text size="H4">Setup Device Verification</Text>
|
<Text as="h2" size="H4">
|
||||||
|
Setup Device Verification
|
||||||
|
</Text>
|
||||||
</Box>
|
</Box>
|
||||||
<IconButton size="300" radii="300" onClick={onCancel}>
|
<IconButton size="300" radii="300" onClick={onCancel} aria-label="Cancel">
|
||||||
<Icon src={Icons.Cross} />
|
<Icon src={Icons.Cross} />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</Header>
|
</Header>
|
||||||
@@ -314,7 +318,7 @@ export const DeviceVerificationSetup = forwardRef<HTMLDivElement, DeviceVerifica
|
|||||||
</Box>
|
</Box>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
);
|
);
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
type DeviceVerificationResetProps = {
|
type DeviceVerificationResetProps = {
|
||||||
onCancel: () => void;
|
onCancel: () => void;
|
||||||
@@ -322,9 +326,10 @@ type DeviceVerificationResetProps = {
|
|||||||
export const DeviceVerificationReset = forwardRef<HTMLDivElement, DeviceVerificationResetProps>(
|
export const DeviceVerificationReset = forwardRef<HTMLDivElement, DeviceVerificationResetProps>(
|
||||||
({ onCancel }, ref) => {
|
({ onCancel }, ref) => {
|
||||||
const [reset, setReset] = useState(false);
|
const [reset, setReset] = useState(false);
|
||||||
|
const modalStyle = useModalStyle(480);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog ref={ref}>
|
<Dialog ref={ref} style={modalStyle}>
|
||||||
<Header
|
<Header
|
||||||
style={{
|
style={{
|
||||||
padding: `0 ${config.space.S200} 0 ${config.space.S400}`,
|
padding: `0 ${config.space.S200} 0 ${config.space.S400}`,
|
||||||
@@ -334,9 +339,11 @@ export const DeviceVerificationReset = forwardRef<HTMLDivElement, DeviceVerifica
|
|||||||
size="500"
|
size="500"
|
||||||
>
|
>
|
||||||
<Box grow="Yes">
|
<Box grow="Yes">
|
||||||
<Text size="H4">Reset Device Verification</Text>
|
<Text as="h2" size="H4">
|
||||||
|
Reset Device Verification
|
||||||
|
</Text>
|
||||||
</Box>
|
</Box>
|
||||||
<IconButton size="300" radii="300" onClick={onCancel}>
|
<IconButton size="300" radii="300" onClick={onCancel} aria-label="Cancel">
|
||||||
<Icon src={Icons.Cross} />
|
<Icon src={Icons.Cross} />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</Header>
|
</Header>
|
||||||
@@ -371,5 +378,5 @@ export const DeviceVerificationReset = forwardRef<HTMLDivElement, DeviceVerifica
|
|||||||
)}
|
)}
|
||||||
</Dialog>
|
</Dialog>
|
||||||
);
|
);
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -0,0 +1,127 @@
|
|||||||
|
import React, { useCallback } from 'react';
|
||||||
|
import FocusTrap from 'focus-trap-react';
|
||||||
|
import { Grid, SearchBar, SearchContext, SearchContextManager } from '@giphy/react-components';
|
||||||
|
import { IGif } from '@giphy/js-types';
|
||||||
|
import { Box, color, config } from 'folds';
|
||||||
|
import { useSetting } from '../state/hooks/settings';
|
||||||
|
import { settingsAtom } from '../state/settings';
|
||||||
|
|
||||||
|
const PICKER_WIDTH = 312;
|
||||||
|
|
||||||
|
type GifPickerInnerProps = {
|
||||||
|
onSelect: (url: string, width: number, height: number) => void;
|
||||||
|
requestClose: () => void;
|
||||||
|
lotusTerminal: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
function GifPickerInner({ onSelect, requestClose, lotusTerminal }: GifPickerInnerProps) {
|
||||||
|
const { fetchGifs, searchKey } = React.useContext(SearchContext);
|
||||||
|
|
||||||
|
const handleClick = useCallback(
|
||||||
|
(gif: IGif, e: React.SyntheticEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
const r = gif.images.downsized ?? gif.images.original;
|
||||||
|
const { url } = r;
|
||||||
|
const width = Number(r.width) || 200;
|
||||||
|
const height = Number(r.height) || 200;
|
||||||
|
onSelect(url, width, height);
|
||||||
|
requestClose();
|
||||||
|
},
|
||||||
|
[onSelect, requestClose],
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box direction="Column" style={{ width: `${PICKER_WIDTH}px` }}>
|
||||||
|
{lotusTerminal && (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
padding: '5px 10px 4px',
|
||||||
|
borderBottom: '1px solid color-mix(in srgb, var(--lt-accent-orange) 20%, transparent)',
|
||||||
|
fontFamily: "'JetBrains Mono', 'Cascadia Code', monospace",
|
||||||
|
fontSize: '10px',
|
||||||
|
fontWeight: 700,
|
||||||
|
letterSpacing: '0.1em',
|
||||||
|
color: 'var(--lt-accent-orange)',
|
||||||
|
userSelect: 'none',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{'// GIF_SEARCH'}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<Box style={{ padding: '8px 8px 4px' }}>
|
||||||
|
<div style={{ width: '100%', borderRadius: lotusTerminal ? '4px' : '8px' }}>
|
||||||
|
<SearchBar />
|
||||||
|
</div>
|
||||||
|
</Box>
|
||||||
|
<div
|
||||||
|
style={{ overflowY: 'auto', overflowX: 'hidden', maxHeight: '340px', padding: '0 8px 8px' }}
|
||||||
|
>
|
||||||
|
<Grid
|
||||||
|
key={searchKey}
|
||||||
|
fetchGifs={fetchGifs}
|
||||||
|
width={PICKER_WIDTH - 16}
|
||||||
|
columns={2}
|
||||||
|
gutter={4}
|
||||||
|
onGifClick={handleClick}
|
||||||
|
hideAttribution={false}
|
||||||
|
noLink
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
type GifPickerProps = {
|
||||||
|
apiKey: string;
|
||||||
|
onSelect: (url: string, width: number, height: number) => void;
|
||||||
|
requestClose: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function GifPicker({ apiKey, onSelect, requestClose }: GifPickerProps) {
|
||||||
|
const [lotusTerminal] = useSetting(settingsAtom, 'lotusTerminal');
|
||||||
|
|
||||||
|
const containerStyle = lotusTerminal
|
||||||
|
? {
|
||||||
|
background: 'var(--lt-bg-secondary)',
|
||||||
|
border: '1px solid color-mix(in srgb, var(--lt-accent-orange) 35%, transparent)',
|
||||||
|
borderRadius: '4px',
|
||||||
|
overflow: 'hidden',
|
||||||
|
boxShadow:
|
||||||
|
'0 4px 24px color-mix(in srgb, var(--lt-accent-orange) 10%, transparent), 0 0 0 1px color-mix(in srgb, var(--lt-accent-orange) 8%, transparent)',
|
||||||
|
width: `${PICKER_WIDTH}px`,
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
background: color.Surface.Container,
|
||||||
|
border: `${config.borderWidth.B300} solid ${color.Surface.ContainerLine}`,
|
||||||
|
borderRadius: config.radii.R400,
|
||||||
|
overflow: 'hidden',
|
||||||
|
boxShadow: color.Other.Shadow,
|
||||||
|
width: `${PICKER_WIDTH}px`,
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FocusTrap
|
||||||
|
focusTrapOptions={{
|
||||||
|
initialFocus: false,
|
||||||
|
returnFocusOnDeactivate: false,
|
||||||
|
onDeactivate: requestClose,
|
||||||
|
clickOutsideDeactivates: true,
|
||||||
|
allowOutsideClick: true,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
direction="Column"
|
||||||
|
data-gif-terminal={lotusTerminal ? '' : undefined}
|
||||||
|
style={containerStyle}
|
||||||
|
>
|
||||||
|
<SearchContextManager apiKey={apiKey} initialTerm="">
|
||||||
|
<GifPickerInner
|
||||||
|
onSelect={onSelect}
|
||||||
|
requestClose={requestClose}
|
||||||
|
lotusTerminal={!!lotusTerminal}
|
||||||
|
/>
|
||||||
|
</SearchContextManager>
|
||||||
|
</Box>
|
||||||
|
</FocusTrap>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -41,5 +41,5 @@ export const ImageOverlay = as<'div', ImageOverlayProps>(
|
|||||||
</FocusTrap>
|
</FocusTrap>
|
||||||
</OverlayCenter>
|
</OverlayCenter>
|
||||||
</Overlay>
|
</Overlay>
|
||||||
)
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ export const useJoinRuleIcons = (roomType?: string): JoinRuleIcons =>
|
|||||||
[JoinRule.Public]: getRoomIconSrc(Icons, roomType, JoinRule.Public),
|
[JoinRule.Public]: getRoomIconSrc(Icons, roomType, JoinRule.Public),
|
||||||
[JoinRule.Private]: getRoomIconSrc(Icons, roomType, JoinRule.Private),
|
[JoinRule.Private]: getRoomIconSrc(Icons, roomType, JoinRule.Private),
|
||||||
}),
|
}),
|
||||||
[roomType]
|
[roomType],
|
||||||
);
|
);
|
||||||
|
|
||||||
type JoinRuleLabels = Record<ExtendedJoinRules, string>;
|
type JoinRuleLabels = Record<ExtendedJoinRules, string>;
|
||||||
@@ -47,7 +47,7 @@ export const useRoomJoinRuleLabel = (): JoinRuleLabels =>
|
|||||||
[JoinRule.Public]: 'Public',
|
[JoinRule.Public]: 'Public',
|
||||||
[JoinRule.Private]: 'Invite Only',
|
[JoinRule.Private]: 'Invite Only',
|
||||||
}),
|
}),
|
||||||
[]
|
[],
|
||||||
);
|
);
|
||||||
|
|
||||||
type JoinRulesSwitcherProps<T extends ExtendedJoinRules[]> = {
|
type JoinRulesSwitcherProps<T extends ExtendedJoinRules[]> = {
|
||||||
@@ -79,7 +79,7 @@ export function JoinRulesSwitcher<T extends ExtendedJoinRules[]>({
|
|||||||
setCords(undefined);
|
setCords(undefined);
|
||||||
onChange(selectedRule);
|
onChange(selectedRule);
|
||||||
},
|
},
|
||||||
[onChange]
|
[onChange],
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -0,0 +1,118 @@
|
|||||||
|
import React, { useId } from 'react';
|
||||||
|
|
||||||
|
const ROOM_ROWS = [
|
||||||
|
{ w: '160px', indent: false },
|
||||||
|
{ w: '120px', indent: true },
|
||||||
|
{ w: '140px', indent: true },
|
||||||
|
{ w: '130px', indent: true },
|
||||||
|
{ w: '150px', indent: false },
|
||||||
|
{ w: '110px', indent: true },
|
||||||
|
];
|
||||||
|
|
||||||
|
export function LobbySkeleton() {
|
||||||
|
const id = useId().replace(/:/g, '');
|
||||||
|
const shimmerKeyframes = `
|
||||||
|
@keyframes shimmer-${id} {
|
||||||
|
0% { background-position: -400px 0; }
|
||||||
|
100% { background-position: 400px 0; }
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const shimmer = {
|
||||||
|
background:
|
||||||
|
'linear-gradient(90deg, var(--skeleton-base) 25%, var(--skeleton-shine) 50%, var(--skeleton-base) 75%)',
|
||||||
|
backgroundSize: '800px 100%',
|
||||||
|
animation: `shimmer-${id} 1.6s ease-in-out infinite`,
|
||||||
|
borderRadius: '4px',
|
||||||
|
} as React.CSSProperties;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<style>{shimmerKeyframes}</style>
|
||||||
|
<div
|
||||||
|
style={
|
||||||
|
{
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
flexGrow: 1,
|
||||||
|
height: '100%',
|
||||||
|
overflow: 'hidden',
|
||||||
|
'--skeleton-base': 'color-mix(in srgb, currentColor 8%, transparent)',
|
||||||
|
'--skeleton-shine': 'color-mix(in srgb, currentColor 15%, transparent)',
|
||||||
|
} as React.CSSProperties
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{/* Header — matches LobbyHeader (56px) */}
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
height: '56px',
|
||||||
|
borderBottom: '1px solid color-mix(in srgb, currentColor 10%, transparent)',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: '12px',
|
||||||
|
padding: '0 16px',
|
||||||
|
flexShrink: 0,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
...shimmer,
|
||||||
|
width: '32px',
|
||||||
|
height: '32px',
|
||||||
|
borderRadius: '50%',
|
||||||
|
flexShrink: 0,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<div style={{ ...shimmer, width: '130px', height: '16px' }} />
|
||||||
|
<div style={{ flex: 1 }} />
|
||||||
|
<div style={{ ...shimmer, width: '24px', height: '24px', borderRadius: '4px' }} />
|
||||||
|
<div style={{ ...shimmer, width: '24px', height: '24px', borderRadius: '4px' }} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={{ flex: 1, overflowY: 'hidden', display: 'flex', flexDirection: 'column' }}>
|
||||||
|
{/* Hero — matches PageHero with large avatar + title + subtitle */}
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
alignItems: 'center',
|
||||||
|
padding: '32px 16px 24px',
|
||||||
|
gap: '12px',
|
||||||
|
flexShrink: 0,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div style={{ ...shimmer, width: '72px', height: '72px', borderRadius: '50%' }} />
|
||||||
|
<div style={{ ...shimmer, width: '180px', height: '20px' }} />
|
||||||
|
<div style={{ ...shimmer, width: '240px', height: '13px' }} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Room list rows */}
|
||||||
|
<div style={{ flex: 1, padding: '8px 0' }}>
|
||||||
|
{ROOM_ROWS.map((row, i) => (
|
||||||
|
<div
|
||||||
|
key={i}
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: '10px',
|
||||||
|
padding: `6px 16px 6px ${row.indent ? '36px' : '16px'}`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
...shimmer,
|
||||||
|
width: '18px',
|
||||||
|
height: '18px',
|
||||||
|
borderRadius: '4px',
|
||||||
|
flexShrink: 0,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<div style={{ ...shimmer, width: row.w, height: '14px' }} />
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -3,6 +3,7 @@ import { Dialog, Header, config, Box, Text, Button, Spinner, color } from 'folds
|
|||||||
import { AsyncStatus, useAsyncCallback } from '../hooks/useAsyncCallback';
|
import { AsyncStatus, useAsyncCallback } from '../hooks/useAsyncCallback';
|
||||||
import { logoutClient } from '../../client/initMatrix';
|
import { logoutClient } from '../../client/initMatrix';
|
||||||
import { useMatrixClient } from '../hooks/useMatrixClient';
|
import { useMatrixClient } from '../hooks/useMatrixClient';
|
||||||
|
import { useModalStyle } from '../hooks/useModalStyle';
|
||||||
import { useCrossSigningActive } from '../hooks/useCrossSigning';
|
import { useCrossSigningActive } from '../hooks/useCrossSigning';
|
||||||
import { InfoCard } from './info-card';
|
import { InfoCard } from './info-card';
|
||||||
import {
|
import {
|
||||||
@@ -16,24 +17,25 @@ type LogoutDialogProps = {
|
|||||||
export const LogoutDialog = forwardRef<HTMLDivElement, LogoutDialogProps>(
|
export const LogoutDialog = forwardRef<HTMLDivElement, LogoutDialogProps>(
|
||||||
({ handleClose }, ref) => {
|
({ handleClose }, ref) => {
|
||||||
const mx = useMatrixClient();
|
const mx = useMatrixClient();
|
||||||
|
const modalStyle = useModalStyle(480);
|
||||||
const hasEncryptedRoom = !!mx.getRooms().find((room) => room.hasEncryptionStateEvent());
|
const hasEncryptedRoom = !!mx.getRooms().find((room) => room.hasEncryptionStateEvent());
|
||||||
const crossSigningActive = useCrossSigningActive();
|
const crossSigningActive = useCrossSigningActive();
|
||||||
const verificationStatus = useDeviceVerificationStatus(
|
const verificationStatus = useDeviceVerificationStatus(
|
||||||
mx.getCrypto(),
|
mx.getCrypto(),
|
||||||
mx.getSafeUserId(),
|
mx.getSafeUserId(),
|
||||||
mx.getDeviceId() ?? undefined
|
mx.getDeviceId() ?? undefined,
|
||||||
);
|
);
|
||||||
|
|
||||||
const [logoutState, logout] = useAsyncCallback<void, Error, []>(
|
const [logoutState, logout] = useAsyncCallback<void, Error, []>(
|
||||||
useCallback(async () => {
|
useCallback(async () => {
|
||||||
await logoutClient(mx);
|
await logoutClient(mx);
|
||||||
}, [mx])
|
}, [mx]),
|
||||||
);
|
);
|
||||||
|
|
||||||
const ongoingLogout = logoutState.status === AsyncStatus.Loading;
|
const ongoingLogout = logoutState.status === AsyncStatus.Loading;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog variant="Surface" ref={ref}>
|
<Dialog variant="Surface" ref={ref} style={modalStyle}>
|
||||||
<Header
|
<Header
|
||||||
style={{
|
style={{
|
||||||
padding: `0 ${config.space.S200} 0 ${config.space.S400}`,
|
padding: `0 ${config.space.S200} 0 ${config.space.S400}`,
|
||||||
@@ -43,7 +45,9 @@ export const LogoutDialog = forwardRef<HTMLDivElement, LogoutDialogProps>(
|
|||||||
size="500"
|
size="500"
|
||||||
>
|
>
|
||||||
<Box grow="Yes">
|
<Box grow="Yes">
|
||||||
<Text size="H4">Logout</Text>
|
<Text as="h2" size="H4">
|
||||||
|
Logout
|
||||||
|
</Text>
|
||||||
</Box>
|
</Box>
|
||||||
</Header>
|
</Header>
|
||||||
<Box style={{ padding: config.space.S400 }} direction="Column" gap="400">
|
<Box style={{ padding: config.space.S400 }} direction="Column" gap="400">
|
||||||
@@ -85,5 +89,5 @@ export const LogoutDialog = forwardRef<HTMLDivElement, LogoutDialogProps>(
|
|||||||
</Box>
|
</Box>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
);
|
);
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -126,7 +126,7 @@ export function ManualVerificationTile({
|
|||||||
const [method, setMethod] = useState(
|
const [method, setMethod] = useState(
|
||||||
hasPassphrase
|
hasPassphrase
|
||||||
? ManualVerificationMethod.RecoveryPassphrase
|
? ManualVerificationMethod.RecoveryPassphrase
|
||||||
: ManualVerificationMethod.RecoveryKey
|
: ManualVerificationMethod.RecoveryKey,
|
||||||
);
|
);
|
||||||
|
|
||||||
const verifyAndRestoreBackup = useCallback(
|
const verifyAndRestoreBackup = useCallback(
|
||||||
@@ -143,11 +143,11 @@ export function ManualVerificationTile({
|
|||||||
|
|
||||||
await crypto.loadSessionBackupPrivateKeyFromSecretStorage();
|
await crypto.loadSessionBackupPrivateKeyFromSecretStorage();
|
||||||
},
|
},
|
||||||
[mx, secretStorageKeyId]
|
[mx, secretStorageKeyId],
|
||||||
);
|
);
|
||||||
|
|
||||||
const [verifyState, handleDecodedRecoveryKey] = useAsyncCallback<void, Error, [Uint8Array]>(
|
const [verifyState, handleDecodedRecoveryKey] = useAsyncCallback<void, Error, [Uint8Array]>(
|
||||||
verifyAndRestoreBackup
|
verifyAndRestoreBackup,
|
||||||
);
|
);
|
||||||
const verifying = verifyState.status === AsyncStatus.Loading;
|
const verifying = verifyState.status === AsyncStatus.Loading;
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ export function MediaConfigLoader({ children }: MediaConfigLoaderProps) {
|
|||||||
const [state, load] = useAsyncCallback(useCallback(() => mx.getMediaConfig(), [mx]));
|
const [state, load] = useAsyncCallback(useCallback(() => mx.getMediaConfig(), [mx]));
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
load();
|
load().catch(() => {});
|
||||||
}, [load]);
|
}, [load]);
|
||||||
|
|
||||||
return children(state.status === AsyncStatus.Success ? state.data : undefined);
|
return children(state.status === AsyncStatus.Success ? state.data : undefined);
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { color, Icon, Icons, Text, Tooltip, TooltipProvider } from 'folds';
|
||||||
|
import { useUserVerifiedStatus } from '../hooks/useUserVerifiedStatus';
|
||||||
|
|
||||||
|
type MemberVerificationBadgeProps = {
|
||||||
|
userId: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function MemberVerificationBadge({ userId }: MemberVerificationBadgeProps) {
|
||||||
|
const vs = useUserVerifiedStatus(userId);
|
||||||
|
if (vs === 'unknown') return null;
|
||||||
|
const iconColor = vs === 'verified' ? color.Success.Main : color.Warning.Main;
|
||||||
|
const label = vs === 'verified' ? 'Identity verified' : 'Not verified';
|
||||||
|
return (
|
||||||
|
<TooltipProvider
|
||||||
|
position="Top"
|
||||||
|
tooltip={
|
||||||
|
<Tooltip>
|
||||||
|
<Text size="T200">{label}</Text>
|
||||||
|
</Tooltip>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{(ref) => (
|
||||||
|
<span
|
||||||
|
ref={ref}
|
||||||
|
title={label}
|
||||||
|
style={{ display: 'inline-flex', alignItems: 'center', flexShrink: 0 }}
|
||||||
|
>
|
||||||
|
<Icon size="100" src={Icons.ShieldUser} style={{ color: iconColor }} />
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</TooltipProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -2,12 +2,14 @@ import React, { ReactNode } from 'react';
|
|||||||
import FocusTrap from 'focus-trap-react';
|
import FocusTrap from 'focus-trap-react';
|
||||||
import { Modal, Overlay, OverlayBackdrop, OverlayCenter } from 'folds';
|
import { Modal, Overlay, OverlayBackdrop, OverlayCenter } from 'folds';
|
||||||
import { stopPropagation } from '../utils/keyboard';
|
import { stopPropagation } from '../utils/keyboard';
|
||||||
|
import { ScreenSize, useScreenSizeContext } from '../hooks/useScreenSize';
|
||||||
|
|
||||||
type Modal500Props = {
|
type Modal500Props = {
|
||||||
requestClose: () => void;
|
requestClose: () => void;
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
};
|
};
|
||||||
export function Modal500({ requestClose, children }: Modal500Props) {
|
export function Modal500({ requestClose, children }: Modal500Props) {
|
||||||
|
const isMobile = useScreenSizeContext() === ScreenSize.Mobile;
|
||||||
return (
|
return (
|
||||||
<Overlay open backdrop={<OverlayBackdrop />}>
|
<Overlay open backdrop={<OverlayBackdrop />}>
|
||||||
<OverlayCenter>
|
<OverlayCenter>
|
||||||
@@ -19,7 +21,25 @@ export function Modal500({ requestClose, children }: Modal500Props) {
|
|||||||
escapeDeactivates: stopPropagation,
|
escapeDeactivates: stopPropagation,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Modal size="500" variant="Background">
|
<Modal
|
||||||
|
size="500"
|
||||||
|
variant="Background"
|
||||||
|
// On mobile expand to fill the viewport. On desktop fall back to the
|
||||||
|
// folds `size="500"` width (~50rem) — overriding maxWidth here would
|
||||||
|
// squish the two-pane settings layout.
|
||||||
|
style={
|
||||||
|
isMobile
|
||||||
|
? {
|
||||||
|
width: '100%',
|
||||||
|
height: '100%',
|
||||||
|
maxWidth: '100%',
|
||||||
|
maxHeight: '100%',
|
||||||
|
borderRadius: 0,
|
||||||
|
overflow: 'hidden auto',
|
||||||
|
}
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
>
|
||||||
{children}
|
{children}
|
||||||
</Modal>
|
</Modal>
|
||||||
</FocusTrap>
|
</FocusTrap>
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
/* eslint-disable no-param-reassign */
|
|
||||||
/* eslint-disable jsx-a11y/no-noninteractive-element-interactions */
|
|
||||||
import React, { FormEventHandler, MouseEventHandler, useEffect, useRef, useState } from 'react';
|
import React, { FormEventHandler, MouseEventHandler, useEffect, useRef, useState } from 'react';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import {
|
import {
|
||||||
@@ -43,7 +41,7 @@ export const PdfViewer = as<'div', PdfViewerProps>(
|
|||||||
const [pdfJSState, loadPdfJS] = usePdfJSLoader();
|
const [pdfJSState, loadPdfJS] = usePdfJSLoader();
|
||||||
const [docState, loadPdfDocument] = usePdfDocumentLoader(
|
const [docState, loadPdfDocument] = usePdfDocumentLoader(
|
||||||
pdfJSState.status === AsyncStatus.Success ? pdfJSState.data : undefined,
|
pdfJSState.status === AsyncStatus.Success ? pdfJSState.data : undefined,
|
||||||
src
|
src,
|
||||||
);
|
);
|
||||||
const isLoading =
|
const isLoading =
|
||||||
pdfJSState.status === AsyncStatus.Loading || docState.status === AsyncStatus.Loading;
|
pdfJSState.status === AsyncStatus.Loading || docState.status === AsyncStatus.Loading;
|
||||||
@@ -108,7 +106,7 @@ export const PdfViewer = as<'div', PdfViewerProps>(
|
|||||||
<Box className={classNames(css.PdfViewer, className)} direction="Column" {...props} ref={ref}>
|
<Box className={classNames(css.PdfViewer, className)} direction="Column" {...props} ref={ref}>
|
||||||
<Header className={css.PdfViewerHeader} size="400">
|
<Header className={css.PdfViewerHeader} size="400">
|
||||||
<Box grow="Yes" alignItems="Center" gap="200">
|
<Box grow="Yes" alignItems="Center" gap="200">
|
||||||
<IconButton size="300" radii="300" onClick={requestClose}>
|
<IconButton size="300" radii="300" onClick={requestClose} aria-label="Close">
|
||||||
<Icon size="50" src={Icons.ArrowLeft} />
|
<Icon size="50" src={Icons.ArrowLeft} />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
<Text size="T300" truncate>
|
<Text size="T300" truncate>
|
||||||
@@ -257,5 +255,5 @@ export const PdfViewer = as<'div', PdfViewerProps>(
|
|||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import {
|
|||||||
RenderBody,
|
RenderBody,
|
||||||
ThumbnailContent,
|
ThumbnailContent,
|
||||||
UnsupportedContent,
|
UnsupportedContent,
|
||||||
|
VerificationRequestContent,
|
||||||
VideoContent,
|
VideoContent,
|
||||||
} from './message';
|
} from './message';
|
||||||
import { UrlPreviewCard, UrlPreviewHolder } from './url-preview';
|
import { UrlPreviewCard, UrlPreviewHolder } from './url-preview';
|
||||||
@@ -37,6 +38,7 @@ type RenderMessageContentProps = {
|
|||||||
msgType: string;
|
msgType: string;
|
||||||
ts: number;
|
ts: number;
|
||||||
edited?: boolean;
|
edited?: boolean;
|
||||||
|
onEditHistoryClick?: () => void;
|
||||||
getContent: <T>() => T;
|
getContent: <T>() => T;
|
||||||
mediaAutoLoad?: boolean;
|
mediaAutoLoad?: boolean;
|
||||||
urlPreview?: boolean;
|
urlPreview?: boolean;
|
||||||
@@ -44,12 +46,14 @@ type RenderMessageContentProps = {
|
|||||||
htmlReactParserOptions: HTMLReactParserOptions;
|
htmlReactParserOptions: HTMLReactParserOptions;
|
||||||
linkifyOpts: Opts;
|
linkifyOpts: Opts;
|
||||||
outlineAttachment?: boolean;
|
outlineAttachment?: boolean;
|
||||||
|
eventId?: string;
|
||||||
};
|
};
|
||||||
export function RenderMessageContent({
|
export function RenderMessageContent({
|
||||||
displayName,
|
displayName,
|
||||||
msgType,
|
msgType,
|
||||||
ts,
|
ts,
|
||||||
edited,
|
edited,
|
||||||
|
onEditHistoryClick,
|
||||||
getContent,
|
getContent,
|
||||||
mediaAutoLoad,
|
mediaAutoLoad,
|
||||||
urlPreview,
|
urlPreview,
|
||||||
@@ -57,6 +61,7 @@ export function RenderMessageContent({
|
|||||||
htmlReactParserOptions,
|
htmlReactParserOptions,
|
||||||
linkifyOpts,
|
linkifyOpts,
|
||||||
outlineAttachment,
|
outlineAttachment,
|
||||||
|
eventId,
|
||||||
}: RenderMessageContentProps) {
|
}: RenderMessageContentProps) {
|
||||||
const renderUrlsPreview = (urls: string[]) => {
|
const renderUrlsPreview = (urls: string[]) => {
|
||||||
const filteredUrls = urls.filter((url) => !testMatrixTo(url));
|
const filteredUrls = urls.filter((url) => !testMatrixTo(url));
|
||||||
@@ -76,6 +81,7 @@ export function RenderMessageContent({
|
|||||||
<MText
|
<MText
|
||||||
style={{ marginTop: config.space.S200 }}
|
style={{ marginTop: config.space.S200 }}
|
||||||
edited={edited}
|
edited={edited}
|
||||||
|
onEditHistoryClick={onEditHistoryClick}
|
||||||
content={content}
|
content={content}
|
||||||
renderBody={(props) => (
|
renderBody={(props) => (
|
||||||
<RenderBody
|
<RenderBody
|
||||||
@@ -132,6 +138,7 @@ export function RenderMessageContent({
|
|||||||
return (
|
return (
|
||||||
<MText
|
<MText
|
||||||
edited={edited}
|
edited={edited}
|
||||||
|
onEditHistoryClick={onEditHistoryClick}
|
||||||
content={getContent()}
|
content={getContent()}
|
||||||
renderBody={(props) => (
|
renderBody={(props) => (
|
||||||
<RenderBody
|
<RenderBody
|
||||||
@@ -142,6 +149,7 @@ export function RenderMessageContent({
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
renderUrlsPreview={urlPreview ? renderUrlsPreview : undefined}
|
renderUrlsPreview={urlPreview ? renderUrlsPreview : undefined}
|
||||||
|
eventId={eventId}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -151,6 +159,7 @@ export function RenderMessageContent({
|
|||||||
<MEmote
|
<MEmote
|
||||||
displayName={displayName}
|
displayName={displayName}
|
||||||
edited={edited}
|
edited={edited}
|
||||||
|
onEditHistoryClick={onEditHistoryClick}
|
||||||
content={getContent()}
|
content={getContent()}
|
||||||
renderBody={(props) => (
|
renderBody={(props) => (
|
||||||
<RenderBody
|
<RenderBody
|
||||||
@@ -161,6 +170,7 @@ export function RenderMessageContent({
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
renderUrlsPreview={urlPreview ? renderUrlsPreview : undefined}
|
renderUrlsPreview={urlPreview ? renderUrlsPreview : undefined}
|
||||||
|
eventId={eventId}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -169,6 +179,7 @@ export function RenderMessageContent({
|
|||||||
return (
|
return (
|
||||||
<MNotice
|
<MNotice
|
||||||
edited={edited}
|
edited={edited}
|
||||||
|
onEditHistoryClick={onEditHistoryClick}
|
||||||
content={getContent()}
|
content={getContent()}
|
||||||
renderBody={(props) => (
|
renderBody={(props) => (
|
||||||
<RenderBody
|
<RenderBody
|
||||||
@@ -179,6 +190,7 @@ export function RenderMessageContent({
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
renderUrlsPreview={urlPreview ? renderUrlsPreview : undefined}
|
renderUrlsPreview={urlPreview ? renderUrlsPreview : undefined}
|
||||||
|
eventId={eventId}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -220,7 +232,18 @@ export function RenderMessageContent({
|
|||||||
<ThumbnailContent
|
<ThumbnailContent
|
||||||
info={info}
|
info={info}
|
||||||
renderImage={(src) => (
|
renderImage={(src) => (
|
||||||
<Image alt={body} title={body} src={src} loading="lazy" />
|
<Image
|
||||||
|
alt={body}
|
||||||
|
title={body}
|
||||||
|
src={src}
|
||||||
|
loading="lazy"
|
||||||
|
style={{
|
||||||
|
objectFit: 'cover',
|
||||||
|
objectPosition: 'center top',
|
||||||
|
width: '100%',
|
||||||
|
height: '100%',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
@@ -264,5 +287,9 @@ export function RenderMessageContent({
|
|||||||
return <MBadEncrypted />;
|
return <MBadEncrypted />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (msgType === 'm.key.verification.request') {
|
||||||
|
return <VerificationRequestContent />;
|
||||||
|
}
|
||||||
|
|
||||||
return <UnsupportedContent />;
|
return <UnsupportedContent />;
|
||||||
}
|
}
|
||||||
|
|||||||