Compare commits
1536 Commits
b24ab838f8
..
dev
| Author | SHA1 | Date | |
|---|---|---|---|
| 2864a5e4b8 | |||
| 735bc15011 | |||
| 341fedd932 | |||
| d186d31399 | |||
| 098684973e | |||
| b107109453 | |||
| a33e8db9a3 | |||
| fb76e3ecb4 | |||
| 3d79293167 | |||
| 74745edcda | |||
| 0812131a97 | |||
| 1068bba5c7 | |||
| 1b5e58a3b4 | |||
| acae043f31 | |||
| b4299f8f37 | |||
| b6adac6714 | |||
| 1c8f203164 | |||
| 0c30ece281 | |||
| 4e559e56d4 | |||
| 19f28b40ac | |||
| bcaf43a540 | |||
| 9c7b635e7e | |||
| 65c87dff3a | |||
| 132a76df27 | |||
| b0954eeddc | |||
| 8f1add6059 | |||
| 8a78c9699e | |||
| 0721b29a2c | |||
| 3d354909d6 | |||
| 7570a84dfd | |||
| 6a05ff5840 | |||
| 919fe8381b | |||
| b76ad3caaf | |||
| 409d45857d | |||
| 0d1566977a | |||
| 0cbfbab5ad | |||
| 37e0c2aaac | |||
| 296249de32 | |||
| 4449e7c6e8 | |||
| 2eb5a9a616 | |||
| d679e68501 | |||
| bc6caddcc8 | |||
| 55e8306576 | |||
| 7953ec80e5 | |||
| c6bb4915bc | |||
| b050cd01f9 | |||
| 730670cf52 | |||
| c5c8703699 | |||
| 2bd1570d6b | |||
| 68b6a09697 | |||
| 7b52c921d5 | |||
| bb8b9ab6da | |||
| 971f312b46 | |||
| e0d5c63dc5 | |||
| 85fcbd84fe | |||
| 221bc04754 | |||
| 6347640a35 | |||
| f2d8ad0b6b | |||
| 739786d9ab | |||
| f642809939 | |||
| 02106a99b9 | |||
| df3a3ba789 | |||
| cd80d4c9e8 | |||
| dab44edef2 | |||
| ed0ad61bc4 | |||
| b2cb717178 | |||
| 7a9f6d2223 | |||
| a9022184fc | |||
| 826b3c2997 | |||
| 2e6c5f7c04 | |||
| 2d6730de56 | |||
| b6cc0e3077 | |||
| 91c8731940 | |||
| 1f03891b25 | |||
| 9ff15b8b03 | |||
| 170f5cd473 | |||
| 29ec172c8b | |||
| 0f220f50d6 | |||
| d866c1b903 | |||
| fbde1a2030 | |||
| 4ba7b9162d | |||
| 9d49418a1f | |||
| 3522751a15 | |||
| 074c555294 | |||
| 206a927f30 | |||
| fd37dfe3f9 | |||
| 1ce6ca2b07 | |||
| 83e5125b37 | |||
| ca82aa283a | |||
| 8ce33ee6ff | |||
| 073a9f5786 | |||
| 655c1c9aff | |||
| 17d4bceb42 | |||
| 0f61f2f328 | |||
| c88cb4bca9 | |||
| 46c02b89de | |||
| e13d97aa98 | |||
| 958ae8945d | |||
| f55a3764d5 | |||
| 3bdcf37bf0 | |||
| 9d7808ec46 | |||
| 20d30903fd | |||
| b78f6f23b5 | |||
| 867a47218a | |||
| afc251aa7c | |||
| 31efbf73b7 | |||
| 31c6d13fdf | |||
| b3497d9ed6 | |||
| 7f40605bfe | |||
| c571c93f61 | |||
| 90ca8ca2c5 | |||
| 399b1a373e | |||
| c1274e851a | |||
| 40d0576b15 | |||
| b4266c26b0 | |||
| 23aa5c6f94 | |||
| 4056cbb11c | |||
| 13cdcbcdb1 | |||
| c881b59957 | |||
| 78a0d11f24 | |||
| 09b88d164f | |||
| 789da641c1 | |||
| abd713d693 | |||
| 802357b7a0 | |||
| c5d4530947 | |||
| 367397fdd4 | |||
| 63fa60e7f4 | |||
| 544a06964d | |||
| 50583f9474 | |||
| 1ad7fe8deb | |||
| 752a19a4e7 | |||
| 76ac4e2987 | |||
| f82cfead46 | |||
| 4d1ae4eafd | |||
| a41dee4a55 | |||
| 13961d501f | |||
| e6f14e79da | |||
| 1ff09d0fc1 | |||
| f2d25c8d6c | |||
| fe4fb4b4f7 | |||
| faa952295f | |||
| e9798a22c3 | |||
| 34dd64103c | |||
| 6a27720709 | |||
| ccf10fc20c | |||
| 31942b1114 | |||
| d8d4714370 | |||
| 9183fd66b2 | |||
| 67b05eeb09 | |||
| 7d4b0dd133 | |||
| 9073dee986 | |||
| 3cdb5c2fe6 | |||
| acc7d4ff56 | |||
| 50cc78788f | |||
| c462a3b8d5 | |||
| c30c142653 | |||
| fbd7e0a14b | |||
| 6b81401e2d | |||
| c757b8967f | |||
| d0a7ef31bc | |||
| 3fd8a18157 | |||
| 54ba1096d7 | |||
| 87fc490c3b | |||
| ebe5beba1d | |||
| 77ab37f637 | |||
| 461e730c34 | |||
| 05e83eabef | |||
| ba72925d53 | |||
| 87ce209050 | |||
| 3ed8260877 | |||
| 44347db6e4 | |||
| 91632aa193 | |||
| e6f4eeca8e | |||
| a23279e633 | |||
| 83057ebbd4 | |||
| c51ba9670e | |||
| 59a007419f | |||
| 206ed33516 | |||
| 0d27bde33e | |||
| df391968d8 | |||
| 5964eee833 | |||
| 387ce9c462 | |||
| 6ddcf2cb02 | |||
| 87e97eab88 | |||
| 13f1d53191 | |||
| 225894d327 | |||
| c14333c540 | |||
| 405d1f6789 | |||
| ff6d0b8f9b | |||
| d141c02074 | |||
| 12ae94cd60 | |||
| 82805dcfdd | |||
| 5c39a36c12 | |||
| 4aed4d7472 | |||
| 649f70332b | |||
| 08e975cd8e | |||
| 7d54eef95b | |||
| 1361c1d5de | |||
| ea48092270 | |||
| 324ed776c9 | |||
| 7af89da092 | |||
| 002223e149 | |||
| 90aecb8d7a | |||
| 3e39dd25af | |||
| 71bfc96b5c | |||
| 074a5e855d | |||
| c16e060f73 | |||
| f688e2d1ae | |||
| 286983c833 | |||
| 00f3df8719 | |||
| d8009978e5 | |||
| 9bb30fbd92 | |||
| 82688c2e13 | |||
| a02d7162d9 | |||
| e39cc32df9 | |||
| 6017c0a2fc | |||
| 78fa6e3925 | |||
| 5d00383d71 | |||
| 19cf700d61 | |||
| 0c5ff65639 | |||
| 1206ffced2 | |||
| 5fbd0c13db | |||
| 36a8ce5561 | |||
| dbadbe34b3 | |||
| 2b8b0dcffd | |||
| b7e5e0db3e | |||
| 5c94471956 | |||
| ccfe30cd68 | |||
| 2c7038cd1f | |||
| d70ca86d7c | |||
| 8d95758ed7 | |||
| dd4c1a94e6 | |||
| 7c6ab366af | |||
| f121cc0a24 | |||
| 7456c152b7 | |||
| b63868bbb5 | |||
| 59e8d66255 | |||
| 1b200eb676 | |||
| 3ada21a1df | |||
| 9fe67da98b | |||
| d8d4bce287 | |||
| b3979b31c7 | |||
| 2e0c7c4406 | |||
| f73dc05e25 | |||
| 5844209bee | |||
| ff334c1c3d | |||
| 4374a89535 | |||
| 31dcad6220 | |||
| a58cffe12b | |||
| de7724dfcf | |||
| 4edc4bad02 | |||
| 2444a60a3e | |||
| ae88480d0a | |||
| 2ed3f877c3 | |||
| 09d85d6c31 | |||
| 999bb7aca1 | |||
| b84f975f83 | |||
| b12228ee95 | |||
| 5460e4922b | |||
| 7b4f684a25 | |||
| 56b754153a | |||
| f5d68fcc22 | |||
| 42e6e6355d | |||
| 8e4475bb56 | |||
| d5766b58fe | |||
| b524778039 | |||
| 45c2a22340 | |||
| 1b01f3e8ba | |||
| 2760f4cab9 | |||
| f3e57715a3 | |||
| 5cef90fad9 | |||
| 0764143d2c | |||
| e0e84f3756 | |||
| 69268187c2 | |||
| 02437a6a20 | |||
| 3c5afaf33a | |||
| 396bf50239 | |||
| 24d35a5817 | |||
| b8d9c4bdd6 | |||
| 35f0e400ad | |||
| 00d5553bcb | |||
| a142630ff9 | |||
| 492a149c7f | |||
| c110e64341 | |||
| 0e51e19cab | |||
| 35b0b1ea42 | |||
| cca8b5f2b2 | |||
| 48265c4227 | |||
| c38efdfbce | |||
| d8833a310d | |||
| 5824d7c716 | |||
| 6e191d3c79 | |||
| 21164a9b61 | |||
| 4923b17ad6 | |||
| c75e903619 | |||
| 042cbc4453 | |||
| 03cc25eec0 | |||
| f2c31d29a2 | |||
| 5482f8e72e | |||
| 96df140153 | |||
| 4dfce32730 | |||
| 388f606ad2 | |||
| 09444f9e08 | |||
| c6a8fb1117 | |||
| 043012e809 | |||
| 5c9ee1a988 | |||
| 22b7f6dd7d | |||
| bdba0332e1 | |||
| 16be69c104 | |||
| 830d05e217 | |||
| 7e7bee8f48 | |||
| ac1797344c | |||
| b4ce8a7cab | |||
| e68c56b334 | |||
| cabfdd47b5 | |||
| cfe893f358 | |||
| 581211f13e | |||
| 8ed78d48fb | |||
| 96222de5bc | |||
| 681287c46a | |||
| 9cb5c70d51 | |||
| c62050445b | |||
| a8f5a6c2f4 | |||
| e54bb2e423 | |||
| 5058136737 | |||
| 74dc76e22e | |||
| 44161c4157 | |||
| e8d04c0603 | |||
| 96415a8d2a | |||
| 2157f9a322 | |||
| b387370aaf | |||
| 3110505b21 | |||
| da536c8c3f | |||
| 98a378ad8a | |||
| ab73225f00 | |||
| cc4c222975 | |||
| a32c8bf228 | |||
| e6d6b0349e | |||
| e2228a18c1 | |||
| e046c59f7c | |||
| fbe27d69c0 | |||
| 021a2c0e2e | |||
| c243b6104c | |||
| a1a822c5b6 | |||
| c4abe39375 | |||
| c52c4f7d32 | |||
| 653ddd9f11 | |||
| e854b88394 | |||
| 66478143df | |||
| 4b461f87ff | |||
| fc2b5744f4 | |||
| 65ad070878 | |||
| f1668999a5 | |||
| 9db81d1913 | |||
| 7c795b800d | |||
| e058a9ae6c | |||
| 4f09e6bbb5 | |||
| 60e022035f | |||
| 7a3e8dba92 | |||
| c4615bd256 | |||
| b6157707db | |||
| 09a0a2d7da | |||
| 9db4b3a9c2 | |||
| 6987332ba8 | |||
| 4c76a7fd18 | |||
| 2b7d825694 | |||
| 07bfa0cf10 | |||
| e15b16b19b | |||
| 76d60b0958 | |||
| 97d02fd7c8 | |||
| 5817186129 | |||
| 2d6dd3b0b2 | |||
| cd5d8e1c20 | |||
| fe2332ee87 | |||
| 215537a261 | |||
| ec65b98874 | |||
| f1c4a38a49 | |||
| 5259f11679 | |||
| 565a6563e1 | |||
| 8267990e6f | |||
| e8020acabf | |||
| e5b980fbc7 | |||
| b803ce99e3 | |||
| 3ae1e58ff2 | |||
| ce347a0ff4 | |||
| d3f97ef93e | |||
| 53cd08f0da | |||
| da5ebf7ab3 | |||
| ca3535b1a5 | |||
| 2c1e51a8b8 | |||
| 71b2859440 | |||
| 1d799185d6 | |||
| b97f410731 | |||
| a18c2e5be1 | |||
| 3025133d18 | |||
| 743e916d12 | |||
| 8c5a1d15cb | |||
| 372d4d5c34 | |||
| b0796f72d3 | |||
| 689adde8ae | |||
| 983d533452 | |||
| ef2733df48 | |||
| 20db27fa7e | |||
| bb88eb7154 | |||
| 2a1bf4a42a | |||
| 2889a72b81 | |||
| 9ecb233763 | |||
| 1db0a9eaa8 | |||
| 687ad8d0f0 | |||
| c3f564605f | |||
| c854c7f9d2 | |||
| 3713125f57 | |||
| 9f9173c691 | |||
| a98903a85b | |||
| a2cbe79787 | |||
| 3cef074c9e | |||
| b24f858369 | |||
| 72bb5b42af | |||
| f53bb28b66 | |||
| 2957a45c4b | |||
| c7e5c1fce8 | |||
| 8731f58948 | |||
| 7b64258af6 | |||
| 122ff2d216 | |||
| c0abb0d50d | |||
| 1ff312d236 | |||
| b80f801d23 | |||
| 9fcd1a0d23 | |||
| 9200e22a7e | |||
| d5ff55e23e | |||
| 5dc613cd79 | |||
| 03af183fb3 | |||
| 144cf71368 | |||
| 5eafa37cdd | |||
| 1d86c6da01 | |||
| a2692e1469 | |||
| ed3d14b131 | |||
| 50429a3513 | |||
| b4e1ced3ed | |||
| b92b281050 | |||
| c980fddfa1 | |||
| 613e6d6503 | |||
| 4d0b6b93bc | |||
| f5bcc9b851 | |||
| 152576e85d | |||
| 609b132106 | |||
| d0f2a865bc | |||
| 5940cf24a0 | |||
| 60b5b5d312 | |||
| bffd27ae5b | |||
| 13573f4b3f | |||
| 1bdb7f4e3a | |||
| f9b895b32c | |||
| 3a95d0da01 | |||
| fcd7723f73 | |||
| 47f6c44c17 | |||
| 34b2901566 | |||
| 1adee07127 | |||
| 3c60976efa | |||
| 053b801262 | |||
| 1a37fd0ca4 | |||
| f14d70ea35 | |||
| b6283b3469 | |||
| b19e248383 | |||
| c07905c360 | |||
| da32d0d9e7 | |||
| 4a6c53703f | |||
| 4c84673bdf | |||
| 715f2bc907 | |||
| b78d568d9f | |||
| 3b1e3ea62c | |||
| e65dd33084 | |||
| bec78e84e6 | |||
| e6a343c7ec | |||
| f05dccd384 | |||
| 41f67cabc0 | |||
| bc5e7445d9 | |||
| 2883b4c35b | |||
| 6d199244ef | |||
| 1c27a29238 | |||
| bd64f7bd86 | |||
| a07d954f1c | |||
| 511c8ea79d | |||
| db33707e5e | |||
| ed5431680f | |||
| f1fcde2142 | |||
| 9f2fb716f7 | |||
| 14b4969a65 | |||
| 15feac81c9 | |||
| 2bbf0d1b82 | |||
| 0b06bed1db | |||
| 2055d7a07f | |||
| da92ce3a46 | |||
| dcad1840c4 | |||
| f6694031a1 | |||
| cfddaaae13 | |||
| 8524472d38 | |||
| 863612d1a1 | |||
| e446fc47ce | |||
| e33a887055 | |||
| 78951a3bc4 | |||
| 44318d1ecd | |||
| a6fb44e1ab | |||
| 374d2d1962 | |||
| 3ad143228d | |||
| 4ea14c853e | |||
| 38bbc1c6f5 | |||
| 9a34daa2bc | |||
| e5e3f5f0a3 | |||
| 899a89cb3b | |||
| 63cb564818 | |||
| e93511fde7 | |||
| eeac5c6c45 | |||
| f7b48333df | |||
| cb88a6fd9c | |||
| 7b95546b80 | |||
| 5f9ea6c06a | |||
| d54d1d964d | |||
| 868f271588 | |||
| 7606c9d325 | |||
| 3c66a6a1c1 | |||
| bf900e7468 | |||
| 52cbcb9128 | |||
| 97e7ba25a6 | |||
| bc9ff23372 | |||
| c176ed8861 | |||
| 63c0653006 | |||
| fbf899465d | |||
| 603e478794 | |||
| 347d23641b | |||
| c4e973e83b | |||
| d22abe300b | |||
| 2bab3119b0 | |||
| ea3d7b8892 | |||
| 7eb2f78174 | |||
| 7db04440ff | |||
| 062beb718d | |||
| b8f3cf1b47 | |||
| c280c1339b | |||
| 543acc9fff | |||
| 55d609c82b | |||
| f144c1cdbd | |||
| 1531535151 | |||
| 4fe4c8bb03 | |||
| 1302682900 | |||
| 62e0821be2 | |||
| 856e616247 | |||
| 71fdba3c4a | |||
| 7803bd5c06 | |||
| 8dc9e01439 | |||
| 1d6ce13a6c | |||
| 6a907b1542 | |||
| bef759e358 | |||
| 63023c5266 | |||
| b6f2f9998e | |||
| 6d9115f593 | |||
| 02f7d7c74d | |||
| eeb4ec0e73 | |||
| 9efa05d4de | |||
| 72a8483a23 | |||
| d4da49512c | |||
| 6437970f7f | |||
| bf79618221 | |||
| dbeac1f8eb | |||
| 1625b8a665 | |||
| 5ab202e5c9 | |||
| 2f4fbc9d49 | |||
| 613ffefeb3 | |||
| 89e93be091 | |||
| 9b42077f7a | |||
| 81b88477bd | |||
| 52376e10a5 | |||
| b8b3f9e7ec | |||
| 194eb30042 | |||
| b52aa9ac76 | |||
| c825c41aed | |||
| 6d8f704542 | |||
| 10a399f7e6 | |||
| d59a118e83 | |||
| 07570f9e8a | |||
| 90972e938e | |||
| 4297eb34c9 | |||
| 507f9c3c22 | |||
| 33551089ad | |||
| c8301c6ad6 | |||
| c3396780d7 | |||
| 8ea2b0b63b | |||
| e170d02bff | |||
| 1f872a9b0c | |||
| bcdc1d8915 | |||
| f577435e98 | |||
| bf94550a84 | |||
| a900b04254 | |||
| 3fe5a9d222 | |||
| a07a7676b7 | |||
| 593cc03962 | |||
| 820f255929 | |||
| 7db4146c8d | |||
| 91a6916f4c | |||
| 78374d0301 | |||
| 9185ee0cf9 | |||
| e2de51832c | |||
| 759f16d5b9 | |||
| d0ddedc2b2 | |||
| 4291005161 | |||
| ee144ccb2b | |||
| 7fea21f221 | |||
| 9a4b40f242 | |||
| c2d353b973 | |||
| 8b6cecdbf8 | |||
| 83d8f2821a | |||
| 0225204b4d | |||
| 478957803d | |||
| 0181cb26d2 | |||
| 2ddb3595c7 | |||
| 76e49d1bd0 | |||
| 91f1ee748e | |||
| 9bddf64510 | |||
| 2200ae143e | |||
| 80d9a2ca7d | |||
| cbd1bf35c6 | |||
| c1be57b205 | |||
| 4848bef0dd | |||
| a8f374dd43 | |||
| d43e41e1ba | |||
| 437ac4c59b | |||
| de218409ab | |||
| 060ed859d4 | |||
| 0783c43169 | |||
| b5a317e021 | |||
| 15c1f6dadf | |||
| efda9991f2 | |||
| 9fb651a04e | |||
| 55c652a02a | |||
| 2be706fb3f | |||
| 3e2cc8bfae | |||
| a6c339e13a | |||
| b0174f3acc | |||
| a343d9999e | |||
| 0a2cca6e40 | |||
| bca00f46a9 | |||
| 5848c02d50 | |||
| 29ddcfa1f9 | |||
| 831bb83f4e | |||
| b8a8babc88 | |||
| 4a35aa7272 | |||
| 2ded7d9b1f | |||
| 19f674cf5f | |||
| 790bf5eac3 | |||
| e1adc6a3bc | |||
| bcedab5113 | |||
| adca914d67 | |||
| 8583ab19f0 | |||
| 1aa16a43ee | |||
| e6661d3b0d | |||
| 678e0dc6ac | |||
| bdc10fb729 | |||
| 584fa87bbd | |||
| ac155bbf4c | |||
| 0f6f65045d | |||
| 843b9ddffd | |||
| e283d4cd21 | |||
| 4fe02d27c8 | |||
| 5a269c68ae | |||
| 570a1c85af | |||
| f7ff1ef2bd | |||
| 04530a801c | |||
| 756cd0921b | |||
| 292eee33b9 | |||
| 583c834ba8 | |||
| 33949dbdb1 | |||
| fd79ea4b9b | |||
| b3bff6b43f | |||
| 80aa55b706 | |||
| 76c16ce294 | |||
| 715e6648ae | |||
| 984fad811c | |||
| eafd2c8a13 | |||
| 82fb243ffe | |||
| 6cbf1b2eac | |||
| 043e411a98 | |||
| f93b666bbf | |||
| 7e28aa1474 | |||
| 1deef51df0 | |||
| 6f7934badc | |||
| 2f2680be3c | |||
| 4cd8f4a94c | |||
| a417980a81 | |||
| af69955801 | |||
| 214d49f1d9 | |||
| c4e36a1f97 | |||
| 5de6a1bea6 | |||
| 74a20a0e14 | |||
| 820d08017a | |||
| 258afec391 | |||
| 0cf5aac591 | |||
| 59fd34a4b4 | |||
| 1da3d252e8 | |||
| 3c1cc59d59 | |||
| 1692098d5d | |||
| fbab53af22 | |||
| ce1e263d57 | |||
| 9f99320fda | |||
| 20e1df43d0 | |||
| 728c5434bb | |||
| 542ac4f4e1 | |||
| d23fc228d7 | |||
| 4ff3e47d54 | |||
| 96b22eb557 | |||
| 9187107751 | |||
| c6812b5b11 | |||
| adb584623e | |||
| 120e8de9d1 | |||
| 21726b63f8 | |||
| 04f910ee03 | |||
| edace32213 | |||
| 5e527e434a | |||
| 1d90f7588b | |||
| f8b8a35152 | |||
| fa4c95a9b6 | |||
| a478fc4805 | |||
| febb28e9c4 | |||
| c78a39af50 | |||
| 21e6049c16 | |||
| 6e418337cc | |||
| 48f34053ab | |||
| abfe263750 | |||
| 9ba003b16d | |||
| 48793f3a95 | |||
| d8cf98fd64 | |||
| 78e12d5bee | |||
| bdb8bdf76c | |||
| 88b79eb3a5 | |||
| b6428197ac | |||
| a46138c8b9 | |||
| 1211ca277b | |||
| e6f395c643 | |||
| 1979646b4b | |||
| 5c0eb20cb4 | |||
| 009966a5c7 | |||
| 4427b3b291 | |||
| 3dda4d6540 | |||
| c9df0be874 | |||
| ca2627d3cf | |||
| 47e6527b0e | |||
| 7decbb6eef | |||
| 68da1d0551 | |||
| a6f21b6606 | |||
| 06a4e0c93b | |||
| 0ca1df24ed | |||
| 1d12a906d4 | |||
| 7bd7518963 | |||
| a9c5765be5 | |||
| 2292f63fb6 | |||
| db92b9f5ff | |||
| f538639882 | |||
| 56bc8c2890 | |||
| 1cba4d3fa7 | |||
| 4c7820ceac | |||
| 118dcd8fa0 | |||
| a142ade923 | |||
| 57ab10a87c | |||
| 8c1c3cd634 | |||
| 2d3634d6bf | |||
| 217f29f068 | |||
| 58c3eee153 | |||
| d9e1fb620b | |||
| a3f5b92484 | |||
| 8b96e0ab98 | |||
| eef2d451b7 | |||
| 371e66a6df | |||
| 0d12144744 | |||
| ba39724813 | |||
| af6e6bfc67 | |||
| 315b5a1048 | |||
| c410d4e9f5 | |||
| 299d976622 | |||
| e8587f99c9 | |||
| 63ab96b71b | |||
| e998438135 | |||
| 5fd7d64d21 | |||
| f05037c7d6 | |||
| d0fd654bf7 | |||
| 7165bd91cd | |||
| d3431a5d53 | |||
| fa6c865000 | |||
| fd680a93e0 | |||
| 38b604ad41 | |||
| 2ca67bb61a | |||
| 95b814b751 | |||
| 9963f3f988 | |||
| fde7d4a25a | |||
| 895b2c4f19 | |||
| 427ea9baab | |||
| df718e4498 | |||
| 00956f5bba | |||
| e48d216d79 | |||
| 489f178c7c | |||
| 3bd4eda789 | |||
| fc6c7b8dc6 | |||
| deef1f2c8a | |||
| 38bd38a487 | |||
| 40de64078a | |||
| 780bd5e65a | |||
| 2cd74b4ea9 | |||
| 0cd3df391e | |||
| 854d2b4c27 | |||
| 7227fc7c43 | |||
| 73dcb44121 | |||
| 54fd394ef5 | |||
| fda71166df | |||
| 69b6055353 | |||
| 1bdd0449e0 | |||
| a6fdf9010b | |||
| 941dae0625 | |||
| 4a715bfd17 | |||
| 0b70c7e490 | |||
| 0539836714 | |||
| c08b0e654b | |||
| b3cb48319a | |||
| 44553cc375 | |||
| fbe287a702 | |||
| 5863dcdf67 | |||
| f77bee25ef | |||
| c11328a064 | |||
| d04de2fba0 | |||
| d2b435618c | |||
| 7525bb78e5 | |||
| 2075a572fe | |||
| 73723ba6ba | |||
| 0791820a6c | |||
| 931f352873 | |||
| 7c7d2e0fa4 | |||
| 3372fb6f74 | |||
| bc856269ff | |||
| 06bae231ef | |||
| 65a0edc3a6 | |||
| b7c322d473 | |||
| 0776a04362 | |||
| e51fc5a585 | |||
| 3afc068a02 | |||
| 5cdad44abf | |||
| 43762df998 | |||
| 95228c6dd9 | |||
| 205fcf8487 | |||
| 336e8921ee | |||
| ef149b9fcf | |||
| 766b4c13c3 | |||
| f5605258e3 | |||
| 2ba4d2f2b7 | |||
| 2e050c066e | |||
| 3f83514427 | |||
| 8c227843c9 | |||
| ba084c0a10 | |||
| 3fdd42706d | |||
| b49b51a671 | |||
| e5bb386dd2 | |||
| 2867bb3bc3 | |||
| 416fd02069 | |||
| 581963cfb4 | |||
| 3b14543e66 | |||
| 5dd2194eba | |||
| 5b7880f817 | |||
| bafe1c5781 | |||
| d760be58c3 | |||
| 3da9b70632 | |||
| b7c5902f67 | |||
| 9a22b25564 | |||
| 44c3dec9dc | |||
| 87f3afd8fd | |||
| 53f1129242 | |||
| 74216f75e2 | |||
| ed8eca0c1d | |||
| dc8e6e53c7 | |||
| 989ab5a432 | |||
| ec26c03d58 | |||
| 3b1b3387e7 | |||
| 62c03d1334 | |||
| 51e12184d7 | |||
| 8c01eb9c00 | |||
| bf264d5add | |||
| 8b3bd38bad | |||
| ba0de8800a | |||
| cd5ae4cb7f | |||
| 6575542281 | |||
| bf2559da80 | |||
| fedc207de2 | |||
| 0d58478a73 | |||
| abf24d1942 | |||
| afe3f2f3f3 | |||
| 1d1cb567da | |||
| 699bbee544 | |||
| 9c54915e73 | |||
| ed4390b99d | |||
| 039d9bae68 | |||
| 93ab48ac9a | |||
| c95e312acb | |||
| 9279bc7060 | |||
| fe61576dcd | |||
| 3f6e3074f2 | |||
| 53a8e2aa57 | |||
| 44ab6f181c | |||
| 3a3a830706 | |||
| 1a6e3e73c5 | |||
| 7d508e5a7d | |||
| 370c224d3a | |||
| 7bce501069 | |||
| 9d15445eba | |||
| 16ee13f1f7 | |||
| 8d25eb0acd | |||
| 9cb13a91cd | |||
| 30f8930773 | |||
| 05eaa8d3e0 | |||
| a32ffdf6d4 | |||
| 0f97de1b09 | |||
| e8d6ccec9a | |||
| 005434f79b | |||
| fe997d8b01 | |||
| 7291932a0b | |||
| 49ade03a9a | |||
| dd6dbd25da | |||
| 50bf90fada | |||
| abb81b6390 | |||
| 36da3d3cba | |||
| ae71a99aa4 | |||
| a7034d6351 | |||
| 6010b4c252 | |||
| 8330f4fba9 | |||
| dc6e153b92 | |||
| a2655ee6a5 | |||
| 13248962af | |||
| b698982186 | |||
| 5a299b21c5 | |||
| bb90f11ec8 | |||
| 12369ba2ec | |||
| f5720bde14 | |||
| 0d59a4de48 | |||
| 73c74d0477 | |||
| 70ffd7ded8 | |||
| 92a3a8d6fa | |||
| 6e9cd02b2b | |||
| d0f90af251 | |||
| c53eb27c6f | |||
| 38773e89ff | |||
| 19cb30d360 | |||
| 50a734b977 | |||
| b988758ac2 | |||
| 2567328e8b | |||
| a7568fcbbf | |||
| 27a06ae90c | |||
| 211fd19031 | |||
| fe18611b4b | |||
| 5e9b45ad5f | |||
| af833daee4 | |||
| 1ad5317d6e | |||
| 7cf5df80ce | |||
| d6b880d110 | |||
| cf58a4376e | |||
| a76dcb289a | |||
| 48ec6224e7 | |||
| 127dd8baf4 | |||
| d25c3ff4fc | |||
| f0e9de4cf9 | |||
| 92607a788e | |||
| 82948c1f55 | |||
| 714929c72f | |||
| 98f0d384cb | |||
| e3c87ecf65 | |||
| 1fb9e98eeb | |||
| 01d7c8ec36 | |||
| 22d8d5a0b8 | |||
| 631ed997ba | |||
| 01930ab0cf | |||
| b8fe4c937e | |||
| a7a5b08ad8 | |||
| faaac72b81 | |||
| 8f41139076 | |||
| e1a67acde1 | |||
| 4ab2af51a5 | |||
| 9ffc4eaa40 | |||
| 5eda2581f4 | |||
| 21ceb4fdc4 | |||
| edcf43efba | |||
| eb1ef16b5e | |||
| 6b9c8b7a87 | |||
| a05b96e9a0 | |||
| 699f67aa75 | |||
| 76e469e444 | |||
| 8df630ee0c | |||
| b0c4c53880 | |||
| 116a9c9de0 | |||
| e63f8ab8b8 | |||
| 424245df18 | |||
| 094a11ec64 | |||
| 6b03c9c99f | |||
| 3d116af011 | |||
| cbd965d8e5 | |||
| c9d972fc12 | |||
| da9979494e | |||
| f5c907af33 | |||
| 906fc2dd3d | |||
| a62df536dd | |||
| 7db674b65d | |||
| 784deaa6ea | |||
| 8bc41c2c32 | |||
| 79afc7649d | |||
| 2eee3736df | |||
| a8ba38ef1e | |||
| d3ddbc0c72 | |||
| 6c4085398e | |||
| 2308578622 | |||
| 47cd87e653 | |||
| 87c9684c71 | |||
| 0b66d73178 | |||
| 24cd238eaf | |||
| 72bb9b80ad | |||
| 3d42ac8e44 | |||
| 205ab3b16d | |||
| bb25da274d | |||
| b56de35664 | |||
| 0655ac82f2 | |||
| 166dbfa491 | |||
| 9348977548 | |||
| 769c3eea9b | |||
| e7095af5b8 | |||
| fd42d72423 | |||
| 1382c455a2 | |||
| 1d7fbc841e | |||
| a8a168b0a7 | |||
| dbd1d02485 | |||
| 1c307daa03 | |||
| 7a31f84d34 | |||
| a2a4dd9862 | |||
| 3d8e0f6c23 | |||
| 146da2d072 | |||
| 6a9964e889 | |||
| 318e7c7458 | |||
| f8e2d27bb0 | |||
| 44544f3289 | |||
| 76a5c99b08 | |||
| 70f0f91a17 | |||
| f1394239a3 | |||
| 9e810502c2 | |||
| c67a1752c5 | |||
| d87e40ada5 | |||
| 86bbaa437e | |||
| 96b6b56d95 | |||
| 1f6e5e71ef | |||
| a55f1df17f | |||
| e654226e60 | |||
| 189dc93a6e | |||
| 76cb52878c | |||
| 8d3f0a9f4d | |||
| fe674ef2ea | |||
| 33b165518e | |||
| ff9d509137 | |||
| 278fd5bd59 | |||
| 8ddbc24dd4 | |||
| e93f429906 | |||
| 1dccb1bb64 | |||
| 6d9e67b9f2 | |||
| 4803d48ec7 | |||
| 7c6a12ea8e | |||
| a17d5d01a7 | |||
| cdd909f2dd | |||
| d0e9728c26 | |||
| c8ae428df8 | |||
| a9692f7db4 | |||
| d05a426d00 | |||
| a842b4c4d9 | |||
| 101abaeda1 | |||
| bb99167433 | |||
| e5b57e5ff9 | |||
| 2bd598f7f8 | |||
| b008c5f07f | |||
| a59aa2c30f | |||
| 93de0d4bf3 | |||
| 7a0ac2077c | |||
| 952f459e70 | |||
| 4702196c6e | |||
| f77e064f4e | |||
| c74a9a30bf | |||
| 1d43b36380 | |||
| 736f4edfbd | |||
| c7e2540f6f | |||
| f796296286 | |||
| eb206d2abb | |||
| bff62fb105 | |||
| 5fc83d7615 | |||
| 552a324e08 | |||
| b155d7d1ba | |||
| e4571bf668 | |||
| 53cc929a72 | |||
| 20f5a4cc91 | |||
| 2aabe559bf | |||
| dcf0902c5b | |||
| 0e8219b200 | |||
| 5f7fa0557f | |||
| 63fb4d57e2 | |||
| b5d6f44f4c | |||
| b21c8f8c3a | |||
| fe7c7660d3 | |||
| 56de33c821 | |||
| 1d442cd0e2 | |||
| c79d7957f6 | |||
| 1b9216b341 | |||
| 6d97e5fa5d | |||
| 162af35254 | |||
| abe03811f1 | |||
| e85a869733 | |||
| 0c32d5302c | |||
| f310196937 | |||
| c828dfd596 | |||
| d0b4e092b3 | |||
| 91d7d78621 | |||
| 2bc21f13d4 | |||
| 833edc9568 | |||
| 9d8efce26d | |||
| 6d778ca223 | |||
| 1e739e94e2 | |||
| 8c013aa2a9 | |||
| fb0a0b0dc2 | |||
| ab3d9ddb13 | |||
| a67b32b3ce | |||
| e15e690c5d | |||
| 55c4c25663 | |||
| 202ff53c41 | |||
| 992da7c7be | |||
| f47998a553 | |||
| f4d24420e7 | |||
| 308bdb3d46 | |||
| 20b99dce48 | |||
| e4f7c6add9 | |||
| 80110d1a48 | |||
| 0a0b45fb8e | |||
| 4ef29ae26f | |||
| ead4b89874 | |||
| e827fb2eb2 | |||
| 4f161fb891 | |||
| 8c30a013c7 | |||
| 9f3f877bfd | |||
| 84a75788af | |||
| f3615117d8 | |||
| 48a701ef87 | |||
| 41c72e0a8e | |||
| a83b875b66 | |||
| dcef08009d | |||
| eddba3c652 | |||
| f0c9a458bb | |||
| 62c9e271d8 | |||
| 871a25364d | |||
| e67abae3e0 | |||
| c50565dfda | |||
| 60c44da974 | |||
| ba6d9d0c23 | |||
| 8c55f38b07 | |||
| 568cf5e2ad | |||
| 5e843f7a4f | |||
| 090ada5807 | |||
| 0e17c57856 | |||
| 74464992e6 | |||
| a1d9c21337 | |||
| 248fc15716 | |||
| e38ddebfb6 | |||
| 57fc8b2f1a | |||
| b7fac8bcbc | |||
| 12f2eed5b3 | |||
| 3f39fd487f | |||
| 950bf14d95 | |||
| a279995982 | |||
| a2eb9734f1 | |||
| cb23991841 | |||
| 769d24d196 | |||
| af61f4f1db | |||
| f8f77075ec | |||
| 34bb5f9928 | |||
| ca3cced6ad | |||
| be905ac7be | |||
| 53f3ccc888 | |||
| c304670f47 | |||
| 6388894aa4 | |||
| c27b11bf25 | |||
| 11f395f65f | |||
| 63a0adaa6e | |||
| 0ddeb02d23 | |||
| c23acf9e9e | |||
| 54635bf0d3 | |||
| 6fdd9ed48b | |||
| a0399b7f5e | |||
| 387f6bcad4 | |||
| 0b43431543 | |||
| 34862f9ace | |||
| cd465ca35a | |||
| 93251e0029 | |||
| c2402ddb72 | |||
| d3dcb320f4 | |||
| fd9f734de1 | |||
| 9ea9bf4035 | |||
| f9b70d65d8 | |||
| 90621bb1e3 | |||
| 6ff339b552 | |||
| 9854f4eb2d | |||
| d46b046f2d | |||
| d02e8dcd4e | |||
| 07b1fe8e47 | |||
| 7c368ae029 | |||
| d6b5f92d6c | |||
| 2b70a49e09 | |||
| 8cfa20be1e | |||
| 246f6caf20 | |||
| 7750366654 | |||
| 0f963a93f1 | |||
| ea5b63af18 | |||
| 5e89675c9c | |||
| 5777c1ab27 | |||
| 8eda0aeab3 | |||
| 23c430fadc | |||
| aa423cfa5b | |||
| eb753a3f32 | |||
| 4a300a3cb2 | |||
| c4e16418e0 | |||
| 27e7a67a9a | |||
| ce9f140ddf | |||
| 85c3240b54 | |||
| 9c12e11375 | |||
| 630dbee817 | |||
| 18dc02c700 | |||
| 3d7e509f9a | |||
| ed27e6b8e4 | |||
| 631ce7645f | |||
| 181382b2b7 | |||
| ca15e69ae0 | |||
| ba64ba0bd0 | |||
| 1df4d32d69 | |||
| 5d380453a4 | |||
| ba629f1764 | |||
| f2edcaff85 | |||
| 6d358d4087 | |||
| 46dd50a744 | |||
| 3c2058f0e1 | |||
| c22c407ee5 | |||
| 1ed1dfc78a | |||
| 5797a1d8e5 | |||
| 6d5d40b8e3 | |||
| 90c6b18cbb | |||
| ecb7d5ef10 | |||
| e2b347c783 | |||
| 88a988d876 | |||
| fbeecc0479 | |||
| 413188c995 | |||
| 77818f9342 | |||
| c9ec161ccc | |||
| 20443f8a4d | |||
| 299ceac557 | |||
| 9365e5bfb9 | |||
| 3e75841a83 | |||
| 74b8a0f10f | |||
| c291729ed6 | |||
| a70245a3b1 | |||
| dde022d179 | |||
| 0d12c64c47 | |||
| 27d0a88b36 | |||
| ca55141276 | |||
| e20b9d054d | |||
| c1e3645d57 | |||
| 50db137dea | |||
| 5b109c2b79 | |||
| 25b7093302 | |||
| 38cbb87a62 | |||
| 0c0a978886 | |||
| fb5f368894 | |||
| 9454ffd1af | |||
| 16f35d9a34 | |||
| bb6a64790d | |||
| b9378118dd | |||
| b6485f91ae | |||
| e1e8ca9633 | |||
| 72f476a750 | |||
| f897809202 | |||
| 647d085c5f | |||
| 9d0f99c509 | |||
| fd25a23d91 | |||
| 7fdf165ff3 | |||
| b3e27da26d | |||
| 2479dc4096 | |||
| 7e7a5e692e | |||
| f628a6c3d6 | |||
| 5b0f95fed9 | |||
| 6aa98d5eac | |||
| 8e1fe9558e | |||
| 38c3e53ce7 | |||
| 9627766f7d | |||
| 57697142a2 | |||
| beb32755a3 | |||
| cb6e71e544 | |||
| 1487dcbadc | |||
| a4b27fdeab | |||
| 1137c11c59 | |||
| 14cd84dab7 | |||
| b5c5cd9586 | |||
| 85cc4cb8f7 | |||
| cf6732fb29 | |||
| 1207f5abad | |||
| 6e9394ec7a | |||
| 2c9e32b6c4 | |||
| fc470d0622 | |||
| a3270041e3 | |||
| 956068d0d6 | |||
| 3776e32364 | |||
| fb5a54dd17 | |||
| 916d564f82 | |||
| 364def188a | |||
| d1228a085b | |||
| 6c5a29fb48 | |||
| a83aecaa69 | |||
| 3d885ec262 | |||
| 6fdace07c8 | |||
| 8711658e75 | |||
| e25dc46863 | |||
| f53f54af7f | |||
| 763aa8865b | |||
| 2194cb65a2 | |||
| 60435d505f | |||
| af983c76b8 | |||
| ef161bbb31 | |||
| ac364e5ab7 | |||
| 2e2b1c6f18 | |||
| 1fa1496d7f | |||
| 8fb9365eaa | |||
| 92ab8331d0 | |||
| 603d373cee | |||
| aca2c3a9dd | |||
| f544dab3e0 | |||
| c489940f8b | |||
| dc7ddeaa9b | |||
| 9b5f42cda9 | |||
| 4022e4969d | |||
| ed62d06b5e | |||
| f11e4f6626 | |||
| 59eec5241a | |||
| d287486165 | |||
| f70270a0b3 | |||
| 36380fe5fd | |||
| dc7fca4f4c | |||
| 977759145e | |||
| 76c3660cb2 | |||
| fa10a67811 | |||
| 8d95fd0ca0 | |||
| 332e95701e | |||
| 124b24ab76 | |||
| 6ccd1e43bc | |||
| 5c09d04912 | |||
| 119325c3a2 | |||
| 462a559bd3 | |||
| 1bd58a0103 | |||
| 6c97d08027 | |||
| 0ebda9d224 | |||
| 808fc8dc0d | |||
| aefed73f5a | |||
| ea47750ea4 | |||
| 0f02bfd2c3 | |||
| 8e5a5baf52 | |||
| 3deb8eb488 | |||
| 1dd7f0371d | |||
| 7d032bb684 | |||
| ecc4a40eea | |||
| 83c6914a50 | |||
| 0f06d88e18 | |||
| ea5f7b65f3 | |||
| 9ce95da8f4 | |||
| abd1fd3efb | |||
| 90ab8dac27 | |||
| c96d556094 | |||
| 26f68a890e | |||
| cd5b7b17f6 | |||
| 14cfa69060 | |||
| d6d1b0eeef | |||
| 393d089229 | |||
| 706d9b1f6f | |||
| 0e9228ba7c | |||
| 5a17badfae | |||
| 437c6f8262 | |||
| 65d55d6660 | |||
| 7ba1aabc09 | |||
| 470fdd62bb | |||
| 6434d10e52 | |||
| 2ed4fc9fbf | |||
| b418895d9d | |||
| 8939927543 | |||
| 0bbe6a0a12 | |||
| 767784a79c | |||
| 64abfd4408 | |||
| 872e2f9753 | |||
| 09f7225eb7 | |||
| 95bb0ac6d4 | |||
| f97596689f | |||
| 204be84c0f | |||
| 93d8ba0b0f | |||
| b07c50e580 | |||
| fc0dc8aea0 | |||
| 284ed9dea1 | |||
| 1651a90dea | |||
| a888427777 | |||
| 6b53b78ee3 | |||
| c2faa605d3 | |||
| 8bf5a6e0bc | |||
| 80551124f1 | |||
| 652f8227b5 | |||
| 42f68f61c6 | |||
| fcb4104856 | |||
| a0139f4157 | |||
| 6c78060876 | |||
| b9b2f9f2c3 | |||
| 87d5cb78b2 | |||
| cdf421f0f1 | |||
| 2e58757bc9 | |||
| c689836208 | |||
| 4efc320f23 | |||
| 6c1a602bdc | |||
| 0ae994de56 | |||
| e7f4a5bd59 | |||
| 180973d49f | |||
| 705910d9e0 | |||
| b5dfc337ec | |||
| 8996b562bc | |||
| 1ae6186647 | |||
| 2848417cf5 | |||
| d3506acd94 | |||
| 9e9ea41bdd | |||
| 7b0aa7b770 | |||
| 3a25d108fe | |||
| d98e213b92 | |||
| 4d44562ada | |||
| b733b3c59f | |||
| 7b54988514 | |||
| ec4da47af6 | |||
| 633d59c13b | |||
| c06a92e0ae | |||
| 18bd9d62cb | |||
| 0bce6c6a46 | |||
| 0465442803 | |||
| eb667bc436 | |||
| c81628a66e | |||
| 50d3631bc4 | |||
| e971069595 | |||
| ac4c0ec1f6 | |||
| fe3d2e0af4 | |||
| a4b762e1b1 | |||
| daa0015fbd | |||
| 804248d6ad | |||
| 78c4c67a6c | |||
| c23be53bfd | |||
| d7e3e70430 | |||
| e95a859ee9 | |||
| 1a3704e700 | |||
| f49048c6e1 | |||
| 59226365c5 | |||
| 683ce431db | |||
| 8d4e796f42 | |||
| 3da1fbf6ca | |||
| 419e25df23 | |||
| 7fddf80c09 | |||
| fa85e61d6f | |||
| ebac0db0df | |||
| 0404f30c87 | |||
| 769fd7b524 | |||
| 4b5553abef | |||
| e730eb3a32 | |||
| 2933b6e732 | |||
| 94ae2f2658 | |||
| 3453451df9 | |||
| 80d4a2b242 | |||
| c9b1a062ce | |||
| 717ffe560f | |||
| 109e2fa82d | |||
| d03fc2fcf1 | |||
| d0111e7741 | |||
| 5e76d6d865 | |||
| 90e67b22b4 | |||
| 92ba6c9802 | |||
| 0286a51525 | |||
| b552e2cda8 | |||
| 0feb56cb3e | |||
| e1a0acdf4a | |||
| 6586f933ff | |||
| 4ec770da63 | |||
| 39b84a083d | |||
| 28857473b2 | |||
| 2bfc10dab4 | |||
| 1dce1157f4 | |||
| 08d53d52e7 | |||
| 2918d97fd0 | |||
| 701e170e19 | |||
| 71ee669fc5 | |||
| 661ac8c6f4 | |||
| 210f6b51df | |||
| 20611b6e4b | |||
| 689e4b8d23 | |||
| d0378ea528 | |||
| 5454ed3458 | |||
| 0070d46a20 | |||
| 540c10aaff | |||
| f5dfc3604a | |||
| b2c5304efe | |||
| fa512f16c6 | |||
| cbfb25b390 | |||
| b1e55201cf | |||
| 8beeb64127 | |||
| 2d2938c8ad | |||
| e9005d57da | |||
| 69f7c8a500 | |||
| 98fd24ffdc | |||
| 21c3e90b58 | |||
| 49281e0d15 | |||
| e597f81b45 | |||
| 9f6047aebd | |||
| 0745f964c1 | |||
| 18b1ad7b62 | |||
| 9448936e32 | |||
| f1dc436a07 | |||
| f163e24201 | |||
| 81f4f1c46f | |||
| a82e74381f | |||
| 7459896bac | |||
| dd2783b040 | |||
| 23ff2c3ab4 | |||
| ad51191a77 | |||
| 8ef5ab6fe9 | |||
| 2218f9781b | |||
| d08d727917 | |||
| 8cf030585f | |||
| c29515db38 | |||
| d9e27bfaf1 | |||
| 2e8830b9d3 | |||
| 4d3d6ea9a7 | |||
| e69f344ab2 |
@@ -1,15 +0,0 @@
|
||||
{
|
||||
"defaultHomeserver": 0,
|
||||
"homeserverList": ["matrix.lotusguild.org"],
|
||||
"allowCustomHomeservers": false,
|
||||
"featuredCommunities": {
|
||||
"openAsDefault": false,
|
||||
"spaces": [],
|
||||
"rooms": [],
|
||||
"servers": []
|
||||
},
|
||||
"hashRouter": {
|
||||
"enabled": false,
|
||||
"basename": "/"
|
||||
}
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
{
|
||||
"defaultHomeserver": 0,
|
||||
"homeserverList": [
|
||||
"matrix.lotusguild.org"
|
||||
],
|
||||
"allowCustomHomeservers": false,
|
||||
"featuredCommunities": {
|
||||
"openAsDefault": false,
|
||||
"spaces": [],
|
||||
"rooms": [],
|
||||
"servers": []
|
||||
},
|
||||
"hashRouter": {
|
||||
"enabled": false,
|
||||
"basename": "/"
|
||||
}
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
VITE_SENTRY_DSN=https://264a5e95c5d31fe080a2e92fb008294d@o4511430568378368.ingest.us.sentry.io/4511430571982849
|
||||
VITE_APP_VERSION=lotus
|
||||
@@ -0,0 +1,2 @@
|
||||
experiment
|
||||
node_modules
|
||||
@@ -0,0 +1,72 @@
|
||||
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',
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -1,96 +0,0 @@
|
||||
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
|
||||
run: npm ci
|
||||
|
||||
# ── Critical gate — if this fails, nothing deploys ──────────────────
|
||||
- name: Build
|
||||
run: npm run build
|
||||
env:
|
||||
NODE_OPTIONS: '--max_old_space_size=4096'
|
||||
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
|
||||
VITE_APP_VERSION: ${{ github.sha }}
|
||||
|
||||
# ── 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:
|
||||
- type: markdown #add faqs in future
|
||||
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.
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: '# Issue Details'
|
||||
value: "# Issue Details"
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Issue Description
|
||||
@@ -64,7 +64,7 @@ body:
|
||||
- Browser:
|
||||
- Cinny Web Version: (app.cinny.in or self hosted)
|
||||
- Cinny desktop Version: (appimage or deb or flatpak)
|
||||
- Matrix Homeserver:
|
||||
- Matrix Homeserver:
|
||||
placeholder: |
|
||||
- OS: Windows 11
|
||||
- Browser: Chrome 120.0.6099.109
|
||||
@@ -80,12 +80,12 @@ body:
|
||||
label: Relevant Logs
|
||||
description: |
|
||||
If applicable, add browser console logs to help explain your problem.
|
||||
|
||||
|
||||
**To get browser console logs:**
|
||||
- Chrome/Edge: Press F12 → Console tab
|
||||
- Firefox: Press F12 → Console tab
|
||||
- Safari: Develop → Show Web Inspector → Console
|
||||
|
||||
|
||||
Please wrap large log outputs in code blocks with triple backticks (```).
|
||||
placeholder: |
|
||||
```
|
||||
@@ -98,7 +98,7 @@ body:
|
||||
render: shell
|
||||
validations:
|
||||
required: false
|
||||
- type: textarea
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Additional context
|
||||
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).
|
||||
- type: checkboxes #add faqs in future
|
||||
attributes:
|
||||
label: 'I acknowledge that:'
|
||||
label: "I acknowledge that:"
|
||||
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.
|
||||
required: true
|
||||
|
||||
@@ -2,29 +2,29 @@
|
||||
|
||||
version: 2
|
||||
updates:
|
||||
# - package-ecosystem: npm
|
||||
# directory: /
|
||||
# schedule:
|
||||
# interval: weekly
|
||||
# day: "tuesday"
|
||||
# time: "01:00"
|
||||
# timezone: "Asia/Kolkata"
|
||||
# open-pull-requests-limit: 15
|
||||
# - package-ecosystem: npm
|
||||
# directory: /
|
||||
# schedule:
|
||||
# interval: weekly
|
||||
# day: "tuesday"
|
||||
# time: "01:00"
|
||||
# timezone: "Asia/Kolkata"
|
||||
# open-pull-requests-limit: 15
|
||||
|
||||
- package-ecosystem: github-actions
|
||||
directory: /
|
||||
schedule:
|
||||
interval: weekly
|
||||
day: 'tuesday'
|
||||
time: '01:00'
|
||||
timezone: 'Asia/Kolkata'
|
||||
day: "tuesday"
|
||||
time: "01:00"
|
||||
timezone: "Asia/Kolkata"
|
||||
open-pull-requests-limit: 5
|
||||
|
||||
- package-ecosystem: docker
|
||||
directory: /
|
||||
schedule:
|
||||
interval: weekly
|
||||
day: 'tuesday'
|
||||
time: '01:00'
|
||||
timezone: 'Asia/Kolkata'
|
||||
day: "tuesday"
|
||||
time: "01:00"
|
||||
timezone: "Asia/Kolkata"
|
||||
open-pull-requests-limit: 5
|
||||
|
||||
@@ -16,7 +16,7 @@ jobs:
|
||||
- name: Setup node
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||
with:
|
||||
node-version-file: '.node-version'
|
||||
node-version-file: ".node-version"
|
||||
package-manager-cache: false
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
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:
|
||||
workflow_run:
|
||||
workflows: ['Build pull request']
|
||||
types: [completed]
|
||||
workflows: ["Build pull request"]
|
||||
types: [completed]
|
||||
|
||||
jobs:
|
||||
deploy-pull-request:
|
||||
@@ -21,15 +21,9 @@ jobs:
|
||||
workflow: ${{ github.event.workflow.id }}
|
||||
run_id: ${{ github.event.workflow_run.id }}
|
||||
name: pr
|
||||
- name: Validate and output pr number
|
||||
- name: Output pr number
|
||||
id: pr
|
||||
run: |
|
||||
PR_ID=$(<pr.txt)
|
||||
if ! [[ "${PR_ID}" =~ ^[0-9]+$ ]]; then
|
||||
echo "::error::pr.txt contains non-numeric content: ${PR_ID}"
|
||||
exit 1
|
||||
fi
|
||||
echo "id=${PR_ID}" >> "${GITHUB_OUTPUT}"
|
||||
run: echo "id=$(<pr.txt)" >> $GITHUB_OUTPUT
|
||||
- name: Download artifact
|
||||
uses: dawidd6/action-download-artifact@b6e2e70617bc3265edd6dab6c906732b2f1ae151 # v21
|
||||
with:
|
||||
@@ -42,13 +36,13 @@ jobs:
|
||||
uses: nwtgck/actions-netlify@4cbaf4c08f1a7bfa537d6113472ef4424e4eb654 # v3.0.0
|
||||
with:
|
||||
publish-dir: dist
|
||||
deploy-message: 'Deploy PR ${{ steps.pr.outputs.id }}'
|
||||
deploy-message: "Deploy PR ${{ steps.pr.outputs.id }}"
|
||||
alias: ${{ steps.pr.outputs.id }}
|
||||
# These don't work because we're in workflow_run
|
||||
enable-pull-request-comment: false
|
||||
enable-commit-comment: false
|
||||
env:
|
||||
NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN_PR }}
|
||||
NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
|
||||
NETLIFY_SITE_ID: ${{ secrets.NETLIFY_SITE_ID_PR_CINNY }}
|
||||
timeout-minutes: 1
|
||||
- name: Comment preview on PR
|
||||
@@ -59,5 +53,5 @@ jobs:
|
||||
pr-number: ${{ steps.pr.outputs.id }}
|
||||
comment-tag: ${{ steps.pr.outputs.id }}
|
||||
message: |
|
||||
Preview: ${{ steps.netlify.outputs.deploy-url }}
|
||||
⚠️ Exercise caution. Use test accounts. ⚠️
|
||||
Preview: ${{ steps.netlify.outputs.deploy-url }}
|
||||
⚠️ Exercise caution. Use test accounts. ⚠️
|
||||
|
||||
@@ -20,7 +20,7 @@ jobs:
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3.7.0
|
||||
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0
|
||||
|
||||
|
||||
@@ -23,4 +23,4 @@ jobs:
|
||||
collapsibleThreshold: 25
|
||||
failOnDowngrade: false
|
||||
path: package-lock.json
|
||||
updateComment: true
|
||||
updateComment: true
|
||||
@@ -15,7 +15,7 @@ jobs:
|
||||
- name: Setup node
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||
with:
|
||||
node-version-file: '.node-version'
|
||||
node-version-file: ".node-version"
|
||||
package-manager-cache: false
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
@@ -1,23 +1,39 @@
|
||||
name: Production deploy
|
||||
|
||||
on:
|
||||
release:
|
||||
types: [published]
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
deploy-and-tarball:
|
||||
name: Netlify deploy and tarball
|
||||
outputs:
|
||||
version: ${{ steps.vars.outputs.tag }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Setup node
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||
with:
|
||||
node-version-file: '.node-version'
|
||||
node-version-file: ".node-version"
|
||||
package-manager-cache: false
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
- name: Run semantic release
|
||||
run: npm run semantic-release
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
GIT_AUTHOR_NAME: ${{ secrets.GIT_AUTHOR_NAME }}
|
||||
GIT_AUTHOR_EMAIL: ${{ secrets.GIT_AUTHOR_EMAIL }}
|
||||
GIT_COMMITTER_NAME: ${{ secrets.GIT_AUTHOR_NAME }}
|
||||
GIT_COMMITTER_EMAIL: ${{ secrets.GIT_AUTHOR_EMAIL }}
|
||||
- name: Get version from tag
|
||||
id: vars
|
||||
run: |
|
||||
TAG=$(git describe --tags --abbrev=0)
|
||||
echo "tag=$TAG" >> $GITHUB_OUTPUT
|
||||
- name: Build app
|
||||
env:
|
||||
NODE_OPTIONS: '--max_old_space_size=4096'
|
||||
@@ -26,7 +42,7 @@ jobs:
|
||||
uses: nwtgck/actions-netlify@4cbaf4c08f1a7bfa537d6113472ef4424e4eb654 # v3.0.0
|
||||
with:
|
||||
publish-dir: dist
|
||||
deploy-message: 'Prod deploy ${{ github.ref_name }}'
|
||||
deploy-message: 'Prod deploy ${{ steps.vars.outputs.tag }}'
|
||||
enable-commit-comment: false
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
production-deploy: true
|
||||
@@ -36,9 +52,6 @@ jobs:
|
||||
NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
|
||||
NETLIFY_SITE_ID: ${{ secrets.NETLIFY_SITE_ID_APP }}
|
||||
timeout-minutes: 1
|
||||
- name: Get version from tag
|
||||
id: vars
|
||||
run: echo "tag=${GITHUB_REF#refs/*/}" >> $GITHUB_OUTPUT
|
||||
- name: Create tar.gz
|
||||
run: tar -czvf cinny-${{ steps.vars.outputs.tag }}.tar.gz dist
|
||||
- name: Sign tar.gz
|
||||
@@ -54,12 +67,16 @@ jobs:
|
||||
- name: Upload tagged release
|
||||
uses: softprops/action-gh-release@b4309332981a82ec1c5618f44dd2e27cc8bfbfda # v3.0.0
|
||||
with:
|
||||
tag_name: ${{ steps.vars.outputs.tag }}
|
||||
files: |
|
||||
cinny-${{ steps.vars.outputs.tag }}.tar.gz
|
||||
cinny-${{ steps.vars.outputs.tag }}.tar.gz.asc
|
||||
|
||||
publish-image:
|
||||
name: Push Docker image to Docker Hub, GHCR
|
||||
needs: deploy-and-tarball
|
||||
env:
|
||||
VERSION: ${{ needs.deploy-and-tarball.outputs.version }}
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
@@ -67,6 +84,8 @@ jobs:
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3.7.0
|
||||
- name: Set up Docker Buildx
|
||||
@@ -89,6 +108,9 @@ jobs:
|
||||
images: |
|
||||
${{ secrets.DOCKER_USERNAME }}/cinny
|
||||
ghcr.io/${{ github.repository }}
|
||||
tags: |
|
||||
type=raw,value=${{ env.VERSION }}
|
||||
type=raw,value=latest
|
||||
- name: Build and push Docker image
|
||||
uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0
|
||||
with:
|
||||
@@ -96,4 +118,4 @@ jobs:
|
||||
platforms: linux/amd64,linux/arm64
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
@@ -4,5 +4,4 @@ node_modules
|
||||
devAssets
|
||||
|
||||
.DS_Store
|
||||
.ideapackage-lock.json
|
||||
public/decorations/
|
||||
.idea
|
||||
@@ -17,23 +17,23 @@ diverse, inclusive, and healthy community.
|
||||
Examples of behavior that contributes to a positive environment for our
|
||||
community include:
|
||||
|
||||
- Demonstrating empathy and kindness toward other people
|
||||
- Being respectful of differing opinions, viewpoints, and experiences
|
||||
- Giving and gracefully accepting constructive feedback
|
||||
- Accepting responsibility and apologizing to those affected by our mistakes,
|
||||
* Demonstrating empathy and kindness toward other people
|
||||
* Being respectful of differing opinions, viewpoints, and experiences
|
||||
* Giving and gracefully accepting constructive feedback
|
||||
* Accepting responsibility and apologizing to those affected by our mistakes,
|
||||
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
|
||||
|
||||
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
|
||||
- Trolling, insulting or derogatory comments, and personal or political attacks
|
||||
- Public or private harassment
|
||||
- Publishing others' private information, such as a physical or email
|
||||
* Trolling, insulting or derogatory comments, and personal or political attacks
|
||||
* Public or private harassment
|
||||
* Publishing others' private information, such as a physical or email
|
||||
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
|
||||
|
||||
## Enforcement Responsibilities
|
||||
@@ -106,7 +106,7 @@ Violating these terms may lead to a permanent ban.
|
||||
### 4. Permanent Ban
|
||||
|
||||
**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.
|
||||
|
||||
**Consequence**: A permanent ban from any sort of public interaction within
|
||||
|
||||
@@ -5,7 +5,6 @@ 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. 🎉
|
||||
|
||||
> 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
|
||||
> - Tweet about it (tag @cinnyapp)
|
||||
> - Refer this project in your project's readme
|
||||
@@ -19,7 +18,6 @@ Bug reports and feature suggestions must use descriptive and concise titles and
|
||||
## Pull requests
|
||||
|
||||
> ### 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.
|
||||
|
||||
**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.
|
||||
@@ -28,9 +26,9 @@ Bug reports and feature suggestions must use descriptive and concise titles and
|
||||
|
||||
Example:
|
||||
|
||||
| Not ideal | Better |
|
||||
| ----------------------------------- | --------------------------------------------- |
|
||||
| Fixed markAllAsRead in RoomTimeline | Fix read marker when paginating room timeline |
|
||||
|Not ideal|Better|
|
||||
|---|----|
|
||||
|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.
|
||||
|
||||
@@ -41,7 +39,6 @@ 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).**
|
||||
|
||||
## Helpful links
|
||||
|
||||
- [BEM methodology](http://getbem.com/introduction/)
|
||||
- [Atomic design](https://bradfrost.com/blog/post/atomic-web-design/)
|
||||
- [Matrix JavaScript SDK documentation](https://matrix-org.github.io/matrix-js-sdk/index.html)
|
||||
|
||||
@@ -1,240 +0,0 @@
|
||||
# Lotus Chat — Bug Report & Technical Audit
|
||||
|
||||
**Date:** June 2026
|
||||
|
||||
This document tracks identified bugs, edge cases, and architectural discrepancies found during the audit of the Lotus Chat codebase. Recommended fixes are provided for each item.
|
||||
|
||||
---
|
||||
|
||||
## 🚩 Critical & UI Bugs
|
||||
|
||||
### 1. No Camera Focus During Screenshare
|
||||
- **File:** `cinny/src/app/features/call/CallControls.tsx`
|
||||
- **Status:** **OPEN**
|
||||
- **Issue:** Automatic screenshare spotlighting forces primary display override, preventing users from manually focusing on camera feeds.
|
||||
- **Root Cause:** Current spotlighting logic prioritizes active screenshare streams over manual participant selections, effectively ignoring or overriding user-initiated focus states.
|
||||
- **Proposed Fix:** Introduce a manual 'Focus' state that takes precedence over automatic screenshare spotlighting, implemented via a toggle/click UI on participant tiles. Update the video renderer to respect this manual override.
|
||||
|
||||
### 2. Chat Background Animation Flickering
|
||||
- **File:** `cinny/src/app/features/lotus/chatBackground.ts`
|
||||
- **Status:** **OPEN**
|
||||
- **Issue:** Animated background properties cause visible flickering on message text and the composer area, particularly on browsers/GPUs susceptible to repaint-induced artifacts.
|
||||
- **Root Cause:** Animation triggers excessive repaints or layout recalculations on descendant elements, likely due to animating non-GPU accelerated properties on parent containers without proper rendering context isolation.
|
||||
- **Proposed Fix:** Promote background container to a compositor layer using `will-change: transform`, strictly limit animations to `transform` and `opacity` properties, and utilize `contain: paint;` to isolate the background rendering context.
|
||||
|
||||
### 3. Avatar Decorations in Element Call
|
||||
- **File:** `cinny/src/app/components/avatar-decoration/AvatarDecoration.tsx`
|
||||
- **Status:** **OPEN**
|
||||
- **Issue:** Avatar decorations are failing to render within the call/room interface member lists.
|
||||
- **Root Cause:** Likely a mismatch between the expected `member` object structure required by the `AvatarDecoration` component and the data actually provided by the call/room UI components. Matrix event data for decorations might not be propagating correctly to these UI member objects.
|
||||
- **Proposed Fix:** Analyze the data propagation chain from Matrix events to the member object in `cinny/src/app/components/call` and `room`, ensuring that decoration-related properties are correctly mapped and passed to the `AvatarDecoration` component.
|
||||
|
||||
### 4. DM and Group Message Calls
|
||||
- **File:** `cinny/src/app/components/CallEmbedProvider.tsx`
|
||||
- **Status:** **OPEN**
|
||||
- **Issue:** Incoming call ringtone is hardcoded, lacks volume control, and is suppressed if the user is already in an active call.
|
||||
- **Root Cause:** Ringing logic is tightly coupled to `RTCNotification` events in `CallEmbedProvider.tsx`, using a hardcoded audio file path. It lacks an abstraction for sound management or user-configurable settings for ringtones/volumes.
|
||||
- **Proposed Fix:** Migrate sound asset management to a dedicated audio service. Implement user-configurable settings for ringtone and notification volume. Update the `IncomingCallListener` to support ringing even during active calls (if appropriate) by enhancing event handling.
|
||||
|
||||
### 5. Seasonal Themes and Chat Backgrounds Design
|
||||
- **File:** `cinny/src/app/hooks/useTheme.ts`, `cinny/src/app/features/lotus/chatBackground.ts`
|
||||
- **Status:** **OPEN**
|
||||
- **Issue:** Basic CSS or random moving lines are insufficient for high-fidelity wallpaper/theming. They lack professional design theory, coherence, and aesthetic depth.
|
||||
- **Root Cause:** Current implementation relies on basic CSS, lacks advanced design theory, and does not leverage modern, performant CSS wallpaper techniques.
|
||||
- **Proposed Fix (Extreme Depth Redesign):**
|
||||
- **Research-Backed Implementation:** Implement advanced design techniques (layered `oklch` gradients, `backdrop-filter` for refractive "liquid glass" effects, GPU-accelerated `transform` animations) to create living, breathing backgrounds.
|
||||
- **Performance Optimization:** Ensure all animations strictly use compositor-thread properties (`transform`, `opacity`) and apply `contain: paint` / `will-change: transform` to prevent layout thrashing/flickering.
|
||||
- **Design Resources (Examples/Inspiration):**
|
||||
- [Uiverse.io Patterns](https://uiverse.io/patterns)
|
||||
- [MagicPattern CSS Backgrounds](https://www.magicpattern.design/tools/css-backgrounds)
|
||||
- [Prismic Blog: CSS Background Effects](https://prismic.io/blog/css-background-effects)
|
||||
- [CSS-Pattern.com](https://css-pattern.com) (Pure CSS pattern library)
|
||||
- [BGJar](https://bgjar.com) (Performance-focused generators)
|
||||
- **Goal:** Treat each theme/background as a week-long development sprint to ensure professional polish, WCAG AA contrast compliance for overlaying UI, and seamless integration with the Lotus TDS.
|
||||
|
||||
### 6. Exclusive Background vs. Seasonal Choice
|
||||
- **File:** `cinny/src/app/state/settings.ts`
|
||||
- **Status:** **OPEN**
|
||||
- **Issue:** Concurrent application of both Chat Backgrounds and Seasonal Themes causes visual clutter and high GPU usage.
|
||||
- **Root Cause:** These are currently handled as independent settings in the `settingsAtom` and applied simultaneously without mutual exclusion.
|
||||
- **Proposed Fix:** Introduce mutual exclusion in the settings application logic. Update the settings UI to present these as a single choice (e.g., a radio group or toggled selection) where activating one deactivates the other. Enforce this rule in `cinny/src/app/features/lotus/chatBackground.ts` and `cinny/src/app/components/seasonal/SeasonalEffect.tsx`.
|
||||
|
||||
### 7. Tiny Touch Targets in Composer Toolbar
|
||||
- **File:** `cinny/src/app/features/room/RoomInput.tsx`
|
||||
- **Status:** **OPEN**
|
||||
- **Issue:** Toolbar buttons have hit areas smaller than the WCAG-recommended 44x44px for touch, hindering mobile accessibility.
|
||||
- **Root Cause:** The `IconButton` component does not explicitly enforce a 44x44px touch target, and the default size is too small for mobile touch precision.
|
||||
- **Proposed Fix:** Apply CSS `min-width: 44px; min-height: 44px;` to all toolbar icons, potentially via a dedicated `MobileTouchTarget` style wrapper or by overriding the `IconButton` component's style for mobile viewports using media queries.
|
||||
|
||||
### 8. Horizontal Overflow in Room Settings
|
||||
- **File:** `cinny/src/app/features/room-settings/RoomSettings.tsx`
|
||||
- **Status:** **OPEN**
|
||||
- **Issue:** Wide tables and input elements in room settings cause horizontal overflow on mobile viewports.
|
||||
- **Root Cause:** A fixed-width constraint applied to the `PageNav` component in `cinny/src/app/components/page/style.css.ts` overrides responsive layout attempts on smaller screens.
|
||||
- **Proposed Fix:** Update the `PageNav` style definition to be responsive, setting its width to `100%` on mobile viewports using CSS media queries, and ensure all child containers in `RoomSettings` use `flex-wrap: wrap` or similar overflow-handling layouts.
|
||||
|
||||
### 9. Modal Float-Style Responsiveness
|
||||
- **File:** `cinny/src/app/components/modal/Modal.tsx`
|
||||
- **Status:** **OPEN**
|
||||
- **Issue:** Modals appear as floating boxes on mobile, creating navigation and readability challenges.
|
||||
- **Root Cause:** Missing viewport-dependent styling in the base modal component.
|
||||
- **Proposed Fix:** Implement media-query-based responsiveness in the base `Modal` wrapper. For viewports below a certain threshold, override floating styles with full-screen layout: `width: 100vw; height: 100vh; border-radius: 0;`.
|
||||
|
||||
### 10. Composer Keyboard Obscurity
|
||||
- **File:** `cinny/src/app/features/room/RoomInput.tsx`
|
||||
- **Status:** **OPEN**
|
||||
- **Issue:** The chat composer is often partially or fully obscured by the virtual keyboard on mobile.
|
||||
- **Root Cause:** Use of `vh` units for container height does not adapt when the virtual keyboard appears, pushing the `RoomInput` container off-screen.
|
||||
- **Proposed Fix:** Adopt modern viewport units (`100svh`) for main layout containers to ensure the height adapts dynamically. Optionally, use `scrollIntoView` on the `RoomInput` container when it receives focus.
|
||||
|
||||
### 11. Inline Jotai atom creation
|
||||
- **File:** `cinny/src/app/hooks/useSpaceHierarchy.ts`
|
||||
- **Status:** **OPEN**
|
||||
- **Issue:** Inline Jotai atom creation in a hook risks re-rendering components unnecessarily.
|
||||
- **Proposed Fix:** Lift atom definition out of the hook or utilize `useMemo` to ensure atom stability.
|
||||
|
||||
---
|
||||
|
||||
## 📦 Barrel File Audit
|
||||
|
||||
| File Path | Note | Status |
|
||||
| :--- | :--- | :--- |
|
||||
| `cinny/src/app/plugins/call/index.ts` | Extensive `export *` usage | OPEN |
|
||||
| `cinny/src/app/plugins/text-area/index.ts` | Extensive `export *` usage | OPEN |
|
||||
| `cinny/src/app/components/message/index.ts` | Extensive `export *` usage | OPEN |
|
||||
|
||||
---
|
||||
|
||||
## 🔍 Technical & Performance Refinements
|
||||
|
||||
| Category | Issue Description | File Path | Status |
|
||||
| :--- | :--- | :--- | :--- |
|
||||
| State Sync | Fire-and-forget network call to set offline presence during `pagehide` event may not complete reliably, potentially causing UI drift in presence status. | `cinny/src/app/hooks/usePresenceUpdater.ts` | OPEN |
|
||||
| State Sync | Fire-and-forget network call `setPresence().catch(...)` suppresses errors, meaning the app may falsely assume presence update success. | `cinny/src/app/hooks/usePresenceUpdater.ts` | OPEN |
|
||||
| Memory Leak | Decrypted Media Memory Leak (Gallery & Lightbox) due to missing virtualization and blob revocation. | `cinny/src/app/features/room/MediaGallery.tsx` | OPEN |
|
||||
| Data Persistence | Scheduled Messages are ephemeral (lost on refresh) due to fragile `localStorage` parsing. | `cinny/src/app/state/scheduledMessages.ts` | OPEN |
|
||||
| Memory Leak | Potential memory leak due to uncleaned `handleMouseMove` listener in `usePan`. | `cinny/src/app/hooks/usePan.ts` | OPEN |
|
||||
| Asset Optimization | Large unoptimized media asset (213KB) found in `public/res`. | `public/res/Lotus.png` | OPEN |
|
||||
| Data Persistence | Non-atomic `localStorage` updates in session management can lead to inconsistent state. | `cinny/src/app/state/sessions.ts` | OPEN |
|
||||
| Data Persistence | Lack of cross-tab synchronization for `localStorage` updates in session management risks race conditions. | `cinny/src/app/state/sessions.ts` | OPEN |
|
||||
| Network Resilience | `uploadContent` lacks retry logic, failing immediately upon network error. | `cinny/src/app/utils/matrix.ts` | OPEN |
|
||||
| Network Resilience | `rateLimitedActions` uses basic retry logic without exponential backoff, which may exacerbate 429 issues. | `cinny/src/app/utils/matrix.ts` | OPEN |
|
||||
| Matrix Event Robustness | `useMatrixEventRenderer` handles unknown events gracefully by returning `null`, which may hide potentially important unrendered data. | `cinny/src/app/hooks/useMatrixEventRenderer.ts` | OPEN |
|
||||
| Data Contract | `MatrixError` instantiation with `UploadResponse` might be brittle. | `cinny/src/app/utils/matrix.ts` | OPEN |
|
||||
| Type Safety | `addRoomIdToMDirect` uses `as any` cast for `AccountDataEvent.Direct`, bypassing type contract validation. | `cinny/src/app/utils/matrix.ts` | OPEN |
|
||||
| Robustness | `rateLimitedActions` relies on `MatrixError.httpStatus` which might not exist on all error variants. | `cinny/src/app/utils/matrix.ts` | OPEN |
|
||||
| Type Contract | Custom types in `cinny/src/types/matrix` mirror SDK types instead of using them, risking drift and contract mismatches. | `cinny/src/types/matrix/` | OPEN |
|
||||
|
||||
## 🏗️ Architectural & Hygiene Audit
|
||||
|
||||
| Category | Issue Description | File Path | Status |
|
||||
| :--- | :--- | :--- | :--- |
|
||||
| Hygiene | No stale development notes or TypeScript strictness issues found | N/A | OPEN |
|
||||
|
||||
---
|
||||
|
||||
## 🏗️ TDS Compliance & Styling Issues
|
||||
|
||||
| Issue Description | File Path |
|
||||
| :--- | :--- |
|
||||
| Hardcoded inline style `cursor: 'pointer'` | `cinny/src/app/plugins/react-custom-html-parser.tsx` |
|
||||
| Hardcoded color `#00D4FF`, `#FFB300` | `cinny/src/app/components/event-readers/EventReaders.tsx` |
|
||||
| Hardcoded color `#EE1D52`, `#9146ff`, `#ff4500`, `#cb3837`, `#f48024` | `cinny/src/app/components/url-preview/UrlPreviewCard.tsx` |
|
||||
| Massive number of hardcoded `backgroundColor` values | `cinny/src/app/features/lotus/chatBackground.ts` |
|
||||
| Hardcoded colors `#00FF88`, `#FF6B00` | `cinny/src/app/features/call/CallControls.tsx` |
|
||||
| Hardcoded fallback hexes in toast colors | `cinny/src/app/features/toast/LotusToastContainer.tsx` |
|
||||
|
||||
---
|
||||
|
||||
## 🌐 Localization, Accessibility & Performance
|
||||
|
||||
| Category | Issue Description | File Path | Status |
|
||||
| :--- | :--- | :--- | :--- |
|
||||
| Localization | Hardcoded UI string: "Chat Room" | `src/app/components/create-room/CreateRoomTypeSelector.tsx` | OPEN |
|
||||
| Localization | Hardcoded UI string: "Messages, photos, and videos." | `src/app/components/create-room/CreateRoomTypeSelector.tsx` | OPEN |
|
||||
| Localization | Hardcoded UI string: "Voice Room" | `src/app/components/create-room/CreateRoomTypeSelector.tsx` | OPEN |
|
||||
| Localization | Hardcoded UI string: "Live audio and video conversations." | `src/app/components/create-room/CreateRoomTypeSelector.tsx` | OPEN |
|
||||
| Localization | Hardcoded UI string: "Download" | `src/app/components/image-viewer/ImageViewer.tsx` | OPEN |
|
||||
| Localization | Hardcoded UI string: "Open Location" | `src/app/components/message/MsgTypeRenderers.tsx` | OPEN |
|
||||
| Localization | Hardcoded UI string: "Thread" | `src/app/components/message/Reply.tsx` | OPEN |
|
||||
| Localization | Hardcoded UI string: "View" | `src/app/components/message/content/ImageContent.tsx` | OPEN |
|
||||
| Localization | Hardcoded UI string: "Spoiler" | `src/app/components/message/content/ImageContent.tsx` | OPEN |
|
||||
| Localization | Hardcoded UI string: "Retry" | `src/app/components/message/content/ImageContent.tsx` | OPEN |
|
||||
| Localization | Hardcoded UI string: "Close" | `src/app/components/DeviceVerification.tsx` | OPEN |
|
||||
| Localization | Hardcoded UI string: "Accept" | `src/app/components/DeviceVerification.tsx` | OPEN |
|
||||
| Localization | Hardcoded UI string: "They Match" | `src/app/components/DeviceVerification.tsx` | OPEN |
|
||||
| Localization | Hardcoded UI string: "Okay" | `src/app/components/DeviceVerification.tsx` | OPEN |
|
||||
| Localization | Hardcoded UI string: "Join Server" | `src/app/components/url-preview/UrlPreviewCard.tsx` | OPEN |
|
||||
| Localization | Hardcoded UI string: "Invite" | `src/app/components/invite-user-prompt/InviteUserPrompt.tsx` | OPEN |
|
||||
| Localization | Hardcoded UI string: "Files" | `src/app/components/upload-board/UploadBoard.tsx` | OPEN |
|
||||
| Localization | Hardcoded UI string: "Send" | `src/app/components/upload-board/UploadBoard.tsx` | OPEN |
|
||||
| Localization | Hardcoded UI string: "Upload Failed" | `src/app/components/upload-board/UploadBoard.tsx` | OPEN |
|
||||
| Localization | Hardcoded UI string: "Password" | `src/app/components/uia-stages/PasswordStage.tsx` | OPEN |
|
||||
| Bundle Size | Large unoptimized media asset (213KB) | `public/res/Lotus.png` | OPEN |
|
||||
| Matrix Logic | Inefficient repeated `mx.getRoom()` calls in component render loops | `src/app/features/lobby/Lobby.tsx` | OPEN |
|
||||
| Matrix Logic | Inefficient repeated `mx.getRoom()` calls in component render loops | `src/app/components/emoji-board/EmojiBoard.tsx` | OPEN |
|
||||
| Performance | Numerous event handlers (e.g., handleUserClick, handleReplyClick) lack `useCallback`, leading to unnecessary re-renders of message components. | `cinny/src/app/features/room/RoomTimeline.tsx` | OPEN |
|
||||
| Performance | The `submit` function and file handling callbacks (e.g., handleSendUpload) are re-created on every render, causing re-renders of the editor and toolbar components. | `cinny/src/app/features/room/RoomInput.tsx` | OPEN |
|
||||
| Accessibility | `button` for edit history lacks `aria-label` | `cinny/src/app/components/message/content/FallbackContent.tsx` | OPEN |
|
||||
| Accessibility | `button` for reaction lacks `aria-label` | `cinny/src/app/components/message/Reaction.tsx` | OPEN |
|
||||
| Accessibility | `button` for ThreadIndicator lacks `aria-label` | `cinny/src/app/components/message/Reply.tsx` | OPEN |
|
||||
| Accessibility | `button` for ReplyLayout lacks `aria-label` | `cinny/src/app/components/message/Reply.tsx` | OPEN |
|
||||
|
||||
---
|
||||
|
||||
## 🔧 Infrastructure, DevEx & Type Safety
|
||||
|
||||
| Category | Issue Description | File Path | Status |
|
||||
| :--- | :--- | :--- | :--- |
|
||||
| Dependencies | `lodash` pinned to non-existent version `4.18.1` | `cinny/package.json` | OPEN |
|
||||
| Dependencies | Various pinned versions of `@atlaskit`, `matrix-js-sdk` | `cinny/package.json` | OPEN |
|
||||
| Dependencies | `matrix-js-sdk` pinned to Release Candidate (`41.6.0-rc.0`) | `cinny/package.json` | OPEN |
|
||||
| Dependencies | Unstable/experimental versions for build tools (`vite` 8.0.14, `typescript` 6.0.3, `eslint` 9.39.4) | `cinny/package.json` | OPEN |
|
||||
| CI/CD | `package-manager-cache` set to `false` | `cinny/.github/workflows/build-pull-request.yml` | OPEN |
|
||||
| CI/CD | Inefficient sequential execution in deployment | `cinny/.github/workflows/prod-deploy.yml` | OPEN |
|
||||
| CI/CD | Aggressive 1-minute timeout for Netlify deploy | `cinny/.github/workflows/prod-deploy.yml` | OPEN |
|
||||
| DevEx | Stale upstream bug tracker link/donations/CLA | `cinny/CONTRIBUTING.md` | OPEN |
|
||||
| DevEx | Alignment issue between README and CONTRIBUTING | `cinny/README.md` | OPEN |
|
||||
| Testing | No evident automated testing configuration/files | `cinny/src/` | OPEN |
|
||||
| Type Safety | Extensive use of `as any` type assertions | `cinny/src/` | OPEN |
|
||||
| Security | Hardcoded public CDN URL; consider moving to environment variable | /root/code/cinny/scripts/syncDecorations.mjs | OPEN |
|
||||
| Architecture | Modifying node_modules directly is brittle; use patch-package instead | /root/code/cinny/scripts/patch-folds.mjs | OPEN |
|
||||
| Robustness | Missing security headers (HSTS, CSP, etc.) and inefficient asset serving using rewrites instead of try_files | /root/code/cinny/contrib/nginx/cinny.domain.tld.conf | OPEN |
|
||||
| Robustness | Incomplete documentation/placeholder path in Caddyfile | /root/code/cinny/contrib/caddy/caddyfile | OPEN |
|
||||
| Matrix SDK | Inefficient listener management (`setMaxListeners: 150`) and incomplete SDK state transition handling. | `src/client/initMatrix.ts` | OPEN |
|
||||
| PWA Robustness | Service worker lacks caching strategy for application assets, resulting in no offline capability. | `cinny/src/sw.ts` | OPEN |
|
||||
| PWA Integrity | `manifest: false` in `vite.config.js` might prevent correct PWA installation if not handled externally. | `cinny/vite.config.js` | OPEN |
|
||||
| PII Leakage | Potential PII exposure via console.error (parameter e likely contains event data). | `cinny/src/app/plugins/call/CallEmbed.ts` | OPEN |
|
||||
| PII Leakage | Potential PII exposure via console.warn (parameter imgError/videoError/thumbError object). | `cinny/src/app/features/room/msgContent.ts` | OPEN |
|
||||
| PII Leakage | Potential PII exposure via console.error (parameter e likely contains event data). | `cinny/src/app/features/room/RoomInput.tsx` | OPEN |
|
||||
|
||||
## 🏗️ Architectural & Resilience Audit
|
||||
|
||||
| Category | Issue Description | File Path | Status |
|
||||
| :--- | :--- | :--- | :--- |
|
||||
| Element Call Integration | Lacks robust iframe failure monitoring beyond initial 'preparing' event; can result in a permanently hung 'Loading...' state with no user-visible error or recovery path. | `src/app/plugins/call/CallEmbed.ts` | OPEN |
|
||||
| Component Resilience | `RoomTimeline` has no `ErrorBoundary` wrapper — a single malformed event crashing the renderer takes down the entire timeline with no fallback UI. | `src/app/features/room/RoomTimeline.tsx` | OPEN |
|
||||
| Component Resilience | `RoomInput` has no `ErrorBoundary` wrapper — a crash in the composer leaves users unable to send messages. | `src/app/features/room/RoomInput.tsx` | OPEN |
|
||||
| Fallback Logic | No explicit empty/error fallback for Matrix SDK data calls in `RoomTimeline`; relies purely on SDK internal error propagation, meaning silent failures show a blank timeline. | `src/app/features/room/RoomTimeline.tsx` | OPEN |
|
||||
| Dependency | Potential for complex dependency chains due to deep nesting in `src/app/features/` and `src/app/hooks/`. | `src/app/` | OPEN |
|
||||
| Hydration/Race Condition | The SyncState listener registered by useSyncState may miss the initial 'PREPARED' event if the client initializes synchronously from IndexedDB before the effect runs, leading to an infinite loading state. | `cinny/src/app/pages/client/ClientRoot.tsx` | OPEN |
|
||||
| Structure | High number of small, highly coupled utility hooks (`src/app/hooks/`) may obscure dependency graphs. | `src/app/hooks/` | OPEN |
|
||||
| Dead Code | Potential for unused CSS modules or UI components in `src/app/features/`. | `src/app/` | OPEN |
|
||||
| Security | Sensitive session data (access tokens, device ID) stored in `localStorage` is vulnerable to XSS. | `src/app/state/sessions.ts` | OPEN |
|
||||
| Privacy | Sensitive user status messages and expiry timestamps are persisted in `localStorage`. | `src/app/features/settings/account/Profile.tsx` | OPEN |
|
||||
| Privacy | Unsent composer drafts stored in `localStorage` without encryption could leak info on shared devices. | `src/app/features/room/RoomInput.tsx` | OPEN |
|
||||
| Persistence | Scheduled messages relying on fragile `localStorage` parsing are prone to data loss on session expiry or error. | `src/app/state/scheduledMessages.ts` | OPEN |
|
||||
| Bundle Bloat | Inefficient `lodash` import; risks including entire library instead of necessary utilities. | `cinny/package.json` | OPEN |
|
||||
| Bundle Bloat | Large `matrix-js-sdk` (RC version) dependency; high potential for tree-shaking overhead. | `cinny/package.json` | OPEN |
|
||||
| Build-Time Overhead | `lotusDenoise` plugin performs heavy, sequential `fs` operations during `closeBundle`, significantly slowing build times. | `cinny/vite.config.js` | OPEN |
|
||||
| Build-Time Overhead | Complex manual `viteStaticCopy` configuration requiring multiple renames and path manipulations; risks redundant processing. | `cinny/vite.config.js` | OPEN |
|
||||
| Architectural Debt | Redundant style variant logic in `SpacingVariant` could be simplified. | `cinny/src/app/components/message/layout/layout.css.ts` | OPEN |
|
||||
| Overhead Analysis | Potential CSS bloat from `DropTarget` composition across multiple recipes (`SidebarItem`, `SidebarFolder`). | `cinny/src/app/components/sidebar/Sidebar.css.ts` | OPEN |
|
||||
|
||||
## 🏗️ Git Workflow & History Audit
|
||||
|
||||
| Category | Issue Description | File Path | Status |
|
||||
| :--- | :--- | :--- | :--- |
|
||||
| Workflow | Monolithic "Fix all bugs" commits (e.g., `10f6544e`, `aa48c9ef`) make `git bisect` difficult. | Git History | OPEN |
|
||||
| Workflow | Inconsistent commit message prefixes (e.g., `fix`, `feat`, `docs`, `assets`). | Git History | OPEN |
|
||||
| Workflow | Use of `fix` or `feat` for large-scale changes affecting multiple disparate systems (e.g., `938ead79`). | Git History | OPEN |
|
||||
@@ -1,723 +0,0 @@
|
||||
# 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)
|
||||
|
||||
---
|
||||
|
||||
## 🏗️ Infrastructure & Maintenance
|
||||
|
||||
- [ ] **Upgrade Synapse to v1.155.0**
|
||||
- **Context:** Synapse 1.155.0 is the last version supporting Debian 12 Bookworm.
|
||||
- **Reference:** https://github.com/element-hq/synapse/releases/tag/v1.155.0
|
||||
- **Plan:** Review release notes, backup database and media store on LXC 151, perform upgrade in a staging environment if possible, then production. Prepare for OS migration to Debian 13 afterward.
|
||||
|
||||
---
|
||||
|
||||
## 📱 Quick Feature Additions
|
||||
|
||||
- [ ] **Full-Screen Camera Broadcasts**
|
||||
- **Context:** Element Call currently supports full-screening screenshares. We need to parity this functionality for camera broadcasts.
|
||||
- **Goal:** Users should be able to toggle any camera feed to full-screen mode, similar to the existing screenshare full-screen implementation.
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ 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.
|
||||
|
||||
---
|
||||
|
||||
Completed features are documented in [LOTUS_FEATURES.md](./LOTUS_FEATURES.md).
|
||||
|
||||
---
|
||||
|
||||
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.153.0` (2026-05-19) — fully up to date
|
||||
- **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` | All safe to use now |
|
||||
| **MSC flags OFF:** `msc4306` (thread subscriptions) · `msc3882` · `msc3912` · `msc4155` | These features are BLOCKED |
|
||||
| **MSC3266** room summary: returns 404 | Room Preview feature BLOCKED |
|
||||
| **MSC3892** relation redaction: not in flags | Reaction Redaction feature BLOCKED |
|
||||
| **MSC4260** report user: server at v1.12, endpoint may not exist | Report User feature BLOCKED |
|
||||
| **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-9 · Advanced Search Filter UI
|
||||
|
||||
**What:** Introduce a more robust search filter UI in `SearchFilters.tsx`.
|
||||
**Approach:** Add UI components for easier filtering and a visual date-range picker that correctly maps to `fromTs` and `toTs`.
|
||||
|
||||
---
|
||||
|
||||
### [ ] 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
|
||||
|
||||
### [ ] P5-1 · Custom Accent Color Picker (non-TDS mode only)
|
||||
|
||||
**What:** A hex/HSL color picker in Settings → Appearance. Chosen color replaces the primary accent throughout the UI: buttons, badges, active states, highlights, presence dot, links. Applied via a CSS custom property override injected into `<head>`.
|
||||
**IMPORTANT:** This feature is completely inactive when TDS is enabled — TDS has its own fixed palette. Add this setting under a "Non-TDS Themes" section that is hidden when TDS is active.
|
||||
**[AUDIT REQUIRED]** Identify all CSS custom properties that constitute the "accent color" in non-TDS mode. Map them to the folds/vanilla-extract token names. (Confirmed: folds uses vanilla-extract, NOT CSS custom properties — must create a new vanilla-extract theme variant dynamically.)
|
||||
**Complexity:** Medium.
|
||||
|
||||
---
|
||||
|
||||
### [ ] P5-2 · Additional Color Theme Presets
|
||||
|
||||
**What:** 5 new one-click theme presets alongside TDS. Each must be a complete, polished system with proper contrast ratios (WCAG AA). All implemented as vanilla-extract themes matching the existing TDS pattern.
|
||||
|
||||
Themes:
|
||||
1. **Cyberpunk** — deep navy bg (`#0a0015`), electric purple (`#bf5fff`) + hot pink (`#ff2d9b`) accents, neon glow
|
||||
2. **Ocean** — deep sea blue bg (`#020b18`), teal (`#00c9b1`) + aqua (`#0096d6`) accents, soft feel
|
||||
3. **Blood Red** — near-black bg (`#0d0203`), deep crimson (`#7a0010`) + bright red (`#ff2233`) accents
|
||||
4. **Classic Matrix** — pure black bg (`#000000`), phosphor green (`#00ff41`) text + accents
|
||||
5. **Midnight** — dark charcoal (`#111827`), cool blue-grey (`#6b7ca8`) accents, clean minimal
|
||||
|
||||
**[AUDIT REQUIRED]** Study `src/lotus-terminal.css.ts` for the full token list before designing themes. All tokens must be covered (~50 CSS custom properties each).
|
||||
**Complexity:** Medium (design effort is the main cost).
|
||||
|
||||
---
|
||||
|
||||
### [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-5 · Intersection-Based Lazy Loading
|
||||
|
||||
**What:** Use `IntersectionObserver` to trigger media decryption and loading only when components approach the viewport.
|
||||
**Approach:** Reduce initial memory footprint and improve timeline load times by deferring decryption of images/videos until they are visible.
|
||||
|
||||
### [ ] P5-6 · Context-Aware Thumbnail Previews
|
||||
|
||||
**What:** Enhance thumbnail rendering in the timeline for consistent, polished aesthetics.
|
||||
**Approach:** Use CSS `object-fit: cover` with improved focal-point centering within `ThumbnailContent` to prevent media stretching or awkward aspect-ratio cropping.
|
||||
|
||||
---
|
||||
|
||||
### [ ] 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.
|
||||
**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.
|
||||
|
||||
---
|
||||
|
||||
### [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)".
|
||||
**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-40 · Desktop — Proactive Update Notifications (Tauri)
|
||||
|
||||
**What:** Automatically check for app updates on launch and periodically during long sessions. If an update is available, show an in-app toast or badge (e.g., on the Settings icon) to alert the user without requiring a manual check in settings.
|
||||
**Mechanism:** Use the `useTauriUpdater` hook in a global component like `ClientNonUIFeatures.tsx`.
|
||||
**Note:** Ensure the check is throttled (e.g., once every 12 hours) to avoid redundant Tauri commands.
|
||||
**Complexity:** Low-Medium.
|
||||
|
||||
---
|
||||
|
||||
### [ ] 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.
|
||||
- [ ] **Remind Me Later:** Slack-style reminders for messages (ping user at a set time, add to bookmarks).
|
||||
- **Root Cause:** Feature does not exist. Reminders require persistence in Matrix account data and a background worker to trigger notifications.
|
||||
- **Approach:** Expand `useReminders.ts` to include `removeReminder`. Add a "Remind me" menu item to `Message.tsx` that triggers a `DatePicker`/`TimePicker` modal (reusing logic from `JumpToTime.tsx`). Implement a `ReminderMonitor` in `ClientNonUIFeatures.tsx` that polls active reminders from `io.lotus.reminders` and pushes to `toastQueueAtom` in `state/toast.ts` when due.
|
||||
- **Complexity:** Medium.
|
||||
- [ ] **Mobile Bookmarks:** Fix visibility issue where bookmarks do not show up when selected via PWA on mobile.
|
||||
- **Root Cause:** `ClientLayout.tsx` explicitly restricts `BookmarksPanel` rendering to `ScreenSize.Desktop` (lines 51-56).
|
||||
- **Approach:** Update `ClientLayout.tsx` to remove the `ScreenSize.Desktop` restriction. Pass `isMobile={screenSize !== ScreenSize.Desktop}` to `BookmarksPanel`. `BookmarksPanel.tsx` already supports this prop (line 127) to enable full-screen absolute positioning and reactive layout.
|
||||
- **Complexity:** Low.
|
||||
|
||||
---
|
||||
|
||||
## 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 /v1/rooms/{id}/summary` returns 404 — endpoint not available on this server
|
||||
**What it would do:** Show room name, topic, avatar, member count before joining.
|
||||
**Action when unblocked:** Build pre-join preview card; trigger on unjoined room navigation.
|
||||
|
||||
### [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).
|
||||
|
||||
### [BLOCKED] · Report User (MSC4260)
|
||||
|
||||
**Blocked by:** Server declares only spec v1.12; MSC4260 merged in v1.14 — endpoint may not exist
|
||||
**What it would do:** Report a specific user to homeserver admins (separate from reporting a message).
|
||||
**Note:** Report Message already exists in upstream Cinny. This would add Report User to the profile panel.
|
||||
**Action when unblocked:** Test `POST /_matrix/client/v3/users/{userId}/report`; if 200, add button to user profile.
|
||||
|
||||
---
|
||||
|
||||
## 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.
|
||||
- **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.
|
||||
|
||||
---
|
||||
|
||||
### P5-20 · Quick Reply from Browser Notification
|
||||
|
||||
**Mechanism:** Service Worker `notificationclick` Action.
|
||||
|
||||
```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,163 +1,115 @@
|
||||
# Lotus Chat
|
||||
# Cinny
|
||||
<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 chat client built for Lotus Guild — fast, private, and packed with the features you actually want.
|
||||
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.
|
||||
- [Roadmap](https://github.com/orgs/cinnyapp/projects/1)
|
||||
- [Contributing](./CONTRIBUTING.md)
|
||||
|
||||
**Deployed at [chat.lotusguild.org](https://chat.lotusguild.org)** | Forked from [Cinny](https://github.com/cinnyapp/cinny) v4.12.1
|
||||
> [!IMPORTANT]
|
||||
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">
|
||||
|
||||
## Licensing & Attribution
|
||||
## Getting started
|
||||
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.
|
||||
|
||||
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).
|
||||
You can also download our desktop app from the [cinny-desktop repository](https://github.com/cinnyapp/cinny-desktop).
|
||||
|
||||
The Lotus Chat logo (`lotus_chat.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.
|
||||
## Self-hosting
|
||||
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).
|
||||
|
||||
## Features
|
||||
* 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).
|
||||
* 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.
|
||||
|
||||
### Messaging
|
||||
* To deploy on subdirectory, you need to rebuild the app youself after updating the `base` path in [`build.config.ts`](build.config.ts).
|
||||
* For example, if you want to deploy on `https://cinny.in/app`, then set `base: '/app'`.
|
||||
|
||||
- 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
|
||||
|
||||
- Push to Talk with a configurable keybind (default: Space)
|
||||
- Push to Deafen with the M key
|
||||
- Camera starts turned off by default when joining a call
|
||||
- Screenshare requires confirmation before going live
|
||||
- Toggle noise suppression on or off
|
||||
- Calls float in a draggable picture-in-picture window when you navigate away
|
||||
- Your chat background shows through the call view
|
||||
- Dark/light mode inside calls matches your Lotus Chat theme
|
||||
- Calls are available in DMs and private groups only — no accidental mass rings
|
||||
- AFK auto-mute: mic is automatically silenced after a configurable idle timeout (1–30 min); a toast confirms the action
|
||||
- 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
|
||||
- Custom join/leave sound effects when someone enters or leaves your call — choose Chime, Soft, Retro, or off
|
||||
|
||||
### Customization & Appearance
|
||||
|
||||
- LotusGuild Terminal Design System (TDS) — a CRT terminal-inspired dark theme
|
||||
- TDS light mode variant for daytime use
|
||||
- 20+ static chat background patterns
|
||||
- 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.
|
||||
|
||||
### Build
|
||||
|
||||
```bash
|
||||
npm ci && npm run build # outputs to dist/
|
||||
```
|
||||
|
||||
If the build is killed due to out-of-memory:
|
||||
|
||||
```bash
|
||||
NODE_OPTIONS=--max_old_space_size=6144 npm run build
|
||||
```
|
||||
|
||||
### CI/CD
|
||||
<details><summary><b>PGP Public Key to verify tarball</b></summary>
|
||||
|
||||
```
|
||||
edit → commit → git push → ~11 min → live at chat.lotusguild.org
|
||||
-----BEGIN PGP PUBLIC KEY BLOCK-----
|
||||
|
||||
mQGNBGJw/g0BDAC8qQeLqDMzYzfPyOmRlHVEoguVTo+eo1aVdQH2X7OELdjjBlyj
|
||||
6d6c1adv/uF2g83NNMoQY7GEeHjRnXE4m8kYSaarb840pxrYUagDc0dAbJOGaCBY
|
||||
FKTo7U1Kvg0vdiaRuus0pvc1NVdXSxRNQbFXBSwduD+zn66TI3HfcEHNN62FG1cE
|
||||
K1jWDwLAU0P3kKmj8+CAc3h9ZklPu0k/+t5bf/LJkvdBJAUzGZpehbPL5f3u3BZ0
|
||||
leZLIrR8uV7PiV5jKFahxlKR5KQHld8qQm+qVhYbUzpuMBGmh419I6UvTzxuRcvU
|
||||
Frn9ttCEzV55Y+so4X2e4ZnB+5gOnNw+ecifGVdj/+UyWnqvqqDvLrEjjK890nLb
|
||||
Pil4siecNMEpiwAN6WSmKpWaCwQAHEGDVeZCc/kT0iYfj5FBcsTVqWiO6eaxkUlm
|
||||
jnulqWqRrlB8CJQQvih/g//uSEBdzIibo+ro+3Jpe120U/XVUH62i9HoRQEm6ADG
|
||||
4zS5hIq4xyA8fL8AEQEAAbQdQ2lubnlBcHAgPGNpbm55YXBwQGdtYWlsLmNvbT6J
|
||||
AdQEEwEIAD4CGwMFCwkIBwIGFQoJCAsCBBYCAwECHgECF4AWIQSRri2MHidaaZv+
|
||||
vvuUMwx6UK/M8wUCZqEDwAUJFvwIswAKCRCUMwx6UK/M877qC/4lxXOQIoWnLLkK
|
||||
YiRCTkGsH6NdxgeYr6wpXT4xuQ45ZxCytwHpOGQmO/5up5961TxWW8D1frRIJHjj
|
||||
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
|
||||
> [!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:
|
||||
```sh
|
||||
npm ci # Installs all dependencies
|
||||
npm start # Serve a development version
|
||||
```
|
||||
|
||||
To build the app:
|
||||
```sh
|
||||
npm run build # Compiles the app into the dist/ directory
|
||||
```
|
||||
|
||||
### Running with Docker
|
||||
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
|
||||
```
|
||||
|
||||
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`.
|
||||
|
||||
@@ -1,340 +0,0 @@
|
||||
/*
|
||||
* 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;
|
||||
}
|
||||
|
||||
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 {
|
||||
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,
|
||||
},
|
||||
'*',
|
||||
);
|
||||
}
|
||||
|
||||
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 }, '*');
|
||||
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,16 +1,32 @@
|
||||
{
|
||||
"defaultHomeserver": 0,
|
||||
"homeserverList": ["matrix.lotusguild.org", "matrix.org", "mozilla.org"],
|
||||
"defaultHomeserver": 1,
|
||||
"homeserverList": ["converser.eu", "matrix.org", "mozilla.org", "unredacted.org", "xmr.se"],
|
||||
"allowCustomHomeservers": true,
|
||||
|
||||
"featuredCommunities": {
|
||||
"openAsDefault": false,
|
||||
"spaces": [],
|
||||
"rooms": [],
|
||||
"servers": []
|
||||
"spaces": [
|
||||
"#cinny-space:matrix.org",
|
||||
"#community:matrix.org",
|
||||
"#space:unredacted.org",
|
||||
"#science-space:matrix.org",
|
||||
"#libregaming-games:tchncs.de",
|
||||
"#mathematics-on:matrix.org",
|
||||
"#stickers-and-emojis:tastytea.de"
|
||||
],
|
||||
"rooms": [
|
||||
"#cinny:matrix.org",
|
||||
"#freesoftware:matrix.org",
|
||||
"#pcapdroid:matrix.org",
|
||||
"#gentoo:matrix.org",
|
||||
"#PrivSec.dev:arcticfoxes.net",
|
||||
"#disroot:aria-net.org"
|
||||
],
|
||||
"servers": ["matrixrooms.info", "matrix.org", "mozilla.org", "unredacted.org"]
|
||||
},
|
||||
|
||||
"hashRouter": {
|
||||
"enabled": false,
|
||||
"basename": "/"
|
||||
},
|
||||
"gifApiKey": ""
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,126 +0,0 @@
|
||||
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,42 +1,36 @@
|
||||
<!doctype html>
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Lotus Chat</title>
|
||||
<meta name="name" content="Lotus Chat" />
|
||||
<meta name="author" content="Lotus Guild" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0" />
|
||||
<title>Cinny</title>
|
||||
<meta name="name" content="Cinny" />
|
||||
<meta name="author" content="Ajay Bura" />
|
||||
<meta
|
||||
name="description"
|
||||
content="Lotus Chat — the Lotus Guild Matrix client. Secure, fast, and built for our community."
|
||||
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."
|
||||
/>
|
||||
<meta name="keywords" content="lotus chat, lotus guild, matrix, matrix client" />
|
||||
|
||||
<meta property="og:type" content="website" />
|
||||
<meta property="og:title" content="Lotus Chat" />
|
||||
<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"
|
||||
name="keywords"
|
||||
content="cinny, cinnyapp, cinnychat, matrix, matrix client, matrix.org, element"
|
||||
/>
|
||||
|
||||
<meta property="og:title" content="Cinny" />
|
||||
<meta property="og:url" content="https://cinny.in" />
|
||||
<meta property="og:image" content="https://cinny.in/assets/favicon-48x48.png" />
|
||||
<meta
|
||||
property="og:description"
|
||||
content="Lotus Chat — the Lotus Guild Matrix client. Secure, fast, and built for our community."
|
||||
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."
|
||||
/>
|
||||
<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 rel="manifest" href="/manifest.json" />
|
||||
<meta name="mobile-web-app-capable" content="yes" />
|
||||
<meta name="application-name" content="Lotus Chat" />
|
||||
<meta name="apple-mobile-web-app-title" content="Lotus Chat" />
|
||||
<meta name="application-name" content="Cinny" />
|
||||
<meta name="apple-mobile-web-app-title" content="Cinny" />
|
||||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
|
||||
|
||||
|
||||
|
Before Width: | Height: | Size: 851 KiB |
|
Before Width: | Height: | Size: 944 KiB |
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "lotus-chat",
|
||||
"version": "4.12.2-lotus",
|
||||
"description": "Lotus Chat — Matrix client for Lotus Guild",
|
||||
"name": "cinny",
|
||||
"version": "4.11.1",
|
||||
"description": "Yet another matrix client",
|
||||
"main": "index.js",
|
||||
"type": "module",
|
||||
"engines": {
|
||||
@@ -16,10 +16,9 @@
|
||||
"check:prettier": "prettier --check .",
|
||||
"fix:prettier": "prettier --write .",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"prepare": "husky",
|
||||
"prepare": "husky install",
|
||||
"commit": "git-cz",
|
||||
"postinstall": "node scripts/patch-folds.mjs",
|
||||
"sync:decorations": "node scripts/syncDecorations.mjs"
|
||||
"semantic-release": "semantic-release"
|
||||
},
|
||||
"lint-staged": {
|
||||
"*.{ts,tsx,js,jsx}": "eslint",
|
||||
@@ -30,120 +29,132 @@
|
||||
"path": "./node_modules/cz-conventional-changelog"
|
||||
}
|
||||
},
|
||||
"release": {
|
||||
"branches": [
|
||||
"dev"
|
||||
],
|
||||
"plugins": [
|
||||
"@semantic-release/commit-analyzer",
|
||||
"@semantic-release/release-notes-generator",
|
||||
[
|
||||
"@semantic-release/exec",
|
||||
{
|
||||
"prepareCmd": "node scripts/update-version.js ${nextRelease.version}"
|
||||
}
|
||||
],
|
||||
[
|
||||
"@semantic-release/git",
|
||||
{
|
||||
"assets": [
|
||||
"package.json",
|
||||
"package-lock.json",
|
||||
"src/app/features/settings/about/About.tsx",
|
||||
"src/app/pages/auth/AuthFooter.tsx",
|
||||
"src/app/pages/client/WelcomePage.tsx"
|
||||
],
|
||||
"message": "chore(release): ${nextRelease.version} [skip ci]"
|
||||
}
|
||||
],
|
||||
"@semantic-release/github"
|
||||
]
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "Ajay Bura",
|
||||
"license": "AGPL-3.0-only",
|
||||
"dependencies": {
|
||||
"@atlaskit/pragmatic-drag-and-drop": "1.8.1",
|
||||
"@atlaskit/pragmatic-drag-and-drop-auto-scroll": "2.1.5",
|
||||
"@atlaskit/pragmatic-drag-and-drop-hitbox": "1.1.0",
|
||||
"@eslint/eslintrc": "3.3.5",
|
||||
"@eslint/js": "10.0.1",
|
||||
"@fontsource-variable/inter": "5.2.8",
|
||||
"@giphy/js-fetch-api": "5.8.0",
|
||||
"@giphy/js-types": "5.1.0",
|
||||
"@giphy/js-util": "5.2.0",
|
||||
"@giphy/react-components": "10.1.2",
|
||||
"@sapphi-red/web-noise-suppressor": "0.3.5",
|
||||
"@sentry/react": "10.53.1",
|
||||
"@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",
|
||||
"@atlaskit/pragmatic-drag-and-drop": "1.1.6",
|
||||
"@atlaskit/pragmatic-drag-and-drop-auto-scroll": "1.3.0",
|
||||
"@atlaskit/pragmatic-drag-and-drop-hitbox": "1.0.3",
|
||||
"@fontsource/inter": "4.5.14",
|
||||
"@tanstack/react-query": "5.24.1",
|
||||
"@tanstack/react-query-devtools": "5.24.1",
|
||||
"@tanstack/react-virtual": "3.2.0",
|
||||
"@vanilla-extract/css": "1.9.3",
|
||||
"@vanilla-extract/recipes": "0.3.0",
|
||||
"@vanilla-extract/vite-plugin": "3.7.1",
|
||||
"await-to-js": "3.0.0",
|
||||
"badwords-list": "2.0.1-4",
|
||||
"blurhash": "2.0.5",
|
||||
"blurhash": "2.0.4",
|
||||
"browser-encrypt-attachment": "0.3.0",
|
||||
"chroma-js": "3.2.0",
|
||||
"classnames": "2.5.1",
|
||||
"chroma-js": "3.1.2",
|
||||
"classnames": "2.3.2",
|
||||
"dateformat": "5.0.3",
|
||||
"dayjs": "1.11.20",
|
||||
"deepfilternet3-noise-filter": "1.2.1",
|
||||
"domhandler": "6.0.1",
|
||||
"dompurify": "3.4.5",
|
||||
"emojibase": "17.0.0",
|
||||
"emojibase-data": "17.0.0",
|
||||
"dayjs": "1.11.10",
|
||||
"domhandler": "5.0.3",
|
||||
"emojibase": "15.3.1",
|
||||
"emojibase-data": "15.3.2",
|
||||
"file-saver": "2.0.5",
|
||||
"focus-trap-react": "12.0.2",
|
||||
"focus-trap-react": "10.0.2",
|
||||
"folds": "2.6.2",
|
||||
"globals": "17.6.0",
|
||||
"html-dom-parser": "7.1.0",
|
||||
"html-react-parser": "6.1.2",
|
||||
"i18next": "26.2.0",
|
||||
"i18next-browser-languagedetector": "8.2.1",
|
||||
"i18next-http-backend": "4.0.0",
|
||||
"immer": "11.1.8",
|
||||
"html-dom-parser": "4.0.0",
|
||||
"html-react-parser": "4.2.0",
|
||||
"i18next": "23.12.2",
|
||||
"i18next-browser-languagedetector": "8.0.0",
|
||||
"i18next-http-backend": "2.5.2",
|
||||
"immer": "9.0.16",
|
||||
"is-hotkey": "0.2.0",
|
||||
"jotai": "2.20.0",
|
||||
"linkify-react": "4.3.3",
|
||||
"linkifyjs": "4.3.3",
|
||||
"lodash": "4.18.1",
|
||||
"matrix-js-sdk": "41.6.0-rc.0",
|
||||
"matrix-widget-api": "1.17.0",
|
||||
"jotai": "2.6.0",
|
||||
"linkify-react": "4.3.2",
|
||||
"linkifyjs": "4.3.2",
|
||||
"matrix-js-sdk": "38.2.0",
|
||||
"matrix-widget-api": "1.13.0",
|
||||
"millify": "6.1.0",
|
||||
"pdfjs-dist": "5.7.284",
|
||||
"pdfjs-dist": "4.2.67",
|
||||
"prismjs": "1.30.0",
|
||||
"react": "19.2.6",
|
||||
"react-aria": "3.48.0",
|
||||
"react-blurhash": "0.3.0",
|
||||
"react-colorful": "5.7.0",
|
||||
"react-dom": "19.2.6",
|
||||
"react-error-boundary": "6.1.1",
|
||||
"react-google-recaptcha": "3.1.0",
|
||||
"react-i18next": "17.0.8",
|
||||
"react-range": "1.10.0",
|
||||
"react-router-dom": "7.15.1",
|
||||
"sanitize-html": "2.17.4",
|
||||
"slate": "0.124.1",
|
||||
"slate-dom": "0.124.1",
|
||||
"react": "18.2.0",
|
||||
"react-aria": "3.29.1",
|
||||
"react-blurhash": "0.2.0",
|
||||
"react-colorful": "5.6.1",
|
||||
"react-dom": "18.2.0",
|
||||
"react-error-boundary": "4.0.13",
|
||||
"react-google-recaptcha": "2.1.0",
|
||||
"react-i18next": "15.0.0",
|
||||
"react-range": "1.8.14",
|
||||
"react-router-dom": "6.30.3",
|
||||
"sanitize-html": "2.12.1",
|
||||
"slate": "0.123.0",
|
||||
"slate-dom": "0.123.0",
|
||||
"slate-history": "0.113.1",
|
||||
"slate-react": "0.124.2",
|
||||
"styled-components": "6.4.2",
|
||||
"ua-parser-js": "2.0.10"
|
||||
"slate-react": "0.123.0",
|
||||
"ua-parser-js": "1.0.35"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@element-hq/element-call-embedded": "0.19.4",
|
||||
"@rollup/plugin-inject": "5.0.5",
|
||||
"@rollup/plugin-wasm": "6.2.2",
|
||||
"@sentry/vite-plugin": "5.3.0",
|
||||
"@types/chroma-js": "3.1.2",
|
||||
"@types/file-saver": "2.0.7",
|
||||
"@element-hq/element-call-embedded": "0.16.3",
|
||||
"@esbuild-plugins/node-globals-polyfill": "0.2.3",
|
||||
"@rollup/plugin-inject": "5.0.3",
|
||||
"@rollup/plugin-wasm": "6.1.1",
|
||||
"@semantic-release/exec": "7.1.0",
|
||||
"@semantic-release/git": "10.0.1",
|
||||
"@types/chroma-js": "3.1.1",
|
||||
"@types/file-saver": "2.0.5",
|
||||
"@types/is-hotkey": "0.1.10",
|
||||
"@types/node": "25.9.1",
|
||||
"@types/prismjs": "1.26.6",
|
||||
"@types/react": "19.2.15",
|
||||
"@types/react-dom": "19.2.3",
|
||||
"@types/react-google-recaptcha": "2.1.9",
|
||||
"@types/sanitize-html": "2.16.1",
|
||||
"@types/ua-parser-js": "0.7.39",
|
||||
"@typescript-eslint/eslint-plugin": "8.59.4",
|
||||
"@typescript-eslint/parser": "8.59.4",
|
||||
"@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",
|
||||
"@types/node": "18.11.18",
|
||||
"@types/prismjs": "1.26.0",
|
||||
"@types/react": "18.2.39",
|
||||
"@types/react-dom": "18.2.17",
|
||||
"@types/react-google-recaptcha": "2.1.8",
|
||||
"@types/sanitize-html": "2.9.0",
|
||||
"@types/ua-parser-js": "0.7.36",
|
||||
"@typescript-eslint/eslint-plugin": "5.46.1",
|
||||
"@typescript-eslint/parser": "5.46.1",
|
||||
"@vitejs/plugin-react": "4.2.0",
|
||||
"buffer": "6.0.3",
|
||||
"cz-conventional-changelog": "3.3.0",
|
||||
"eslint": "9.39.4",
|
||||
"eslint": "8.29.0",
|
||||
"eslint-config-airbnb": "19.0.4",
|
||||
"eslint-config-prettier": "10.1.8",
|
||||
"eslint-plugin-import": "2.32.0",
|
||||
"eslint-plugin-jsx-a11y": "6.10.2",
|
||||
"eslint-plugin-react": "7.37.5",
|
||||
"eslint-plugin-react-hooks": "7.1.1",
|
||||
"eslint-config-prettier": "8.5.0",
|
||||
"eslint-plugin-import": "2.29.1",
|
||||
"eslint-plugin-jsx-a11y": "6.6.1",
|
||||
"eslint-plugin-react": "7.31.11",
|
||||
"eslint-plugin-react-hooks": "4.6.0",
|
||||
"husky": "9.1.7",
|
||||
"lint-staged": "17.0.5",
|
||||
"prettier": "3.8.3",
|
||||
"typescript": "6.0.3",
|
||||
"vite": "8.0.14",
|
||||
"vite-plugin-pwa": "1.3.0",
|
||||
"vite-plugin-static-copy": "4.1.0"
|
||||
},
|
||||
"overrides": {
|
||||
"@giphy/js-util": {
|
||||
"dompurify": ">=3.3.4"
|
||||
},
|
||||
"js-cookie": ">=3.0.6"
|
||||
"lint-staged": "16.3.2",
|
||||
"prettier": "2.8.1",
|
||||
"semantic-release": "25.0.3",
|
||||
"typescript": "4.9.4",
|
||||
"vite": "5.4.19",
|
||||
"vite-plugin-pwa": "0.20.5",
|
||||
"vite-plugin-static-copy": "1.0.4",
|
||||
"vite-plugin-top-level-await": "1.4.4"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
{
|
||||
"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: 128 KiB After Width: | Height: | Size: 32 KiB |
@@ -1,11 +0,0 @@
|
||||
<!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>
|
||||
@@ -1,11 +0,0 @@
|
||||
<!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>
|
||||
@@ -1,11 +0,0 @@
|
||||
<!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>
|
||||
@@ -1,51 +0,0 @@
|
||||
/* 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;
|
||||
}
|
||||
@@ -1,74 +1,59 @@
|
||||
{
|
||||
"name": "Lotus Chat",
|
||||
"short_name": "Lotus Chat",
|
||||
"description": "Lotus Chat \u2014 the Lotus Guild Matrix client",
|
||||
"name": "Cinny",
|
||||
"short_name": "Cinny",
|
||||
"description": "Yet another matrix client",
|
||||
"dir": "auto",
|
||||
"lang": "en-US",
|
||||
"display": "standalone",
|
||||
"orientation": "portrait",
|
||||
"start_url": "./",
|
||||
"background_color": "#0a0a0a",
|
||||
"theme_color": "#980000",
|
||||
"background_color": "#fff",
|
||||
"theme_color": "#fff",
|
||||
"icons": [
|
||||
{
|
||||
"src": "./res/android/android-chrome-36x36.png",
|
||||
"src": "./public/android/android-chrome-36x36.png",
|
||||
"sizes": "36x36",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "./res/android/android-chrome-48x48.png",
|
||||
"src": "./public/android/android-chrome-48x48.png",
|
||||
"sizes": "48x48",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "./res/android/android-chrome-72x72.png",
|
||||
"src": "./public/android/android-chrome-72x72.png",
|
||||
"sizes": "72x72",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "./res/android/android-chrome-96x96.png",
|
||||
"src": "./public/android/android-chrome-96x96.png",
|
||||
"sizes": "96x96",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "./res/android/android-chrome-144x144.png",
|
||||
"src": "./public/android/android-chrome-144x144.png",
|
||||
"sizes": "144x144",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "./res/android/android-chrome-192x192.png",
|
||||
"src": "./public/android/android-chrome-192x192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "./res/android/android-chrome-256x256.png",
|
||||
"src": "./public/android/android-chrome-256x256.png",
|
||||
"sizes": "256x256",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "./res/android/android-chrome-384x384.png",
|
||||
"src": "./public/android/android-chrome-384x384.png",
|
||||
"sizes": "384x384",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "./res/android/android-chrome-512x512.png",
|
||||
"src": "./public/android/android-chrome-512x512.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png"
|
||||
}
|
||||
],
|
||||
"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"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
|
Before Width: | Height: | Size: 208 KiB |
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 4.4 KiB |
|
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 6.1 KiB |
|
Before Width: | Height: | Size: 40 KiB After Width: | Height: | Size: 8.2 KiB |
|
Before Width: | Height: | Size: 2.0 KiB After Width: | Height: | Size: 1.0 KiB |
|
Before Width: | Height: | Size: 79 KiB After Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 3.1 KiB After Width: | Height: | Size: 1.5 KiB |
|
Before Width: | Height: | Size: 134 KiB After Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 5.8 KiB After Width: | Height: | Size: 2.2 KiB |
|
Before Width: | Height: | Size: 8.9 KiB After Width: | Height: | Size: 2.9 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 4.8 KiB |
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 5.0 KiB |
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 5.9 KiB |
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 6.5 KiB |
|
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 7.2 KiB |
|
Before Width: | Height: | Size: 23 KiB After Width: | Height: | Size: 7.7 KiB |
|
Before Width: | Height: | Size: 4.0 KiB After Width: | Height: | Size: 2.3 KiB |
|
Before Width: | Height: | Size: 4.3 KiB After Width: | Height: | Size: 2.4 KiB |
|
Before Width: | Height: | Size: 5.8 KiB After Width: | Height: | Size: 2.9 KiB |
|
Before Width: | Height: | Size: 6.2 KiB After Width: | Height: | Size: 3.1 KiB |
|
Before Width: | Height: | Size: 2.0 KiB |
|
Before Width: | Height: | Size: 2.0 KiB |
|
Before Width: | Height: | Size: 2.0 KiB |
@@ -1,14 +1,13 @@
|
||||
<svg viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g fill="#980000" fill-opacity="0.88">
|
||||
<path d="M9,9 Q6.4,4.8 9,1.2 Q11.6,4.8 9,9Z" transform="rotate(0,9,9)"/>
|
||||
<path d="M9,9 Q6.4,4.8 9,1.2 Q11.6,4.8 9,9Z" transform="rotate(45,9,9)"/>
|
||||
<path d="M9,9 Q6.4,4.8 9,1.2 Q11.6,4.8 9,9Z" transform="rotate(90,9,9)"/>
|
||||
<path d="M9,9 Q6.4,4.8 9,1.2 Q11.6,4.8 9,9Z" transform="rotate(135,9,9)"/>
|
||||
<path d="M9,9 Q6.4,4.8 9,1.2 Q11.6,4.8 9,9Z" transform="rotate(180,9,9)"/>
|
||||
<path d="M9,9 Q6.4,4.8 9,1.2 Q11.6,4.8 9,9Z" transform="rotate(225,9,9)"/>
|
||||
<path d="M9,9 Q6.4,4.8 9,1.2 Q11.6,4.8 9,9Z" transform="rotate(270,9,9)"/>
|
||||
<path d="M9,9 Q6.4,4.8 9,1.2 Q11.6,4.8 9,9Z" transform="rotate(315,9,9)"/>
|
||||
</g>
|
||||
<circle cx="9" cy="9" r="2.2" fill="#cc2000"/>
|
||||
<circle cx="14.5" cy="14.5" r="3" fill="#45B83B"/>
|
||||
</svg>
|
||||
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_2707_1961)">
|
||||
<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="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 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="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"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_2707_1961">
|
||||
<rect width="18" height="18" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 841 B After Width: | Height: | Size: 1.5 KiB |
@@ -1,14 +1,13 @@
|
||||
<svg viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g fill="#980000" fill-opacity="0.88">
|
||||
<path d="M9,9 Q6.4,4.8 9,1.2 Q11.6,4.8 9,9Z" transform="rotate(0,9,9)"/>
|
||||
<path d="M9,9 Q6.4,4.8 9,1.2 Q11.6,4.8 9,9Z" transform="rotate(45,9,9)"/>
|
||||
<path d="M9,9 Q6.4,4.8 9,1.2 Q11.6,4.8 9,9Z" transform="rotate(90,9,9)"/>
|
||||
<path d="M9,9 Q6.4,4.8 9,1.2 Q11.6,4.8 9,9Z" transform="rotate(135,9,9)"/>
|
||||
<path d="M9,9 Q6.4,4.8 9,1.2 Q11.6,4.8 9,9Z" transform="rotate(180,9,9)"/>
|
||||
<path d="M9,9 Q6.4,4.8 9,1.2 Q11.6,4.8 9,9Z" transform="rotate(225,9,9)"/>
|
||||
<path d="M9,9 Q6.4,4.8 9,1.2 Q11.6,4.8 9,9Z" transform="rotate(270,9,9)"/>
|
||||
<path d="M9,9 Q6.4,4.8 9,1.2 Q11.6,4.8 9,9Z" transform="rotate(315,9,9)"/>
|
||||
</g>
|
||||
<circle cx="9" cy="9" r="2.2" fill="#cc2000"/>
|
||||
<circle cx="14.5" cy="14.5" r="3" fill="#989898"/>
|
||||
</svg>
|
||||
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_2707_2015)">
|
||||
<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="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 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="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"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_2707_2015">
|
||||
<rect width="18" height="18" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 841 B After Width: | Height: | Size: 1.5 KiB |
@@ -1,13 +1,19 @@
|
||||
<svg viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g fill="#980000" fill-opacity="0.88">
|
||||
<path d="M9,9 Q6.4,4.8 9,1.2 Q11.6,4.8 9,9Z" transform="rotate(0,9,9)"/>
|
||||
<path d="M9,9 Q6.4,4.8 9,1.2 Q11.6,4.8 9,9Z" transform="rotate(45,9,9)"/>
|
||||
<path d="M9,9 Q6.4,4.8 9,1.2 Q11.6,4.8 9,9Z" transform="rotate(90,9,9)"/>
|
||||
<path d="M9,9 Q6.4,4.8 9,1.2 Q11.6,4.8 9,9Z" transform="rotate(135,9,9)"/>
|
||||
<path d="M9,9 Q6.4,4.8 9,1.2 Q11.6,4.8 9,9Z" transform="rotate(180,9,9)"/>
|
||||
<path d="M9,9 Q6.4,4.8 9,1.2 Q11.6,4.8 9,9Z" transform="rotate(225,9,9)"/>
|
||||
<path d="M9,9 Q6.4,4.8 9,1.2 Q11.6,4.8 9,9Z" transform="rotate(270,9,9)"/>
|
||||
<path d="M9,9 Q6.4,4.8 9,1.2 Q11.6,4.8 9,9Z" transform="rotate(315,9,9)"/>
|
||||
</g>
|
||||
<circle cx="9" cy="9" r="2.2" fill="#cc2000"/>
|
||||
<!-- Generator: Adobe Illustrator 18.0.0, SVG Export Plug-In -->
|
||||
<svg version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:a="http://ns.adobe.com/AdobeSVGViewerExtensions/3.0/"
|
||||
x="0px" y="0px" width="18px" height="18px" viewBox="0 0 18 18" enable-background="new 0 0 18 18" xml:space="preserve">
|
||||
<defs>
|
||||
</defs>
|
||||
<g>
|
||||
<g>
|
||||
<circle fill="#FFFFFF" cx="9" cy="9" r="8.5"/>
|
||||
</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
|
||||
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>
|
||||
|
Before Width: | Height: | Size: 788 B After Width: | Height: | Size: 871 B |
@@ -1,26 +0,0 @@
|
||||
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 {
|
||||
console.warn('Warning: folds Icon patch target not found - may need updating.');
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('Warning: Could not patch folds:', e.message);
|
||||
}
|
||||
@@ -1,87 +0,0 @@
|
||||
#!/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');
|
||||
|
||||
const CDN = 'https://drive.lotusguild.org/public.php/dav/files/bHswJ9pNKp2t26N/cinny-decorations';
|
||||
|
||||
// Extract all slugs from the catalog file
|
||||
const catalog = readFileSync(catalogPath, 'utf8');
|
||||
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 {
|
||||
return { slug, ok: false, status: 0 };
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
const missing = results.filter((r) => !r.ok);
|
||||
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 path from 'path';
|
||||
import { execSync } from 'child_process';
|
||||
import { fileURLToPath } from 'url';
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
import { execSync } from "child_process";
|
||||
import { fileURLToPath } from "url";
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
@@ -9,26 +9,26 @@ const __dirname = path.dirname(__filename);
|
||||
const version = process.argv[2];
|
||||
|
||||
if (!version) {
|
||||
console.error('Version argument missing');
|
||||
console.error("Version argument missing");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const root = path.resolve(__dirname, '..');
|
||||
const root = path.resolve(__dirname, "..");
|
||||
const newVersionTag = `v${version}`;
|
||||
|
||||
// Update package.json + package-lock.json safely
|
||||
execSync(`npm version ${version} --no-git-tag-version`, {
|
||||
cwd: root,
|
||||
stdio: 'inherit',
|
||||
stdio: "inherit",
|
||||
});
|
||||
|
||||
console.log(`Updated package.json and package-lock.json → ${version}`);
|
||||
|
||||
// Update UI version references
|
||||
const files = [
|
||||
'src/app/features/settings/about/About.tsx',
|
||||
'src/app/pages/auth/AuthFooter.tsx',
|
||||
'src/app/pages/client/WelcomePage.tsx',
|
||||
"src/app/features/settings/about/About.tsx",
|
||||
"src/app/pages/auth/AuthFooter.tsx",
|
||||
"src/app/pages/client/WelcomePage.tsx",
|
||||
];
|
||||
|
||||
files.forEach((filePath) => {
|
||||
@@ -39,10 +39,10 @@ files.forEach((filePath) => {
|
||||
return;
|
||||
}
|
||||
|
||||
const content = fs.readFileSync(absPath, 'utf8');
|
||||
const content = fs.readFileSync(absPath, "utf8");
|
||||
const updated = content.replace(/v\d+\.\d+\.\d+/g, newVersionTag);
|
||||
|
||||
fs.writeFileSync(absPath, updated);
|
||||
|
||||
console.log(`Updated ${filePath} → ${newVersionTag}`);
|
||||
});
|
||||
});
|
||||
@@ -54,7 +54,7 @@ function AccountDataEdit({
|
||||
|
||||
const { handleKeyDown, operations, getTarget } = useTextAreaCodeEditor(
|
||||
textAreaRef,
|
||||
EDITOR_INTENT_SPACE_COUNT,
|
||||
EDITOR_INTENT_SPACE_COUNT
|
||||
);
|
||||
|
||||
const [submitState, submit] = useAsyncCallback<void, MatrixError, [string, object]>(submitChange);
|
||||
@@ -127,7 +127,6 @@ function AccountDataEdit({
|
||||
<Input
|
||||
variant={type.length > 0 || submitting ? 'SurfaceVariant' : 'Background'}
|
||||
name="typeInput"
|
||||
aria-label="Account data type"
|
||||
size="400"
|
||||
radii="300"
|
||||
readOnly={type.length > 0 || submitting}
|
||||
@@ -171,7 +170,6 @@ function AccountDataEdit({
|
||||
<TextAreaComponent
|
||||
ref={textAreaRef}
|
||||
name="contentTextArea"
|
||||
aria-label="JSON content"
|
||||
style={{
|
||||
fontFamily: 'monospace',
|
||||
}}
|
||||
@@ -278,7 +276,7 @@ export function AccountDataEditor({
|
||||
|
||||
const contentJSONStr = useMemo(
|
||||
() => JSON.stringify(data.content, null, EDITOR_INTENT_SPACE_COUNT),
|
||||
[data.content],
|
||||
[data.content]
|
||||
);
|
||||
|
||||
return (
|
||||
@@ -296,7 +294,7 @@ export function AccountDataEditor({
|
||||
</Chip>
|
||||
</Box>
|
||||
<Box shrink="No">
|
||||
<IconButton onClick={requestClose} variant="Surface" aria-label="Close">
|
||||
<IconButton onClick={requestClose} variant="Surface">
|
||||
<Icon src={Icons.Cross} />
|
||||
</IconButton>
|
||||
</Box>
|
||||
|
||||
@@ -45,11 +45,11 @@ export function AuthFlowsLoader({ fallback, error, children }: AuthFlowsLoaderPr
|
||||
};
|
||||
|
||||
return authFlows;
|
||||
}, [mx]),
|
||||
}, [mx])
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
load().catch(() => {});
|
||||
load();
|
||||
}, [load]);
|
||||
|
||||
if (state.status === AsyncStatus.Idle || state.status === AsyncStatus.Loading) {
|
||||
|
||||
@@ -1,71 +0,0 @@
|
||||
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,
|
||||
end: false,
|
||||
},
|
||||
location.pathname,
|
||||
location.pathname
|
||||
)
|
||||
) {
|
||||
navigate(getHomePath());
|
||||
@@ -37,7 +37,7 @@ export function BackRouteHandler({ children }: BackRouteHandlerProps) {
|
||||
caseSensitive: true,
|
||||
end: false,
|
||||
},
|
||||
location.pathname,
|
||||
location.pathname
|
||||
)
|
||||
) {
|
||||
navigate(getDirectPath());
|
||||
@@ -49,7 +49,7 @@ export function BackRouteHandler({ children }: BackRouteHandlerProps) {
|
||||
caseSensitive: true,
|
||||
end: false,
|
||||
},
|
||||
location.pathname,
|
||||
location.pathname
|
||||
);
|
||||
const encodedSpaceIdOrAlias = spaceMatch?.params.spaceIdOrAlias;
|
||||
const decodedSpaceIdOrAlias =
|
||||
@@ -66,7 +66,7 @@ export function BackRouteHandler({ children }: BackRouteHandlerProps) {
|
||||
caseSensitive: true,
|
||||
end: false,
|
||||
},
|
||||
location.pathname,
|
||||
location.pathname
|
||||
)
|
||||
) {
|
||||
navigate(getExplorePath());
|
||||
@@ -79,7 +79,7 @@ export function BackRouteHandler({ children }: BackRouteHandlerProps) {
|
||||
caseSensitive: true,
|
||||
end: false,
|
||||
},
|
||||
location.pathname,
|
||||
location.pathname
|
||||
)
|
||||
) {
|
||||
navigate(getInboxPath());
|
||||
|
||||
@@ -157,7 +157,7 @@ export function BackupRestoreTile({ crypto }: BackupRestoreTileProps) {
|
||||
setRestoreProgress(progress);
|
||||
},
|
||||
});
|
||||
}, [crypto, setRestoreProgress]),
|
||||
}, [crypto, setRestoreProgress])
|
||||
);
|
||||
|
||||
const handleRestore = () => {
|
||||
@@ -178,7 +178,6 @@ export function BackupRestoreTile({ crypto }: BackupRestoreTileProps) {
|
||||
)}
|
||||
<IconButton
|
||||
aria-pressed={!!menuCords}
|
||||
aria-label="Backup options"
|
||||
size="300"
|
||||
variant="Surface"
|
||||
radii="300"
|
||||
|
||||
@@ -1,31 +1,5 @@
|
||||
import React, { ReactNode, useCallback, useEffect, useRef, useState } from 'react';
|
||||
import React, { ReactNode, useCallback, useRef } from 'react';
|
||||
import { useAtomValue, useSetAtom } from 'jotai';
|
||||
import FocusTrap from 'focus-trap-react';
|
||||
import {
|
||||
Avatar,
|
||||
Box,
|
||||
Button,
|
||||
color,
|
||||
config,
|
||||
Dialog,
|
||||
Icon,
|
||||
Icons,
|
||||
Overlay,
|
||||
OverlayBackdrop,
|
||||
OverlayCenter,
|
||||
Text,
|
||||
toRem,
|
||||
} from 'folds';
|
||||
import {
|
||||
EventTimelineSetHandlerMap,
|
||||
EventType,
|
||||
JoinRule,
|
||||
RelationType,
|
||||
Room,
|
||||
RoomEvent,
|
||||
} from 'matrix-js-sdk';
|
||||
import { IRTCNotificationContent, RTCNotificationType } from 'matrix-js-sdk/lib/matrixrtc/types';
|
||||
import { CryptoBackend } from 'matrix-js-sdk/lib/common-crypto/CryptoBackend';
|
||||
import {
|
||||
CallEmbedContextProvider,
|
||||
CallEmbedRefContextProvider,
|
||||
@@ -33,426 +7,33 @@ import {
|
||||
useCallJoined,
|
||||
useCallThemeSync,
|
||||
useCallMemberSoundSync,
|
||||
useCallStart,
|
||||
} from '../hooks/useCallEmbed';
|
||||
import { callChatAtom, callEmbedAtom } from '../state/callEmbed';
|
||||
import { CallEmbed, useCallControlState } from '../plugins/call';
|
||||
import { CallEmbed } from '../plugins/call';
|
||||
import { useSelectedRoom } from '../hooks/router/useSelectedRoom';
|
||||
import { ScreenSize, useScreenSizeContext } from '../hooks/useScreenSize';
|
||||
import { useMatrixClient } from '../hooks/useMatrixClient';
|
||||
import CallSound from '../../../public/sound/call.ogg';
|
||||
import { useCallMembersChange, useCallSession } from '../hooks/useCall';
|
||||
import { useCallJoinLeaveSounds } from '../hooks/useCallJoinLeaveSounds';
|
||||
import { useRemoteAllMuted } from '../hooks/useCallSpeakers';
|
||||
import { useRoomAvatar, useRoomName } from '../hooks/useRoomMeta';
|
||||
import { mDirectAtom } from '../state/mDirectList';
|
||||
import { useMediaAuthentication } from '../hooks/useMediaAuthentication';
|
||||
import { mxcUrlToHttp, getMxIdLocalPart } from '../utils/matrix';
|
||||
import { RoomAvatar, RoomIcon } from './room-avatar';
|
||||
import { useRoomNavigate } from '../hooks/useRoomNavigate';
|
||||
import { getChatBg } from '../features/lotus/chatBackground';
|
||||
import { useTheme, ThemeKind } from '../hooks/useTheme';
|
||||
import { useSetting } from '../state/hooks/settings';
|
||||
import { settingsAtom } from '../state/settings';
|
||||
import { getStateEvent, getStateEvents, getMemberDisplayName } from '../utils/room';
|
||||
import { StateEvent } from '../../types/matrix/room';
|
||||
import { getPowersLevelFromMatrixEvent } from '../hooks/usePowerLevels';
|
||||
import { getRoomCreatorsForRoomId } from '../hooks/useRoomCreators';
|
||||
import { getRoomPermissionsAPI } from '../hooks/useRoomPermissions';
|
||||
import { useLivekitSupport } from '../hooks/useLivekitSupport';
|
||||
import { CallAvatarAnimation } from '../styles/Animations.css';
|
||||
import { webRTCSupported } from '../utils/rtc';
|
||||
|
||||
const PIP_MIN_W = 200;
|
||||
const PIP_MIN_H = 112;
|
||||
|
||||
type Corner = 'se' | 'sw' | 'ne' | 'nw';
|
||||
|
||||
/** Normalise the element to top/left positioning so resize math is uniform. */
|
||||
function normaliseToTopLeft(el: HTMLElement) {
|
||||
const rect = el.getBoundingClientRect();
|
||||
el.style.left = `${rect.left}px`;
|
||||
el.style.top = `${rect.top}px`;
|
||||
el.style.width = `${rect.width}px`;
|
||||
el.style.height = `${rect.height}px`;
|
||||
el.style.right = 'auto';
|
||||
el.style.bottom = 'auto';
|
||||
}
|
||||
|
||||
type IncomingCallInfo = {
|
||||
room: Room;
|
||||
sender: string;
|
||||
senderTs: number;
|
||||
lifetime: number;
|
||||
intent?: string;
|
||||
notificationType: RTCNotificationType;
|
||||
refEventId: string;
|
||||
};
|
||||
type IncomingCallProps = {
|
||||
dm: boolean;
|
||||
info: IncomingCallInfo;
|
||||
onIgnore: () => void;
|
||||
onAnswer: (room: Room, video: boolean) => void;
|
||||
onReject: (room: Room, eventId: string) => void;
|
||||
};
|
||||
function IncomingCall({ dm, info, onIgnore, onAnswer, onReject }: IncomingCallProps) {
|
||||
const mx = useMatrixClient();
|
||||
const useAuthentication = useMediaAuthentication();
|
||||
const livekitSupported = useLivekitSupport();
|
||||
const rtcSupported = webRTCSupported();
|
||||
const canAnswer = livekitSupported && rtcSupported;
|
||||
const { room } = info;
|
||||
|
||||
const audioRef = useRef<HTMLAudioElement>(null);
|
||||
|
||||
const roomName = useRoomName(room);
|
||||
const roomAvatar = useRoomAvatar(room, dm);
|
||||
const avatarUrl = roomAvatar
|
||||
? (mxcUrlToHttp(mx, roomAvatar, useAuthentication, 96, 96, 'crop') ?? undefined)
|
||||
: undefined;
|
||||
|
||||
const session = useCallSession(room);
|
||||
useCallMembersChange(
|
||||
session,
|
||||
useCallback(
|
||||
(members) => {
|
||||
if (members.length === 0) {
|
||||
onIgnore();
|
||||
}
|
||||
},
|
||||
[onIgnore],
|
||||
),
|
||||
);
|
||||
|
||||
const playSound = useCallback(() => {
|
||||
const audioElement = audioRef.current;
|
||||
audioElement?.play().catch(() => undefined);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const audioEl = audioRef.current;
|
||||
if (info.notificationType === 'ring') {
|
||||
playSound();
|
||||
}
|
||||
return () => {
|
||||
if (audioEl) {
|
||||
audioEl.pause();
|
||||
audioEl.currentTime = 0;
|
||||
}
|
||||
};
|
||||
}, [playSound, info.notificationType]);
|
||||
|
||||
useEffect(() => {
|
||||
const remaining = info.senderTs + info.lifetime - Date.now();
|
||||
if (remaining <= 0) {
|
||||
onIgnore();
|
||||
return;
|
||||
}
|
||||
const id = setTimeout(onIgnore, remaining);
|
||||
return () => clearTimeout(id);
|
||||
}, [info.senderTs, info.lifetime, onIgnore]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Overlay open backdrop={<OverlayBackdrop />}>
|
||||
<OverlayCenter>
|
||||
<FocusTrap
|
||||
focusTrapOptions={{
|
||||
initialFocus: false,
|
||||
onDeactivate: () => onIgnore(),
|
||||
clickOutsideDeactivates: false,
|
||||
escapeDeactivates: false,
|
||||
}}
|
||||
>
|
||||
<Dialog style={{ maxWidth: toRem(324) }}>
|
||||
<Box style={{ padding: config.space.S400 }} direction="Column" gap="700">
|
||||
<Text size="T200" align="Center">
|
||||
{getMemberDisplayName(info.room, info.sender) ??
|
||||
getMxIdLocalPart(info.sender) ??
|
||||
info.sender}
|
||||
</Text>
|
||||
<Box direction="Column" gap="500" alignItems="Center">
|
||||
<Box shrink="No">
|
||||
<Avatar size="500" className={CallAvatarAnimation}>
|
||||
<RoomAvatar
|
||||
roomId={room.roomId}
|
||||
src={avatarUrl}
|
||||
alt={roomName}
|
||||
renderFallback={() => (
|
||||
<RoomIcon
|
||||
roomType={room.getType()}
|
||||
size="400"
|
||||
joinRule={room.getJoinRule()}
|
||||
filled
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</Avatar>
|
||||
</Box>
|
||||
<Box grow="Yes" direction="Column" gap="100">
|
||||
<Text size="H3" align="Center" truncate>
|
||||
{roomName}
|
||||
</Text>
|
||||
<Text size="T300">
|
||||
{info.intent === 'video' ? 'Incoming Video Call' : 'Incoming Voice Call'}
|
||||
</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
{!livekitSupported && (
|
||||
<Text
|
||||
style={{ margin: 'auto', color: color.Critical.Main }}
|
||||
size="L400"
|
||||
align="Center"
|
||||
>
|
||||
Your homeserver does not support calling.
|
||||
</Text>
|
||||
)}
|
||||
{!webRTCSupported() && (
|
||||
<Text
|
||||
style={{ margin: 'auto', color: color.Critical.Main }}
|
||||
size="L400"
|
||||
align="Center"
|
||||
>
|
||||
Your browser does not support WebRTC, which is required for calling.
|
||||
</Text>
|
||||
)}
|
||||
<Box direction="Column" gap="300">
|
||||
<Button
|
||||
style={{ flexGrow: 1 }}
|
||||
variant="Success"
|
||||
size="400"
|
||||
radii="400"
|
||||
onClick={() => onAnswer(room, info.intent === 'video')}
|
||||
before={
|
||||
<Icon
|
||||
size="200"
|
||||
src={info.intent === 'video' ? Icons.VideoCamera : Icons.Phone}
|
||||
filled
|
||||
/>
|
||||
}
|
||||
disabled={!canAnswer}
|
||||
>
|
||||
<Text as="span" size="B400">
|
||||
Answer
|
||||
</Text>
|
||||
</Button>
|
||||
<Button
|
||||
style={{ flexGrow: 1 }}
|
||||
variant={dm ? 'Critical' : 'Secondary'}
|
||||
fill="Soft"
|
||||
size="400"
|
||||
radii="400"
|
||||
onClick={() => (dm ? onReject(room, info.refEventId) : onIgnore())}
|
||||
before={<Icon size="200" src={Icons.Cross} filled />}
|
||||
>
|
||||
<Text as="span" size="B400">
|
||||
{dm ? 'Reject' : 'Ignore'}
|
||||
</Text>
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
</Dialog>
|
||||
</FocusTrap>
|
||||
</OverlayCenter>
|
||||
</Overlay>
|
||||
<audio ref={audioRef} loop style={{ display: 'none' }}>
|
||||
<source src={CallSound} type="audio/ogg" />
|
||||
</audio>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
type IncomingCallListenerProps = {
|
||||
callEmbed?: CallEmbed;
|
||||
joined?: boolean;
|
||||
};
|
||||
function IncomingCallListener({ callEmbed, joined }: IncomingCallListenerProps) {
|
||||
const mx = useMatrixClient();
|
||||
const directs = useAtomValue(mDirectAtom);
|
||||
const { navigateRoom } = useRoomNavigate();
|
||||
|
||||
const [callInfo, setCallInfo] = useState<IncomingCallInfo>();
|
||||
const dm = callInfo ? directs.has(callInfo.room.roomId) : false;
|
||||
const startCall = useCallStart(dm);
|
||||
|
||||
const handleTimelineEvent: EventTimelineSetHandlerMap[RoomEvent.Timeline] = useCallback(
|
||||
async (event, room, toStartOfTimeline, removed, data) => {
|
||||
// only process rtc notification reference events.
|
||||
// we do not want to wait to decrypt all events.
|
||||
if (event.getRelation()?.rel_type !== RelationType.Reference) return;
|
||||
|
||||
if (event.isEncrypted()) {
|
||||
if (!event.isBeingDecrypted()) {
|
||||
await event.attemptDecryption(mx.getCrypto() as CryptoBackend);
|
||||
}
|
||||
await event.getDecryptionPromise();
|
||||
}
|
||||
|
||||
if (
|
||||
!room ||
|
||||
event.getType() !== EventType.RTCNotification ||
|
||||
event.getSender() === mx.getSafeUserId() ||
|
||||
!data.liveEvent
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const sender = event.getSender();
|
||||
const content = event.getContent<IRTCNotificationContent>();
|
||||
const senderTs =
|
||||
content.sender_ts - event.getTs() > 20000 ? event.getTs() : content.sender_ts;
|
||||
const lifetime = Math.min(content.lifetime, 120000);
|
||||
const notificationType = content.notification_type;
|
||||
const relation =
|
||||
event.getRelation()?.rel_type === RelationType.Reference ? event.getRelation() : undefined;
|
||||
const refEventId = relation?.event_id;
|
||||
|
||||
const mention =
|
||||
content['m.mentions']?.room ||
|
||||
content['m.mentions']?.user_ids?.includes(mx.getSafeUserId());
|
||||
if (!sender || !refEventId || !mention || Date.now() >= senderTs + lifetime) {
|
||||
return;
|
||||
}
|
||||
|
||||
const powerLevelsEvent = getStateEvent(room, StateEvent.RoomPowerLevels);
|
||||
const powerLevels = getPowersLevelFromMatrixEvent(powerLevelsEvent);
|
||||
const creators = getRoomCreatorsForRoomId(mx, room.roomId);
|
||||
const permissions = getRoomPermissionsAPI(creators, powerLevels);
|
||||
|
||||
const hasCallPermission = permissions.stateEvent(
|
||||
StateEvent.GroupCallMemberPrefix,
|
||||
mx.getSafeUserId(),
|
||||
);
|
||||
if (!hasCallPermission) return;
|
||||
|
||||
// Only ring in rooms where the call button is visible: DMs or invite-only rooms
|
||||
// with no space parent. Persistent voice rooms (call rooms), space channels,
|
||||
// restricted rooms, and public rooms must never trigger ringing.
|
||||
if (room.isCallRoom()) return;
|
||||
const isDirect = directs.has(room.roomId);
|
||||
const isSpaceChild = getStateEvents(room, StateEvent.SpaceParent).length > 0;
|
||||
const joinRule = room.getJoinRule();
|
||||
const isPrivateInviteGroup = !isSpaceChild && joinRule === JoinRule.Invite;
|
||||
if (!isDirect && !isPrivateInviteGroup) return;
|
||||
|
||||
const info: IncomingCallInfo = {
|
||||
room,
|
||||
sender,
|
||||
senderTs,
|
||||
lifetime,
|
||||
intent:
|
||||
'm.call.intent' in content && typeof content['m.call.intent'] === 'string'
|
||||
? content['m.call.intent']
|
||||
: undefined,
|
||||
notificationType,
|
||||
refEventId,
|
||||
};
|
||||
|
||||
setCallInfo(info);
|
||||
},
|
||||
[mx, directs],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
mx.on(RoomEvent.Timeline, handleTimelineEvent);
|
||||
return () => {
|
||||
mx.removeListener(RoomEvent.Timeline, handleTimelineEvent);
|
||||
};
|
||||
}, [mx, handleTimelineEvent]);
|
||||
|
||||
const handleIgnore = useCallback(() => {
|
||||
setCallInfo(undefined);
|
||||
}, []);
|
||||
|
||||
const handleReject = useCallback(
|
||||
(room: Room, eventId: string) => {
|
||||
mx.sendEvent(room.roomId, EventType.RTCDecline, {
|
||||
'm.relates_to': {
|
||||
rel_type: RelationType.Reference,
|
||||
event_id: eventId,
|
||||
},
|
||||
});
|
||||
setCallInfo(undefined);
|
||||
},
|
||||
[mx],
|
||||
);
|
||||
|
||||
const handleAnswer = useCallback(
|
||||
(room: Room, video: boolean) => {
|
||||
startCall(room, { microphone: true, video, sound: true });
|
||||
setCallInfo(undefined);
|
||||
navigateRoom(room.roomId);
|
||||
},
|
||||
[startCall, navigateRoom],
|
||||
);
|
||||
|
||||
if (callInfo && callEmbed?.roomId === callInfo.room.roomId) {
|
||||
return null;
|
||||
}
|
||||
return !joined && callInfo ? (
|
||||
<IncomingCall
|
||||
dm={dm}
|
||||
info={callInfo}
|
||||
onIgnore={handleIgnore}
|
||||
onAnswer={handleAnswer}
|
||||
onReject={handleReject}
|
||||
/>
|
||||
) : null;
|
||||
}
|
||||
|
||||
function CallUtils({ embed }: { embed: CallEmbed }) {
|
||||
const setCallEmbed = useSetAtom(callEmbedAtom);
|
||||
|
||||
useCallMemberSoundSync(embed);
|
||||
useCallJoinLeaveSounds(embed);
|
||||
useCallThemeSync(embed);
|
||||
useCallHangupEvent(
|
||||
embed,
|
||||
useCallback(() => {
|
||||
setCallEmbed(undefined);
|
||||
}, [setCallEmbed]),
|
||||
}, [setCallEmbed])
|
||||
);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/** Shown inside the PiP window when the local microphone is muted. */
|
||||
function PipMuteOverlay({ callEmbed }: { callEmbed: CallEmbed }) {
|
||||
const allMuted = useRemoteAllMuted(callEmbed);
|
||||
if (!allMuted) return null;
|
||||
return (
|
||||
<div
|
||||
aria-label="Microphone muted"
|
||||
style={{
|
||||
position: 'absolute',
|
||||
bottom: '8px',
|
||||
left: '8px',
|
||||
zIndex: 3,
|
||||
background: 'rgba(0,0,0,0.60)',
|
||||
backdropFilter: 'blur(4px)',
|
||||
borderRadius: '6px',
|
||||
padding: '3px 7px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '4px',
|
||||
pointerEvents: 'none',
|
||||
color: color.Critical.Main,
|
||||
fontSize: '13px',
|
||||
lineHeight: 1,
|
||||
userSelect: 'none',
|
||||
}}
|
||||
>
|
||||
<Icon size="100" src={Icons.MicMute} filled />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
type CallEmbedProviderProps = {
|
||||
children?: ReactNode;
|
||||
};
|
||||
export function CallEmbedProvider({ children }: CallEmbedProviderProps) {
|
||||
const callEmbed = useAtomValue(callEmbedAtom);
|
||||
const callEmbedRef = useRef<HTMLDivElement>(null) as React.RefObject<HTMLDivElement>;
|
||||
const callEmbedRef = useRef<HTMLDivElement>(null);
|
||||
const joined = useCallJoined(callEmbed);
|
||||
|
||||
const selectedRoom = useSelectedRoom();
|
||||
@@ -460,388 +41,13 @@ export function CallEmbedProvider({ children }: CallEmbedProviderProps) {
|
||||
const screenSize = useScreenSizeContext();
|
||||
|
||||
const chatOnlyView = chat && screenSize !== ScreenSize.Desktop;
|
||||
const inCallRoom = callEmbed && selectedRoom === callEmbed.roomId;
|
||||
const callActive = callEmbed && joined;
|
||||
const callVisible = inCallRoom && callActive && !chatOnlyView;
|
||||
const pipMode = callActive && !inCallRoom;
|
||||
const { navigateRoom } = useRoomNavigate();
|
||||
|
||||
const { screenshare: pipScreenshare } = useCallControlState(callEmbed?.control);
|
||||
|
||||
// Sync pip mode into CallControl so it can adjust behavior accordingly
|
||||
useEffect(() => {
|
||||
if (!callEmbed) return;
|
||||
callEmbed.control.setPipMode(!!pipMode);
|
||||
}, [pipMode, callEmbed]);
|
||||
|
||||
// When entering pip with screenshare active (or screenshare starts while in pip),
|
||||
// enable spotlight so the screenshare fills the pip window.
|
||||
// When screenshare ends, release the spotlight we auto-enabled.
|
||||
const pipAutoSpotlightRef = React.useRef(false);
|
||||
useEffect(() => {
|
||||
if (!pipMode || !callEmbed) return;
|
||||
if (pipScreenshare) {
|
||||
if (!callEmbed.control.spotlight) {
|
||||
callEmbed.control.toggleSpotlight();
|
||||
pipAutoSpotlightRef.current = true;
|
||||
}
|
||||
} else if (pipAutoSpotlightRef.current) {
|
||||
if (callEmbed.control.spotlight) callEmbed.control.toggleSpotlight();
|
||||
pipAutoSpotlightRef.current = false;
|
||||
}
|
||||
}, [pipMode, pipScreenshare, callEmbed]);
|
||||
|
||||
const theme = useTheme();
|
||||
const isDark = theme.kind === ThemeKind.Dark;
|
||||
const [chatBackground] = useSetting(settingsAtom, 'chatBackground');
|
||||
const wallpaperStyle = React.useMemo(
|
||||
() => getChatBg(chatBackground, isDark),
|
||||
[chatBackground, isDark],
|
||||
);
|
||||
|
||||
const pipDragRef = React.useRef<{
|
||||
startX: number;
|
||||
startY: number;
|
||||
origLeft: number;
|
||||
origTop: number;
|
||||
dragged: boolean;
|
||||
} | null>(null);
|
||||
const activeDragCleanupRef = React.useRef<(() => void) | null>(null);
|
||||
React.useEffect(
|
||||
() => () => {
|
||||
activeDragCleanupRef.current?.();
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
// Track previous pipMode to only reset position when entering/exiting pip
|
||||
const prevPipModeRef = React.useRef(false);
|
||||
|
||||
React.useEffect(() => {
|
||||
const el = callEmbedRef.current;
|
||||
if (!el) return;
|
||||
const wasInPip = prevPipModeRef.current;
|
||||
prevPipModeRef.current = !!pipMode;
|
||||
if (pipMode) {
|
||||
if (!wasInPip) {
|
||||
const saved = localStorage.getItem('pip-position');
|
||||
const savedPos = saved ? (JSON.parse(saved) as { left: number; top: number }) : null;
|
||||
el.style.right = 'auto';
|
||||
el.style.bottom = 'auto';
|
||||
if (savedPos) {
|
||||
el.style.left = `${Math.max(0, Math.min(savedPos.left, window.innerWidth - 280))}px`;
|
||||
el.style.top = `${Math.max(0, Math.min(savedPos.top, window.innerHeight - 158))}px`;
|
||||
} else {
|
||||
el.style.left = `${window.innerWidth - 280 - 16}px`;
|
||||
el.style.top = `${window.innerHeight - 158 - 72}px`;
|
||||
}
|
||||
el.style.width = '280px';
|
||||
el.style.height = '158px';
|
||||
el.style.borderRadius = '12px';
|
||||
el.style.overflow = 'hidden';
|
||||
el.style.zIndex = '99';
|
||||
el.style.boxShadow = '0 8px 32px rgba(0,0,0,0.55)';
|
||||
el.style.border = '1px solid rgba(255,255,255,0.1)';
|
||||
}
|
||||
el.style.visibility = 'visible';
|
||||
} else {
|
||||
if (wasInPip) {
|
||||
// Exiting pip: clear all pip styles; syncCallEmbedPlacement will restore correct position
|
||||
el.style.top = '';
|
||||
el.style.left = '';
|
||||
el.style.bottom = '';
|
||||
el.style.right = '';
|
||||
el.style.width = '';
|
||||
el.style.height = '';
|
||||
el.style.borderRadius = '';
|
||||
el.style.overflow = '';
|
||||
el.style.zIndex = '';
|
||||
el.style.boxShadow = '';
|
||||
el.style.border = '';
|
||||
}
|
||||
// syncCallEmbedPlacement owns top/left/width/height; don't clear them on visibility changes
|
||||
el.style.visibility = callVisible ? '' : 'hidden';
|
||||
}
|
||||
}, [pipMode, callVisible]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!pipMode) return;
|
||||
const onPipWindowResize = (): void => {
|
||||
const el = callEmbedRef.current;
|
||||
if (!el) return;
|
||||
// Normalise bottom/right → top/left so clamp math works regardless of initial position.
|
||||
if (!el.style.left || el.style.left === 'auto') normaliseToTopLeft(el);
|
||||
const l = parseFloat(el.style.left);
|
||||
const t = parseFloat(el.style.top);
|
||||
if (!isNaN(l))
|
||||
el.style.left = `${Math.max(0, Math.min(l, window.innerWidth - el.offsetWidth))}px`;
|
||||
if (!isNaN(t))
|
||||
el.style.top = `${Math.max(0, Math.min(t, window.innerHeight - el.offsetHeight))}px`;
|
||||
};
|
||||
window.addEventListener('resize', onPipWindowResize);
|
||||
return () => window.removeEventListener('resize', onPipWindowResize);
|
||||
}, [pipMode, callEmbedRef]);
|
||||
|
||||
const handlePipDoubleClick = (e: React.MouseEvent) => {
|
||||
const el = callEmbedRef.current;
|
||||
if (!el) return;
|
||||
e.stopPropagation();
|
||||
const margin = 16;
|
||||
const w = el.offsetWidth;
|
||||
const h = el.offsetHeight;
|
||||
const elRect = el.getBoundingClientRect();
|
||||
const cx = elRect.left + w / 2;
|
||||
const cy = elRect.top + h / 2;
|
||||
const snapLeft = cx < window.innerWidth / 2 ? margin : window.innerWidth - w - margin;
|
||||
const snapTop = cy < window.innerHeight / 2 ? margin : window.innerHeight - h - margin;
|
||||
el.style.left = `${snapLeft}px`;
|
||||
el.style.top = `${snapTop}px`;
|
||||
el.style.right = 'auto';
|
||||
el.style.bottom = 'auto';
|
||||
el.style.transition = 'left 0.18s ease, top 0.18s ease';
|
||||
setTimeout(() => {
|
||||
if (el) el.style.transition = '';
|
||||
}, 200);
|
||||
localStorage.setItem('pip-position', JSON.stringify({ left: snapLeft, top: snapTop }));
|
||||
};
|
||||
|
||||
const handlePipMouseDown = (e: React.MouseEvent) => {
|
||||
const el = callEmbedRef.current;
|
||||
if (!el) return;
|
||||
const rect = el.getBoundingClientRect();
|
||||
pipDragRef.current = {
|
||||
startX: e.clientX,
|
||||
startY: e.clientY,
|
||||
origLeft: rect.left,
|
||||
origTop: rect.top,
|
||||
dragged: false,
|
||||
};
|
||||
const onMove = (ev: MouseEvent) => {
|
||||
if (!pipDragRef.current || !el) return;
|
||||
const dx = ev.clientX - pipDragRef.current.startX;
|
||||
const dy = ev.clientY - pipDragRef.current.startY;
|
||||
if (!pipDragRef.current.dragged && Math.abs(dx) + Math.abs(dy) > 5) {
|
||||
pipDragRef.current.dragged = true;
|
||||
document.body.style.cursor = 'grabbing';
|
||||
document.body.style.userSelect = 'none';
|
||||
}
|
||||
if (pipDragRef.current.dragged) {
|
||||
el.style.left = `${Math.max(
|
||||
0,
|
||||
Math.min(window.innerWidth - el.offsetWidth, pipDragRef.current.origLeft + dx),
|
||||
)}px`;
|
||||
el.style.top = `${Math.max(
|
||||
0,
|
||||
Math.min(window.innerHeight - el.offsetHeight, pipDragRef.current.origTop + dy),
|
||||
)}px`;
|
||||
el.style.right = 'auto';
|
||||
el.style.bottom = 'auto';
|
||||
}
|
||||
};
|
||||
const onUp = () => {
|
||||
document.removeEventListener('mousemove', onMove);
|
||||
document.removeEventListener('mouseup', onUp);
|
||||
document.body.style.cursor = '';
|
||||
document.body.style.userSelect = '';
|
||||
activeDragCleanupRef.current = null;
|
||||
if (el && pipDragRef.current?.dragged) {
|
||||
const savedRect = el.getBoundingClientRect();
|
||||
localStorage.setItem(
|
||||
'pip-position',
|
||||
JSON.stringify({ left: savedRect.left, top: savedRect.top }),
|
||||
);
|
||||
}
|
||||
setTimeout(() => {
|
||||
if (pipDragRef.current) pipDragRef.current.dragged = false;
|
||||
}, 0);
|
||||
};
|
||||
activeDragCleanupRef.current = () => {
|
||||
document.removeEventListener('mousemove', onMove);
|
||||
document.removeEventListener('mouseup', onUp);
|
||||
document.body.style.cursor = '';
|
||||
document.body.style.userSelect = '';
|
||||
};
|
||||
document.addEventListener('mousemove', onMove);
|
||||
document.addEventListener('mouseup', onUp);
|
||||
};
|
||||
|
||||
const handlePipTouchStart = (e: React.TouchEvent) => {
|
||||
const el = callEmbedRef.current;
|
||||
if (!el || e.touches.length !== 1) return;
|
||||
const touch = e.touches[0];
|
||||
const rect = el.getBoundingClientRect();
|
||||
pipDragRef.current = {
|
||||
startX: touch.clientX,
|
||||
startY: touch.clientY,
|
||||
origLeft: rect.left,
|
||||
origTop: rect.top,
|
||||
dragged: false,
|
||||
};
|
||||
const onTouchMove = (ev: TouchEvent) => {
|
||||
if (!pipDragRef.current || !el || ev.touches.length !== 1) return;
|
||||
ev.preventDefault();
|
||||
const t = ev.touches[0];
|
||||
const dx = t.clientX - pipDragRef.current.startX;
|
||||
const dy = t.clientY - pipDragRef.current.startY;
|
||||
if (!pipDragRef.current.dragged && Math.abs(dx) + Math.abs(dy) > 5)
|
||||
pipDragRef.current.dragged = true;
|
||||
if (pipDragRef.current.dragged) {
|
||||
el.style.left = `${Math.max(
|
||||
0,
|
||||
Math.min(window.innerWidth - el.offsetWidth, pipDragRef.current.origLeft + dx),
|
||||
)}px`;
|
||||
el.style.top = `${Math.max(
|
||||
0,
|
||||
Math.min(window.innerHeight - el.offsetHeight, pipDragRef.current.origTop + dy),
|
||||
)}px`;
|
||||
el.style.right = 'auto';
|
||||
el.style.bottom = 'auto';
|
||||
}
|
||||
};
|
||||
const onTouchEnd = () => {
|
||||
document.removeEventListener('touchmove', onTouchMove);
|
||||
document.removeEventListener('touchend', onTouchEnd);
|
||||
activeDragCleanupRef.current = null;
|
||||
if (el && pipDragRef.current?.dragged) {
|
||||
const savedRect = el.getBoundingClientRect();
|
||||
localStorage.setItem(
|
||||
'pip-position',
|
||||
JSON.stringify({ left: savedRect.left, top: savedRect.top }),
|
||||
);
|
||||
}
|
||||
setTimeout(() => {
|
||||
if (pipDragRef.current) pipDragRef.current.dragged = false;
|
||||
}, 0);
|
||||
};
|
||||
activeDragCleanupRef.current = () => {
|
||||
document.removeEventListener('touchmove', onTouchMove);
|
||||
document.removeEventListener('touchend', onTouchEnd);
|
||||
};
|
||||
document.addEventListener('touchmove', onTouchMove, { passive: false });
|
||||
document.addEventListener('touchend', onTouchEnd);
|
||||
};
|
||||
|
||||
function applyResize(
|
||||
el: HTMLElement,
|
||||
corner: Corner,
|
||||
sx: number,
|
||||
sy: number,
|
||||
sw: number,
|
||||
sh: number,
|
||||
sl: number,
|
||||
st: number,
|
||||
cx: number,
|
||||
cy: number,
|
||||
) {
|
||||
const dx = cx - sx;
|
||||
const dy = cy - sy;
|
||||
let w = sw;
|
||||
let h = sh;
|
||||
let l = sl;
|
||||
let t = st;
|
||||
if (corner === 'se') {
|
||||
w = sw + dx;
|
||||
h = sh + dy;
|
||||
}
|
||||
if (corner === 'sw') {
|
||||
w = sw - dx;
|
||||
h = sh + dy;
|
||||
l = sl + sw - Math.max(PIP_MIN_W, w);
|
||||
}
|
||||
if (corner === 'ne') {
|
||||
w = sw + dx;
|
||||
h = sh - dy;
|
||||
t = st + sh - Math.max(PIP_MIN_H, h);
|
||||
}
|
||||
if (corner === 'nw') {
|
||||
w = sw - dx;
|
||||
h = sh - dy;
|
||||
l = sl + sw - Math.max(PIP_MIN_W, w);
|
||||
t = st + sh - Math.max(PIP_MIN_H, h);
|
||||
}
|
||||
w = Math.max(PIP_MIN_W, Math.min(w, window.innerWidth));
|
||||
h = Math.max(PIP_MIN_H, Math.min(h, window.innerHeight));
|
||||
l = Math.max(0, Math.min(l, window.innerWidth - PIP_MIN_W));
|
||||
t = Math.max(0, Math.min(t, window.innerHeight - PIP_MIN_H));
|
||||
el.style.width = `${w}px`;
|
||||
el.style.height = `${h}px`;
|
||||
el.style.left = `${l}px`;
|
||||
el.style.top = `${t}px`;
|
||||
}
|
||||
|
||||
const handleResizeMouseDown = (e: React.MouseEvent, corner: Corner) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
const el = callEmbedRef.current;
|
||||
if (!el) return;
|
||||
normaliseToTopLeft(el);
|
||||
const sx = e.clientX;
|
||||
const sy = e.clientY;
|
||||
const sw = el.offsetWidth;
|
||||
const sh = el.offsetHeight;
|
||||
const sl = parseFloat(el.style.left);
|
||||
const st = parseFloat(el.style.top);
|
||||
document.body.style.cursor = `${corner}-resize`;
|
||||
document.body.style.userSelect = 'none';
|
||||
const onMove = (ev: MouseEvent) => {
|
||||
applyResize(el, corner, sx, sy, sw, sh, sl, st, ev.clientX, ev.clientY);
|
||||
};
|
||||
const onUp = () => {
|
||||
document.removeEventListener('mousemove', onMove);
|
||||
document.removeEventListener('mouseup', onUp);
|
||||
document.body.style.cursor = '';
|
||||
document.body.style.userSelect = '';
|
||||
activeDragCleanupRef.current = null;
|
||||
};
|
||||
activeDragCleanupRef.current = () => {
|
||||
document.removeEventListener('mousemove', onMove);
|
||||
document.removeEventListener('mouseup', onUp);
|
||||
document.body.style.cursor = '';
|
||||
document.body.style.userSelect = '';
|
||||
};
|
||||
document.addEventListener('mousemove', onMove);
|
||||
document.addEventListener('mouseup', onUp);
|
||||
};
|
||||
|
||||
const handleResizeTouchStart = (e: React.TouchEvent, corner: Corner) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
const el = callEmbedRef.current;
|
||||
if (!el || e.touches.length !== 1) return;
|
||||
normaliseToTopLeft(el);
|
||||
const touch = e.touches[0];
|
||||
const sx = touch.clientX;
|
||||
const sy = touch.clientY;
|
||||
const sw = el.offsetWidth;
|
||||
const sh = el.offsetHeight;
|
||||
const sl = parseFloat(el.style.left);
|
||||
const st = parseFloat(el.style.top);
|
||||
const onMove = (ev: TouchEvent) => {
|
||||
if (ev.touches.length !== 1) return;
|
||||
ev.preventDefault();
|
||||
const t = ev.touches[0];
|
||||
applyResize(el, corner, sx, sy, sw, sh, sl, st, t.clientX, t.clientY);
|
||||
};
|
||||
const onEnd = () => {
|
||||
document.removeEventListener('touchmove', onMove);
|
||||
document.removeEventListener('touchend', onEnd);
|
||||
activeDragCleanupRef.current = null;
|
||||
};
|
||||
activeDragCleanupRef.current = () => {
|
||||
document.removeEventListener('touchmove', onMove);
|
||||
document.removeEventListener('touchend', onEnd);
|
||||
};
|
||||
document.addEventListener('touchmove', onMove, { passive: false });
|
||||
document.addEventListener('touchend', onEnd);
|
||||
};
|
||||
const callVisible = callEmbed && selectedRoom === callEmbed.roomId && joined && !chatOnlyView;
|
||||
|
||||
return (
|
||||
<CallEmbedContextProvider value={callEmbed}>
|
||||
{callEmbed && <CallUtils embed={callEmbed} />}
|
||||
<CallEmbedRefContextProvider value={callEmbedRef}>
|
||||
<IncomingCallListener callEmbed={callEmbed} joined={joined} />
|
||||
{children}
|
||||
</CallEmbedRefContextProvider>
|
||||
<CallEmbedRefContextProvider value={callEmbedRef}>{children}</CallEmbedRefContextProvider>
|
||||
<div
|
||||
data-call-embed-container
|
||||
style={{
|
||||
@@ -851,93 +57,9 @@ export function CallEmbedProvider({ children }: CallEmbedProviderProps) {
|
||||
left: 0,
|
||||
width: '100%',
|
||||
height: '50%',
|
||||
...(callVisible && !pipMode ? wallpaperStyle : {}),
|
||||
}}
|
||||
ref={callEmbedRef}
|
||||
>
|
||||
{pipMode && callEmbed && (
|
||||
<>
|
||||
<div
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
aria-label="Return to call"
|
||||
onMouseDown={handlePipMouseDown}
|
||||
onTouchStart={handlePipTouchStart}
|
||||
onDoubleClick={handlePipDoubleClick}
|
||||
onClick={() => {
|
||||
if (!pipDragRef.current?.dragged) navigateRoom(callEmbed.roomId);
|
||||
}}
|
||||
onKeyDown={(e) => e.key === 'Enter' && navigateRoom(callEmbed.roomId)}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
inset: 0,
|
||||
zIndex: 1,
|
||||
background: 'transparent',
|
||||
cursor: 'grab',
|
||||
display: 'flex',
|
||||
alignItems: 'flex-start',
|
||||
justifyContent: 'flex-end',
|
||||
padding: '6px',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
background: 'rgba(0,0,0,0.65)',
|
||||
backdropFilter: 'blur(4px)',
|
||||
borderRadius: '6px',
|
||||
padding: '3px 8px',
|
||||
color: '#fff',
|
||||
fontSize: '11px',
|
||||
fontWeight: 600,
|
||||
pointerEvents: 'none',
|
||||
}}
|
||||
>
|
||||
↗ Return to call
|
||||
</div>
|
||||
</div>
|
||||
<PipMuteOverlay callEmbed={callEmbed} />
|
||||
{(['se', 'sw', 'ne', 'nw'] as Corner[]).map((corner) => {
|
||||
const s = corner.includes('s');
|
||||
const e2 = corner.includes('e');
|
||||
const dots = [
|
||||
[3, 3],
|
||||
[3, 10],
|
||||
[10, 3],
|
||||
].map(([a, b]) => ({
|
||||
position: 'absolute' as const,
|
||||
width: 5,
|
||||
height: 5,
|
||||
borderRadius: '50%',
|
||||
background: 'rgba(255,255,255,0.65)',
|
||||
boxShadow: '0 0 3px rgba(0,0,0,0.4)',
|
||||
[s ? 'bottom' : 'top']: a,
|
||||
[e2 ? 'right' : 'left']: b,
|
||||
}));
|
||||
return (
|
||||
<div
|
||||
key={corner}
|
||||
onMouseDown={(ev) => handleResizeMouseDown(ev, corner)}
|
||||
onTouchStart={(ev) => handleResizeTouchStart(ev, corner)}
|
||||
onClick={(ev) => ev.stopPropagation()}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
width: '24px',
|
||||
height: '24px',
|
||||
[s ? 'bottom' : 'top']: 0,
|
||||
[e2 ? 'right' : 'left']: 0,
|
||||
cursor: `${corner}-resize`,
|
||||
zIndex: 2,
|
||||
}}
|
||||
>
|
||||
{dots.map((style, i) => (
|
||||
<div key={i} style={style} />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
/>
|
||||
</CallEmbedContextProvider>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ export function CapabilitiesLoader({ children }: CapabilitiesLoaderProps) {
|
||||
const [state, load] = useAsyncCallback(useCallback(() => mx.getCapabilities(), [mx]));
|
||||
|
||||
useEffect(() => {
|
||||
load().catch(() => {});
|
||||
load();
|
||||
}, [load]);
|
||||
|
||||
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), []);
|
||||
|
||||
useEffect(() => {
|
||||
load().catch(() => undefined);
|
||||
load();
|
||||
}, [load]);
|
||||
|
||||
if (state.status === AsyncStatus.Idle || state.status === AsyncStatus.Loading) {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { ReactNode, RefObject, useCallback, useRef, useState } from 'react';
|
||||
import { ReactNode, RefObject, useCallback, useRef, useState } from 'react';
|
||||
import { useDebounce } from '../hooks/useDebounce';
|
||||
|
||||
type ConfirmPasswordMatchProps = {
|
||||
@@ -7,13 +7,13 @@ type ConfirmPasswordMatchProps = {
|
||||
match: boolean,
|
||||
doMatch: () => void,
|
||||
passRef: RefObject<HTMLInputElement>,
|
||||
confPassRef: RefObject<HTMLInputElement>,
|
||||
confPassRef: RefObject<HTMLInputElement>
|
||||
) => ReactNode;
|
||||
};
|
||||
export function ConfirmPasswordMatch({ initialValue, children }: ConfirmPasswordMatchProps) {
|
||||
const [match, setMatch] = useState(initialValue);
|
||||
const passRef = useRef<HTMLInputElement>(null) as React.RefObject<HTMLInputElement>;
|
||||
const confPassRef = useRef<HTMLInputElement>(null) as React.RefObject<HTMLInputElement>;
|
||||
const passRef = useRef<HTMLInputElement>(null);
|
||||
const confPassRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const doMatch = useDebounce(
|
||||
useCallback(() => {
|
||||
@@ -28,7 +28,7 @@ export function ConfirmPasswordMatch({ initialValue, children }: ConfirmPassword
|
||||
{
|
||||
wait: 500,
|
||||
immediate: false,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
return children(match, doMatch, passRef, confPassRef);
|
||||
|
||||
@@ -137,6 +137,7 @@ function CompareEmoji({ sasData }: { sasData: ShowSasCallbacks }) {
|
||||
>
|
||||
{sasData.sas.emoji?.map(([emoji, name], index) => (
|
||||
<Box
|
||||
// eslint-disable-next-line react/no-array-index-key
|
||||
key={`${emoji}${name}${index}`}
|
||||
direction="Column"
|
||||
gap="100"
|
||||
@@ -258,16 +259,9 @@ export function DeviceVerification({ request, onExit }: DeviceVerificationProps)
|
||||
<Dialog variant="Surface">
|
||||
<Header style={DialogHeaderStyles} variant="Surface" size="500">
|
||||
<Box grow="Yes">
|
||||
<Text as="h2" size="H4">
|
||||
Device Verification
|
||||
</Text>
|
||||
<Text size="H4">Device Verification</Text>
|
||||
</Box>
|
||||
<IconButton
|
||||
size="300"
|
||||
radii="300"
|
||||
onClick={handleCancel}
|
||||
aria-label="Cancel verification"
|
||||
>
|
||||
<IconButton size="300" radii="300" onClick={handleCancel}>
|
||||
<Icon src={Icons.Cross} />
|
||||
</IconButton>
|
||||
</Header>
|
||||
@@ -316,5 +310,9 @@ export function ReceiveSelfDeviceVerification() {
|
||||
|
||||
if (!request) return null;
|
||||
|
||||
if (!request.isSelfVerification) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return <DeviceVerification request={request} onExit={handleExit} />;
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@ import { useAlive } from '../hooks/useAlive';
|
||||
import { UseStateProvider } from './UseStateProvider';
|
||||
|
||||
type UIACallback<T> = (
|
||||
authDict: AuthDict | null,
|
||||
authDict: AuthDict | null
|
||||
) => Promise<[IAuthData, undefined] | [undefined, T]>;
|
||||
|
||||
type PerformAction<T> = (authDict: AuthDict | null) => Promise<T>;
|
||||
@@ -42,7 +42,7 @@ function makeUIAAction<T>(
|
||||
authData: IAuthData,
|
||||
performAction: PerformAction<T>,
|
||||
resolve: (data: T) => void,
|
||||
reject: (error?: any) => void,
|
||||
reject: (error?: any) => void
|
||||
): UIAAction<T> {
|
||||
const action: UIAAction<T> = {
|
||||
authData,
|
||||
@@ -91,7 +91,7 @@ function SetupVerification({ onComplete }: SetupVerificationProps) {
|
||||
setNextAuthData(authData);
|
||||
}
|
||||
},
|
||||
[uiaAction, alive],
|
||||
[uiaAction, alive]
|
||||
);
|
||||
|
||||
const resetUIA = useCallback(() => {
|
||||
@@ -118,7 +118,7 @@ function SetupVerification({ onComplete }: SetupVerificationProps) {
|
||||
(err) => {
|
||||
resetUIA();
|
||||
reject(err);
|
||||
},
|
||||
}
|
||||
);
|
||||
if (alive()) {
|
||||
setUIAAction(action);
|
||||
@@ -130,7 +130,7 @@ function SetupVerification({ onComplete }: SetupVerificationProps) {
|
||||
reject(error);
|
||||
});
|
||||
}),
|
||||
[alive, resetUIA],
|
||||
[alive, resetUIA]
|
||||
);
|
||||
|
||||
const [setupState, setup] = useAsyncCallback<void, Error, [string | undefined]>(
|
||||
@@ -159,8 +159,8 @@ function SetupVerification({ onComplete }: SetupVerificationProps) {
|
||||
|
||||
onComplete(recoveryKeyData.encodedPrivateKey);
|
||||
},
|
||||
[mx, onComplete, authUploadDeviceSigningKeys],
|
||||
),
|
||||
[mx, onComplete, authUploadDeviceSigningKeys]
|
||||
)
|
||||
);
|
||||
|
||||
const loading = setupState.status === AsyncStatus.Loading;
|
||||
@@ -299,11 +299,9 @@ export const DeviceVerificationSetup = forwardRef<HTMLDivElement, DeviceVerifica
|
||||
size="500"
|
||||
>
|
||||
<Box grow="Yes">
|
||||
<Text as="h2" size="H4">
|
||||
Setup Device Verification
|
||||
</Text>
|
||||
<Text size="H4">Setup Device Verification</Text>
|
||||
</Box>
|
||||
<IconButton size="300" radii="300" onClick={onCancel} aria-label="Cancel">
|
||||
<IconButton size="300" radii="300" onClick={onCancel}>
|
||||
<Icon src={Icons.Cross} />
|
||||
</IconButton>
|
||||
</Header>
|
||||
@@ -316,7 +314,7 @@ export const DeviceVerificationSetup = forwardRef<HTMLDivElement, DeviceVerifica
|
||||
</Box>
|
||||
</Dialog>
|
||||
);
|
||||
},
|
||||
}
|
||||
);
|
||||
type DeviceVerificationResetProps = {
|
||||
onCancel: () => void;
|
||||
@@ -336,11 +334,9 @@ export const DeviceVerificationReset = forwardRef<HTMLDivElement, DeviceVerifica
|
||||
size="500"
|
||||
>
|
||||
<Box grow="Yes">
|
||||
<Text as="h2" size="H4">
|
||||
Reset Device Verification
|
||||
</Text>
|
||||
<Text size="H4">Reset Device Verification</Text>
|
||||
</Box>
|
||||
<IconButton size="300" radii="300" onClick={onCancel} aria-label="Cancel">
|
||||
<IconButton size="300" radii="300" onClick={onCancel}>
|
||||
<Icon src={Icons.Cross} />
|
||||
</IconButton>
|
||||
</Header>
|
||||
@@ -375,5 +371,5 @@ export const DeviceVerificationReset = forwardRef<HTMLDivElement, DeviceVerifica
|
||||
)}
|
||||
</Dialog>
|
||||
);
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
@@ -1,126 +0,0 @@
|
||||
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 } 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: 'var(--bg-surface)',
|
||||
border: '1px solid rgba(255,255,255,0.08)',
|
||||
borderRadius: '12px',
|
||||
overflow: 'hidden',
|
||||
boxShadow: '0 8px 32px rgba(0,0,0,0.4)',
|
||||
width: `${PICKER_WIDTH}px`,
|
||||
};
|
||||
|
||||
return (
|
||||
<FocusTrap
|
||||
focusTrapOptions={{
|
||||
initialFocus: 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>
|
||||
</OverlayCenter>
|
||||
</Overlay>
|
||||
),
|
||||
)
|
||||
);
|
||||
|
||||
@@ -33,7 +33,7 @@ export const useJoinRuleIcons = (roomType?: string): JoinRuleIcons =>
|
||||
[JoinRule.Public]: getRoomIconSrc(Icons, roomType, JoinRule.Public),
|
||||
[JoinRule.Private]: getRoomIconSrc(Icons, roomType, JoinRule.Private),
|
||||
}),
|
||||
[roomType],
|
||||
[roomType]
|
||||
);
|
||||
|
||||
type JoinRuleLabels = Record<ExtendedJoinRules, string>;
|
||||
@@ -47,7 +47,7 @@ export const useRoomJoinRuleLabel = (): JoinRuleLabels =>
|
||||
[JoinRule.Public]: 'Public',
|
||||
[JoinRule.Private]: 'Invite Only',
|
||||
}),
|
||||
[],
|
||||
[]
|
||||
);
|
||||
|
||||
type JoinRulesSwitcherProps<T extends ExtendedJoinRules[]> = {
|
||||
@@ -79,7 +79,7 @@ export function JoinRulesSwitcher<T extends ExtendedJoinRules[]>({
|
||||
setCords(undefined);
|
||||
onChange(selectedRule);
|
||||
},
|
||||
[onChange],
|
||||
[onChange]
|
||||
);
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,118 +0,0 @@
|
||||
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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -21,13 +21,13 @@ export const LogoutDialog = forwardRef<HTMLDivElement, LogoutDialogProps>(
|
||||
const verificationStatus = useDeviceVerificationStatus(
|
||||
mx.getCrypto(),
|
||||
mx.getSafeUserId(),
|
||||
mx.getDeviceId() ?? undefined,
|
||||
mx.getDeviceId() ?? undefined
|
||||
);
|
||||
|
||||
const [logoutState, logout] = useAsyncCallback<void, Error, []>(
|
||||
useCallback(async () => {
|
||||
await logoutClient(mx);
|
||||
}, [mx]),
|
||||
}, [mx])
|
||||
);
|
||||
|
||||
const ongoingLogout = logoutState.status === AsyncStatus.Loading;
|
||||
@@ -43,9 +43,7 @@ export const LogoutDialog = forwardRef<HTMLDivElement, LogoutDialogProps>(
|
||||
size="500"
|
||||
>
|
||||
<Box grow="Yes">
|
||||
<Text as="h2" size="H4">
|
||||
Logout
|
||||
</Text>
|
||||
<Text size="H4">Logout</Text>
|
||||
</Box>
|
||||
</Header>
|
||||
<Box style={{ padding: config.space.S400 }} direction="Column" gap="400">
|
||||
@@ -87,5 +85,5 @@ export const LogoutDialog = forwardRef<HTMLDivElement, LogoutDialogProps>(
|
||||
</Box>
|
||||
</Dialog>
|
||||
);
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
@@ -126,7 +126,7 @@ export function ManualVerificationTile({
|
||||
const [method, setMethod] = useState(
|
||||
hasPassphrase
|
||||
? ManualVerificationMethod.RecoveryPassphrase
|
||||
: ManualVerificationMethod.RecoveryKey,
|
||||
: ManualVerificationMethod.RecoveryKey
|
||||
);
|
||||
|
||||
const verifyAndRestoreBackup = useCallback(
|
||||
@@ -143,11 +143,11 @@ export function ManualVerificationTile({
|
||||
|
||||
await crypto.loadSessionBackupPrivateKeyFromSecretStorage();
|
||||
},
|
||||
[mx, secretStorageKeyId],
|
||||
[mx, secretStorageKeyId]
|
||||
);
|
||||
|
||||
const [verifyState, handleDecodedRecoveryKey] = useAsyncCallback<void, Error, [Uint8Array]>(
|
||||
verifyAndRestoreBackup,
|
||||
verifyAndRestoreBackup
|
||||
);
|
||||
const verifying = verifyState.status === AsyncStatus.Loading;
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ export function MediaConfigLoader({ children }: MediaConfigLoaderProps) {
|
||||
const [state, load] = useAsyncCallback(useCallback(() => mx.getMediaConfig(), [mx]));
|
||||
|
||||
useEffect(() => {
|
||||
load().catch(() => {});
|
||||
load();
|
||||
}, [load]);
|
||||
|
||||
return children(state.status === AsyncStatus.Success ? state.data : undefined);
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
import React from 'react';
|
||||
import { 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 color =
|
||||
vs === 'verified' ? 'var(--tc-positive-normal, #5effc4)' : 'var(--tc-warning-normal, #ffcc55)';
|
||||
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 }} />
|
||||
</span>
|
||||
)}
|
||||
</TooltipProvider>
|
||||
);
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
/* eslint-disable no-param-reassign */
|
||||
/* eslint-disable jsx-a11y/no-noninteractive-element-interactions */
|
||||
import React, { FormEventHandler, MouseEventHandler, useEffect, useRef, useState } from 'react';
|
||||
import classNames from 'classnames';
|
||||
import {
|
||||
@@ -41,7 +43,7 @@ export const PdfViewer = as<'div', PdfViewerProps>(
|
||||
const [pdfJSState, loadPdfJS] = usePdfJSLoader();
|
||||
const [docState, loadPdfDocument] = usePdfDocumentLoader(
|
||||
pdfJSState.status === AsyncStatus.Success ? pdfJSState.data : undefined,
|
||||
src,
|
||||
src
|
||||
);
|
||||
const isLoading =
|
||||
pdfJSState.status === AsyncStatus.Loading || docState.status === AsyncStatus.Loading;
|
||||
@@ -106,7 +108,7 @@ export const PdfViewer = as<'div', PdfViewerProps>(
|
||||
<Box className={classNames(css.PdfViewer, className)} direction="Column" {...props} ref={ref}>
|
||||
<Header className={css.PdfViewerHeader} size="400">
|
||||
<Box grow="Yes" alignItems="Center" gap="200">
|
||||
<IconButton size="300" radii="300" onClick={requestClose} aria-label="Close">
|
||||
<IconButton size="300" radii="300" onClick={requestClose}>
|
||||
<Icon size="50" src={Icons.ArrowLeft} />
|
||||
</IconButton>
|
||||
<Text size="T300" truncate>
|
||||
@@ -255,5 +257,5 @@ export const PdfViewer = as<'div', PdfViewerProps>(
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
@@ -22,7 +22,6 @@ import {
|
||||
RenderBody,
|
||||
ThumbnailContent,
|
||||
UnsupportedContent,
|
||||
VerificationRequestContent,
|
||||
VideoContent,
|
||||
} from './message';
|
||||
import { UrlPreviewCard, UrlPreviewHolder } from './url-preview';
|
||||
@@ -38,7 +37,6 @@ type RenderMessageContentProps = {
|
||||
msgType: string;
|
||||
ts: number;
|
||||
edited?: boolean;
|
||||
onEditHistoryClick?: () => void;
|
||||
getContent: <T>() => T;
|
||||
mediaAutoLoad?: boolean;
|
||||
urlPreview?: boolean;
|
||||
@@ -46,14 +44,12 @@ type RenderMessageContentProps = {
|
||||
htmlReactParserOptions: HTMLReactParserOptions;
|
||||
linkifyOpts: Opts;
|
||||
outlineAttachment?: boolean;
|
||||
eventId?: string;
|
||||
};
|
||||
export function RenderMessageContent({
|
||||
displayName,
|
||||
msgType,
|
||||
ts,
|
||||
edited,
|
||||
onEditHistoryClick,
|
||||
getContent,
|
||||
mediaAutoLoad,
|
||||
urlPreview,
|
||||
@@ -61,7 +57,6 @@ export function RenderMessageContent({
|
||||
htmlReactParserOptions,
|
||||
linkifyOpts,
|
||||
outlineAttachment,
|
||||
eventId,
|
||||
}: RenderMessageContentProps) {
|
||||
const renderUrlsPreview = (urls: string[]) => {
|
||||
const filteredUrls = urls.filter((url) => !testMatrixTo(url));
|
||||
@@ -81,7 +76,6 @@ export function RenderMessageContent({
|
||||
<MText
|
||||
style={{ marginTop: config.space.S200 }}
|
||||
edited={edited}
|
||||
onEditHistoryClick={onEditHistoryClick}
|
||||
content={content}
|
||||
renderBody={(props) => (
|
||||
<RenderBody
|
||||
@@ -138,7 +132,6 @@ export function RenderMessageContent({
|
||||
return (
|
||||
<MText
|
||||
edited={edited}
|
||||
onEditHistoryClick={onEditHistoryClick}
|
||||
content={getContent()}
|
||||
renderBody={(props) => (
|
||||
<RenderBody
|
||||
@@ -149,7 +142,6 @@ export function RenderMessageContent({
|
||||
/>
|
||||
)}
|
||||
renderUrlsPreview={urlPreview ? renderUrlsPreview : undefined}
|
||||
eventId={eventId}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -159,7 +151,6 @@ export function RenderMessageContent({
|
||||
<MEmote
|
||||
displayName={displayName}
|
||||
edited={edited}
|
||||
onEditHistoryClick={onEditHistoryClick}
|
||||
content={getContent()}
|
||||
renderBody={(props) => (
|
||||
<RenderBody
|
||||
@@ -170,7 +161,6 @@ export function RenderMessageContent({
|
||||
/>
|
||||
)}
|
||||
renderUrlsPreview={urlPreview ? renderUrlsPreview : undefined}
|
||||
eventId={eventId}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -179,7 +169,6 @@ export function RenderMessageContent({
|
||||
return (
|
||||
<MNotice
|
||||
edited={edited}
|
||||
onEditHistoryClick={onEditHistoryClick}
|
||||
content={getContent()}
|
||||
renderBody={(props) => (
|
||||
<RenderBody
|
||||
@@ -190,7 +179,6 @@ export function RenderMessageContent({
|
||||
/>
|
||||
)}
|
||||
renderUrlsPreview={urlPreview ? renderUrlsPreview : undefined}
|
||||
eventId={eventId}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -276,9 +264,5 @@ export function RenderMessageContent({
|
||||
return <MBadEncrypted />;
|
||||
}
|
||||
|
||||
if (msgType === 'm.key.verification.request') {
|
||||
return <VerificationRequestContent />;
|
||||
}
|
||||
|
||||
return <UnsupportedContent />;
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ const useRoomNotificationModes = (): RoomNotificationMode[] =>
|
||||
RoomNotificationMode.SpecialMessages,
|
||||
RoomNotificationMode.Mute,
|
||||
],
|
||||
[],
|
||||
[]
|
||||
);
|
||||
|
||||
const useRoomNotificationModeStr = (): Record<RoomNotificationMode, string> =>
|
||||
@@ -28,7 +28,7 @@ const useRoomNotificationModeStr = (): Record<RoomNotificationMode, string> =>
|
||||
[RoomNotificationMode.SpecialMessages]: 'Mention & Keywords',
|
||||
[RoomNotificationMode.Mute]: 'Mute',
|
||||
}),
|
||||
[],
|
||||
[]
|
||||
);
|
||||
|
||||
type NotificationModeSwitcherProps = {
|
||||
@@ -37,7 +37,7 @@ type NotificationModeSwitcherProps = {
|
||||
children: (
|
||||
handleOpen: MouseEventHandler<HTMLButtonElement>,
|
||||
opened: boolean,
|
||||
changing: boolean,
|
||||
changing: boolean
|
||||
) => ReactNode;
|
||||
};
|
||||
export function RoomNotificationModeSwitcher({
|
||||
|
||||
@@ -1,125 +0,0 @@
|
||||
import React, { useId } from 'react';
|
||||
|
||||
const MESSAGES = [
|
||||
{ showAvatar: true, lines: [{ w: '55%' }, { w: '35%' }] },
|
||||
{ showAvatar: false, lines: [{ w: '72%' }] },
|
||||
{ showAvatar: false, lines: [{ w: '48%' }, { w: '60%' }] },
|
||||
{ showAvatar: true, lines: [{ w: '80%' }] },
|
||||
{ showAvatar: false, lines: [{ w: '40%' }] },
|
||||
{ showAvatar: true, lines: [{ w: '65%' }, { w: '50%' }, { w: '30%' }] },
|
||||
{ showAvatar: false, lines: [{ w: '58%' }] },
|
||||
{ showAvatar: true, lines: [{ w: '45%' }] },
|
||||
{ showAvatar: false, lines: [{ w: '70%' }, { w: '25%' }] },
|
||||
];
|
||||
|
||||
export function RoomSkeleton() {
|
||||
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',
|
||||
// CSS vars resolve against both light and dark themes automatically
|
||||
'--skeleton-base': 'color-mix(in srgb, currentColor 8%, transparent)',
|
||||
'--skeleton-shine': 'color-mix(in srgb, currentColor 15%, transparent)',
|
||||
} as React.CSSProperties
|
||||
}
|
||||
>
|
||||
{/* Header — matches PageHeader size="600" (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,
|
||||
}}
|
||||
>
|
||||
{/* Avatar */}
|
||||
<div
|
||||
style={{
|
||||
...shimmer,
|
||||
width: '32px',
|
||||
height: '32px',
|
||||
borderRadius: '50%',
|
||||
flexShrink: 0,
|
||||
}}
|
||||
/>
|
||||
{/* Room name */}
|
||||
<div style={{ ...shimmer, width: '140px', height: '16px' }} />
|
||||
{/* Spacer */}
|
||||
<div style={{ flex: 1 }} />
|
||||
{/* Icon buttons */}
|
||||
<div style={{ ...shimmer, width: '24px', height: '24px', borderRadius: '4px' }} />
|
||||
<div style={{ ...shimmer, width: '24px', height: '24px', borderRadius: '4px' }} />
|
||||
</div>
|
||||
|
||||
{/* Timeline */}
|
||||
<div style={{ flex: 1, overflowY: 'hidden', padding: '16px 0' }}>
|
||||
{MESSAGES.map((msg, i) => (
|
||||
<div
|
||||
key={i}
|
||||
style={{
|
||||
display: 'flex',
|
||||
gap: '12px',
|
||||
padding: '4px 16px',
|
||||
alignItems: 'flex-start',
|
||||
marginBottom: msg.showAvatar ? '8px' : '2px',
|
||||
}}
|
||||
>
|
||||
{/* Avatar — only shown on first message in a group */}
|
||||
<div style={{ width: '36px', flexShrink: 0 }}>
|
||||
{msg.showAvatar && (
|
||||
<div style={{ ...shimmer, width: '36px', height: '36px', borderRadius: '50%' }} />
|
||||
)}
|
||||
</div>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '6px', flex: 1 }}>
|
||||
{/* Username on first in group */}
|
||||
{msg.showAvatar && (
|
||||
<div style={{ ...shimmer, width: '90px', height: '12px', marginBottom: '2px' }} />
|
||||
)}
|
||||
{msg.lines.map((line, j) => (
|
||||
<div key={j} style={{ ...shimmer, width: line.w, height: '14px' }} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Input bar */}
|
||||
<div
|
||||
style={{
|
||||
borderTop: '1px solid color-mix(in srgb, currentColor 10%, transparent)',
|
||||
padding: '12px 16px',
|
||||
flexShrink: 0,
|
||||
}}
|
||||
>
|
||||
<div style={{ ...shimmer, width: '100%', height: '44px', borderRadius: '8px' }} />
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -36,7 +36,7 @@ export function SecretStorageRecoveryPassphrase({
|
||||
passphrase,
|
||||
salt,
|
||||
iterations,
|
||||
bits,
|
||||
bits
|
||||
);
|
||||
|
||||
const match = await mx.secretStorage.checkKey(decodedRecoveryKey, keyContent as any);
|
||||
@@ -47,8 +47,8 @@ export function SecretStorageRecoveryPassphrase({
|
||||
|
||||
return decodedRecoveryKey;
|
||||
},
|
||||
[mx, keyContent],
|
||||
),
|
||||
[mx, keyContent]
|
||||
)
|
||||
);
|
||||
|
||||
const drivingKey = driveKeyState.status === AsyncStatus.Loading;
|
||||
@@ -80,7 +80,6 @@ export function SecretStorageRecoveryPassphrase({
|
||||
<Text size="L400">Recovery Passphrase</Text>
|
||||
<PasswordInput
|
||||
name="recoveryPassphraseInput"
|
||||
aria-label="Recovery passphrase"
|
||||
size="400"
|
||||
variant="Secondary"
|
||||
radii="300"
|
||||
@@ -140,8 +139,8 @@ export function SecretStorageRecoveryKey({
|
||||
|
||||
return decodedRecoveryKey;
|
||||
},
|
||||
[mx, keyContent],
|
||||
),
|
||||
[mx, keyContent]
|
||||
)
|
||||
);
|
||||
|
||||
const drivingKey = driveKeyState.status === AsyncStatus.Loading;
|
||||
@@ -171,7 +170,6 @@ export function SecretStorageRecoveryKey({
|
||||
<Text size="L400">Recovery Key</Text>
|
||||
<PasswordInput
|
||||
name="recoveryKeyInput"
|
||||
aria-label="Recovery key"
|
||||
size="400"
|
||||
variant="Secondary"
|
||||
radii="300"
|
||||
|
||||
@@ -31,14 +31,10 @@ export function ServerConfigsLoader({ children }: ServerConfigsLoaderProps) {
|
||||
const authMetadata = promiseFulfilledResult(result[2]);
|
||||
let validatedAuthMetadata: ValidatedAuthMetadata | undefined;
|
||||
|
||||
// Only validate if the server returned metadata — a rejected promise means no native
|
||||
// Matrix OIDC (MSC3861/MAS), which is normal for servers using traditional SSO.
|
||||
if (authMetadata !== undefined) {
|
||||
try {
|
||||
validatedAuthMetadata = validateAuthMetadata(authMetadata);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
try {
|
||||
validatedAuthMetadata = validateAuthMetadata(authMetadata);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -46,7 +42,7 @@ export function ServerConfigsLoader({ children }: ServerConfigsLoaderProps) {
|
||||
mediaConfig,
|
||||
authMetadata: validatedAuthMetadata,
|
||||
};
|
||||
}, [mx]),
|
||||
}, [mx])
|
||||
);
|
||||
|
||||
const configs: ServerConfigs =
|
||||
|
||||
@@ -21,7 +21,7 @@ export function SpaceChildDirectsProvider({
|
||||
const childDirects = useSpaceChildren(
|
||||
allRoomsAtom,
|
||||
spaceId,
|
||||
useChildDirectScopeFactory(mx, mDirects, roomToParents),
|
||||
useChildDirectScopeFactory(mx, mDirects, roomToParents)
|
||||
);
|
||||
|
||||
return children(childDirects);
|
||||
|
||||
@@ -21,7 +21,7 @@ export function SpaceChildRoomsProvider({
|
||||
const childRooms = useSpaceChildren(
|
||||
allRoomsAtom,
|
||||
spaceId,
|
||||
useChildRoomScopeFactory(mx, mDirects, roomToParents),
|
||||
useChildRoomScopeFactory(mx, mDirects, roomToParents)
|
||||
);
|
||||
|
||||
return children(childRooms);
|
||||
|
||||