forked from cory/tildefriends
Compare commits
1304 Commits
f0772f9b99
...
v0.2025.11
| Author | SHA1 | Date | |
|---|---|---|---|
| ba8253fa30 | |||
| f5bd389183 | |||
| 0c34a38e15 | |||
| 7c7857a6cd | |||
| 716bce2bb0 | |||
| 33fb96b120 | |||
| 28a4accabf | |||
| 31c7394c17 | |||
| e2974d34e2 | |||
| 4a06c84511 | |||
| 4960a1d9d6 | |||
| 75dd8889e9 | |||
| 111a6c3c6e | |||
| 775fdafa63 | |||
| dae38bbd83 | |||
| 35f374047a | |||
| aea4a14a62 | |||
| 98f7504a4c | |||
| bb52cdd7c2 | |||
| 07b660a0d6 | |||
| 2b9d712d48 | |||
| 3c1f60b62d | |||
| bb75edfd42 | |||
| c2b61cec2c | |||
| 05c3107b27 | |||
| bb67df7846 | |||
| 89ec523ea2 | |||
| f30458d953 | |||
| 42df0d830e | |||
| 50b2c0c7f4 | |||
| 0edb76b678 | |||
| 2d71af3243 | |||
| b571cd213b | |||
| b52c79ac4e | |||
| 63c6a5ab07 | |||
| 4447ea63e2 | |||
| 61200c4a7d | |||
| 62dc9d6cc0 | |||
| a28d41e1ee | |||
| 6a05e3770b | |||
| 53a93e510c | |||
| 1cb3ecf1ea | |||
| 687665cd6b | |||
| 7879ab1d50 | |||
| 24f0cdb398 | |||
| 6d5555e596 | |||
| 4052e3235f | |||
| 13302ad1c7 | |||
| 0f8687e473 | |||
| 9399ccd684 | |||
| b3604039fa | |||
| 732089da2c | |||
| b3e7e4b196 | |||
| 09a0cfd349 | |||
| 5bf7346321 | |||
| dd558c57e0 | |||
| 5647196924 | |||
| 49b1834bc6 | |||
| d111647ea8 | |||
| d7580dab9b | |||
| 28db8a8d5f | |||
| e64d5617e7 | |||
| acd114650a | |||
| 7a47ffaa61 | |||
| 42f7f66f35 | |||
| b2b4ffeeae | |||
| 26de1f7daa | |||
| 07605933dc | |||
| 4bdc7ec616 | |||
| 8ca64550e5 | |||
| 25dbac804c | |||
| 6ab3fd168b | |||
| 94858e2371 | |||
| 6d13502e94 | |||
| 77001e595c | |||
| 6fad20ffa3 | |||
| 00fb6c9839 | |||
| 97fcf72d63 | |||
| 5d8d02515d | |||
| 859fe1feb0 | |||
| 8f61d83f41 | |||
| 6423b3e479 | |||
| 2bc8cec8a2 | |||
| b49a6cd685 | |||
| 2885380f40 | |||
| 2ec3b6a249 | |||
| 3ef795452d | |||
| 479d87c8b8 | |||
| a56077dcc7 | |||
| d3f4587c3b | |||
| 623705b7a1 | |||
| 8f87f4751d | |||
| 2ac6dfde9d | |||
| 81ade7a400 | |||
| 63f7ff9f27 | |||
| 8a0fa17a79 | |||
| 0ead5ed967 | |||
| 53261a6fbc | |||
| c60ff86a4d | |||
| 83a0b017c5 | |||
| 3746622a11 | |||
| ccd50cf59f | |||
| 93680eb43d | |||
| 3ae4b7086a | |||
| 446b1f8600 | |||
| 00fd208a2c | |||
| e574d03716 | |||
| c1f3116c9d | |||
| 3aec7e6c14 | |||
| 9f0020dec8 | |||
| 6e78ad9729 | |||
| 44d84a9b2a | |||
| ac7809415c | |||
| 675cecaa20 | |||
| 5d179cc088 | |||
| d905618590 | |||
| 3fd9bc0b18 | |||
| 39abee7f73 | |||
| b770619111 | |||
| 1c44857da4 | |||
| bca4440867 | |||
| 4855543961 | |||
| cb3d6a98b9 | |||
| ada67a13d3 | |||
| f4c928f26e | |||
| 91fd515d39 | |||
| be6e841d3d | |||
| af6afa6903 | |||
| 6ab5d2a28d | |||
| 4be033f288 | |||
| c550f92003 | |||
| ed836b3ee0 | |||
| ac7a43abf4 | |||
| 49f19fce91 | |||
| b2197eb8e9 | |||
| 5fbc2cae1c | |||
| 730abb49ce | |||
| edccab054a | |||
| e8210c6fdd | |||
| 55d69d7c13 | |||
| 50fb18d4ff | |||
| 77b1ea1fc8 | |||
| 61501a9b64 | |||
| c1507adac5 | |||
| 45fb9eda1c | |||
| 982a61f4bf | |||
| 18e5b41663 | |||
| 910c39cbd0 | |||
| 9952dfd49d | |||
| 00f75d5382 | |||
| d78828554b | |||
| b84b561109 | |||
| a618815500 | |||
| e1f3dc6ae4 | |||
| f378db6c6f | |||
| 7cec0f7d61 | |||
| f902d0374c | |||
| b5f0a0c4f7 | |||
| 00623cea09 | |||
| ed4f1d6f2c | |||
| 73f4a3407f | |||
| 6f11318e84 | |||
| e88ee91f0e | |||
| 3f8daf257c | |||
| dc387acadc | |||
| 68aa41ab96 | |||
| 85b23437b3 | |||
| c59fba817d | |||
| c3415ab75c | |||
| f1d0151d71 | |||
| 3c5c1756d1 | |||
| 6a6b65d1b3 | |||
| 81bd54dbe6 | |||
| 6a1bb0d3bc | |||
| 705e8b553f | |||
| e4729b22f2 | |||
| 662112551a | |||
| 38fe88aab8 | |||
| 578c51faa0 | |||
| a3ccc73b81 | |||
| 7312f4d43a | |||
| 8b546c7e02 | |||
| c0b6ff2e64 | |||
| 638b7cc1e5 | |||
| 05e54e1be0 | |||
| 4c3299ead0 | |||
| 1ef56b35ad | |||
| 061e79c295 | |||
| 5edfe732b1 | |||
| a8f9b67f71 | |||
| de7fbf1eb7 | |||
| a51a3d7e43 | |||
| 433b3b1003 | |||
| 6703c5b584 | |||
| 5f729efabe | |||
| b2085b3f28 | |||
| 2f893494b0 | |||
| e26af21f63 | |||
| 7e1d738f8d | |||
| 199448e11e | |||
| fdaabab807 | |||
| ca4560c5c9 | |||
| 2478f3064d | |||
| e9b8b43e7c | |||
| 951155f1b6 | |||
| 1b678175ef | |||
| 8eb1f40eec | |||
| 235887b3bf | |||
| 0b3d66dd48 | |||
| beb9ef3754 | |||
| 9f6a480736 | |||
| b3bac2927d | |||
| ef389f2ba2 | |||
| ef21dc6ae8 | |||
| 6e55b6b49e | |||
| db115ef1bd | |||
| 678838dbd5 | |||
| 586f87625d | |||
| 1542370f9b | |||
| 1f7d5968c7 | |||
| 39e51f7790 | |||
| 052663efbe | |||
| 8f84ff2611 | |||
| 37e1c5d97b | |||
| cef526bcf3 | |||
| 6af36cafa9 | |||
| fca859d93d | |||
| 2178300d8d | |||
| 636bdcce6b | |||
| 94b7703ca9 | |||
| a391dd1316 | |||
| b6ba5211b7 | |||
| 8e8e130045 | |||
| 1f40bc1a0f | |||
| 5437212222 | |||
| a8ab845cd2 | |||
| 8cee6dc98b | |||
| 70c2b73414 | |||
| 98013c4422 | |||
| e9e22b762d | |||
| 620db19936 | |||
| 94a79dd62c | |||
| b56c3efde0 | |||
| 066827f8f1 | |||
| c3b65d9cd8 | |||
| a15b916b06 | |||
| 31d0a5c233 | |||
| 140179e80a | |||
| 53cba2d7e4 | |||
| e54312d3b8 | |||
| cadc27b7b5 | |||
| 388b829ec1 | |||
| 67861f0f33 | |||
| a1f1eb34d5 | |||
| 2a6789063e | |||
| cbf1273a55 | |||
| 8143a23ced | |||
| 3c17810747 | |||
| bea7a2e9ed | |||
| 2f0a2ac6b0 | |||
| 18908b6b56 | |||
| b135a210cc | |||
| 3a2a829940 | |||
| e56dd2dd2d | |||
| 3f41a48bc7 | |||
| 65ed53281a | |||
| 1121557a2e | |||
| d4a7b86ee7 | |||
| 626c18b04e | |||
| bfa97ed7c7 | |||
| deae4d5367 | |||
| 899605c860 | |||
| dc9a279991 | |||
| 2a53892581 | |||
| 6bef0eb764 | |||
| 462b40640c | |||
| 72e1b2025c | |||
| fc7c4b1257 | |||
| 6c22c59056 | |||
| 94c2b1184f | |||
| 45231d703d | |||
| 7882fcbe8f | |||
| 3bbc8c4d35 | |||
| 8ae10dc80b | |||
| 9b11c2c629 | |||
| e2a231fb4a | |||
| 8a9502d1f2 | |||
| 534438df63 | |||
| 45a4feec96 | |||
| aa7a32395e | |||
| ab9f57f044 | |||
| 4040d6aa08 | |||
| 1c96f5c35e | |||
| 4d3e42812d | |||
| f7b3711d4f | |||
| 2408e076ff | |||
| 6f71ffb477 | |||
| 214433f36a | |||
| 309b22732e | |||
| 6fe7687b2a | |||
| a8cbf757ff | |||
| 4a4bedfe2b | |||
| 051291f725 | |||
| d2b338095f | |||
| 899827a8f2 | |||
| 5fcbe3d6a9 | |||
| a0a40e6cb2 | |||
| bb1190e3f8 | |||
| 0a3baed1da | |||
| 4931c489ed | |||
| 996f9abaa2 | |||
| 08c097e176 | |||
| daa861a98b | |||
| a25d08fd76 | |||
| 392d31cc53 | |||
| 92926fa8df | |||
| 61ae9ae465 | |||
| 89622697d5 | |||
| 17694f5646 | |||
| 5a1303149f | |||
| 8a0e190a86 | |||
| 0d7dfd8c9e | |||
| f979ff7050 | |||
| e3fcdea362 | |||
| 476fec2757 | |||
| 53c215399b | |||
| 2c330802da | |||
| 851d7046ea | |||
| c0019d7246 | |||
| 7688e4d3a8 | |||
| ef58749ce3 | |||
| 35941a7ddc | |||
| 1f2664e5a8 | |||
| 35656a5c34 | |||
| 799f22e989 | |||
| e226a37251 | |||
| 8e3bc9d700 | |||
| 58c3e6c2ab | |||
| 0dc148bfea | |||
| 3eff1b08a9 | |||
| 02d789471f | |||
| d367d47c4d | |||
| c93b8fc045 | |||
| eb9377e21d | |||
| a1764eee42 | |||
| 86ef74e20d | |||
| 4de53b9926 | |||
| 99a195a3fd | |||
| f1ced31f69 | |||
| b3cedf2baa | |||
| 3bf19fabda | |||
| cf81ebe8ad | |||
| 278b5566a1 | |||
| e8c1390f09 | |||
| 3c04abda45 | |||
| 2597f99ccf | |||
| 9d3a07c1cf | |||
| bdfd8925b5 | |||
| 1a4d1985f4 | |||
| 6273f3ea53 | |||
| 5bdc6fa471 | |||
| 3ba41291db | |||
| 0867811952 | |||
| 8d961cd805 | |||
| 97cea7b40b | |||
| 4106834db8 | |||
| a4a8f7cab2 | |||
| 9e209ee800 | |||
| ddfa84f040 | |||
| 6b3a6ec7c1 | |||
| 4d037c02bf | |||
| deaeab10d8 | |||
| 2a5375b1e7 | |||
| e7a03e3283 | |||
| efb3a12dcc | |||
| 3830d695d7 | |||
| f36620927b | |||
| 5423cbd628 | |||
| abde709e54 | |||
| 27f2d319ab | |||
| 66234b14bc | |||
| 6a9167e565 | |||
| 3c60f8ca06 | |||
| c26bf5c112 | |||
| 41cbde934a | |||
| 946941d95e | |||
| 50f0104239 | |||
| 40fa7edadf | |||
| d6926569c6 | |||
| a8bba324ca | |||
| 5bba5776b3 | |||
| 8104f6f228 | |||
| 3f4738e593 | |||
| 1516e17f5d | |||
| 676d2702b7 | |||
| 5d39548964 | |||
| 67d458bd38 | |||
| d9684c7d62 | |||
| 5a818d2119 | |||
| 6f96d4ce65 | |||
| f72395756a | |||
| 38d746b310 | |||
| f7270987ea | |||
| 6f565c0f0a | |||
| 7f252e79b6 | |||
| ba2bb17638 | |||
| bc7c658293 | |||
| 4d84e69bb5 | |||
| 03fac74908 | |||
| 5252ff1ecf | |||
| 20100d3fd4 | |||
| dd3b2656ad | |||
| 657f25e22b | |||
| 8be354fc49 | |||
| e574758340 | |||
| 40cf519492 | |||
| 0e4fda54e9 | |||
| 868f91e1ef | |||
| b9000c154f | |||
| 894c72a82f | |||
| c128cfc25c | |||
| 36c88b463c | |||
| 8a66e74074 | |||
| ea60b165da | |||
| 1011e0026b | |||
| 9462521287 | |||
| 576022c41a | |||
| 70c38b7ea8 | |||
| 36370f2dea | |||
| db9bf7f7bd | |||
| fa7aef0c37 | |||
| b135ea17f6 | |||
| 4b1643bc47 | |||
| 240a8ce9c7 | |||
| 8928e8722b | |||
| d692734e55 | |||
| 50197198b4 | |||
| 1ee1107c93 | |||
| cf90533b6c | |||
| f0211f621e | |||
| d9693af89b | |||
| 13722232fb | |||
| 0bcb033349 | |||
| e92c439724 | |||
| 7f34b585d3 | |||
| d7e9fd918a | |||
| 9899c0c5e2 | |||
| c50de0b0f0 | |||
| bb7d2d7ae0 | |||
| 862d172ca8 | |||
| 3671051d0e | |||
| 223e20cbbc | |||
| 9af4312561 | |||
| 934e40240e | |||
| edb1980387 | |||
| bb7b04013f | |||
| 26a3007268 | |||
| 5de2b09596 | |||
| 3660577a23 | |||
| 98b4c7cf04 | |||
| 427a7b8d25 | |||
| 67b84830cd | |||
| 973cd53266 | |||
| 1afdbe6932 | |||
| 942f582329 | |||
| 951a80389a | |||
| b7ecfc9925 | |||
| 59e389d793 | |||
| 2ec047cc00 | |||
| ee33f54745 | |||
| 7a79534ca8 | |||
| a74a9fc821 | |||
| e2c388b9db | |||
| 0f573ce09e | |||
| bc70e41b7c | |||
| f500e14aa3 | |||
| 8b47938238 | |||
| 8912212d8e | |||
| 6a346bf940 | |||
| b5bdae4611 | |||
| 6590da5793 | |||
| eb2b426ec7 | |||
| 4864a0411f | |||
| 68590cae33 | |||
| abf2bbaec2 | |||
| 0da7e2722f | |||
| 60d4b06057 | |||
| 4c3df34950 | |||
| 7737e60b52 | |||
| 71e816bc13 | |||
| c74f90ef04 | |||
| 26cb7e5a17 | |||
| 2bad6672d8 | |||
| 71c4011526 | |||
| 5e81078f59 | |||
| 31b78e74df | |||
| 2ff689aab0 | |||
| 0a6f0ed3f7 | |||
| bfec46673d | |||
| 3a16614c72 | |||
| 9c9efb845c | |||
| 6192f1b94d | |||
| ce451b2449 | |||
| 8534e16469 | |||
| f6cc6f2eae | |||
| 0904425221 | |||
| a729886522 | |||
| e5dfedc7d1 | |||
| f02423d084 | |||
| 8f4b6e83eb | |||
| 3029919553 | |||
| ac67db0591 | |||
| 1e08838f5b | |||
|
d814f7ee77
|
|||
| 7edfb9d386 | |||
| 6928d6caba | |||
| 1a626875cf | |||
| 6eb3b64334 | |||
| 09f3595e93 | |||
| 11e89622d4 | |||
| 0fa8acc264 | |||
| afc7c64ed8 | |||
| c794c1b885 | |||
| 6247529799 | |||
| ad3eedc1fb | |||
| 1cfac3cae6 | |||
| 478bcd5d13 | |||
| 6932da69b3 | |||
| 857f47bf55 | |||
| 373d742751 | |||
| 575622c522 | |||
| a3e86ccb1d | |||
| e491798ff1 | |||
| 15df4ac236 | |||
| 017a74c4e4 | |||
| 6e5b1127f3 | |||
| 95f4f88949 | |||
| 92858882d7 | |||
| 4cba95ec8c | |||
| bbb2f89d85 | |||
| 88213038b2 | |||
| cc18b41e76 | |||
| f16127a238 | |||
| 2a6ecfaede | |||
| cf4a09bf03 | |||
| 20e60262fd | |||
| 9e3928c976 | |||
| 95d8768545 | |||
| 8679d09040 | |||
| 4cc5c6acb3 | |||
| 6f9b548b1a | |||
| fbff3386a9 | |||
| e5899fca58 | |||
| dddec489b9 | |||
| b4049eaeaa | |||
| b3fd724b5a | |||
| aca25be86a | |||
| 0f8cbdac57 | |||
| 630219d667 | |||
| fef268e434 | |||
| e18dcf3a48 | |||
| e019320146 | |||
| 65b31e14f9 | |||
| 9cb5cbcde9 | |||
| 25914ff5a7 | |||
| c4af799279 | |||
| 3f8f0e14f4 | |||
| 5414b30e7f | |||
| 7aee897c1b | |||
| 4060f9cc11 | |||
| 5b526cbf5b | |||
| f5065ff42b | |||
| 86b5546f5f | |||
| 43ae2a293b | |||
| b8ddbd4255 | |||
| 87cdba1db8 | |||
| df83187e33 | |||
| eccdbf29ab | |||
| 28d181f8bc | |||
| d386daf2ff | |||
| 0c1e116c1e | |||
| bb0ed67827 | |||
| b111d06851 | |||
| 79388845ea | |||
| 99faef2e77 | |||
| 21fffbfe10 | |||
| 64fb9f0c2a | |||
| a42e0bef2c | |||
| 45a09006e1 | |||
| 240484be4c | |||
| 22f4d115e3 | |||
| 32920e0e5d | |||
| f03a5918d1 | |||
| dd1870b52a | |||
| f0c1a8f98f | |||
| 0c181d7e7c | |||
| e3dc0e833a | |||
| 6de875edea | |||
| 986a55173f | |||
| 59c8cabf02 | |||
| a33b9fab07 | |||
| f8a725e1e7 | |||
| b3ab3af01b | |||
| 79f9463e56 | |||
| 4257b2ed51 | |||
| 6488ab60ec | |||
| 18bd3dfcf9 | |||
| ec114e160d | |||
| de033af18f | |||
| e971c6fcb7 | |||
| d3c465391c | |||
| f1fa19593d | |||
| 60b6f9c73e | |||
| 55b95ddecb | |||
| 0800a251b2 | |||
| 82cf7a80eb | |||
| 379f3d12eb | |||
| e52972d4d4 | |||
| 1a0ca4dec2 | |||
| 0bac9d8d5a | |||
| a9608363c5 | |||
| f1a2c5ae8e | |||
| 192a81ede7 | |||
| 916aa5abbd | |||
| d4a5cc6eee | |||
| d19605cc8d | |||
| 8d529327a4 | |||
| 697e2f2071 | |||
| f7fb112f21 | |||
| b2c0211190 | |||
| c59e0ea6e5 | |||
| 6c2fc1444d | |||
| 94d969fe46 | |||
| a7bc3d301c | |||
| 18bab849f7 | |||
| 04878fcc30 | |||
| ffe1299548 | |||
| 64bdbf5725 | |||
| b82deb557f | |||
| d529a48a11 | |||
| b711e4e6bd | |||
| 186eecfbff | |||
| d766c33f59 | |||
| 2c9257f1a8 | |||
| 71ff604f90 | |||
| 83e00763ea | |||
| 5b647e0937 | |||
| e0444510f4 | |||
| 82e876a892 | |||
| c728e05032 | |||
| c7a8ce7060 | |||
| 762b4339fe | |||
| f824b8988e | |||
| 6280d6d167 | |||
| 4f18e744b4 | |||
| 01d8f720e8 | |||
| 18cf058af3 | |||
| e2406df367 | |||
| 1fd669bdb3 | |||
| f6add12c80 | |||
| 0f643bfe39 | |||
| 15be498e4b | |||
| fba465dd62 | |||
| 19dbe354e7 | |||
| fca5d37b7e | |||
| aa04ad2dc2 | |||
| 7ef4d814ef | |||
| 3f3deb665c | |||
| 97fc22ce57 | |||
| 616f3ad76d | |||
| faca63946c | |||
| 57bae341a2 | |||
| fd09a766d2 | |||
| 11564a5292 | |||
| ac12e350bf | |||
| 2def15337d | |||
| 3e3d58a4a9 | |||
| 5ce4f55228 | |||
| 21788fc7b0 | |||
| a1c4382fde | |||
| 364e95698e | |||
| 6eec142499 | |||
| a8f6b3a39a | |||
| 250933bf41 | |||
| 56c77c781a | |||
| f7602b39a1 | |||
| 8c86092356 | |||
| db0a4bff77 | |||
| e198ff9cb1 | |||
| b8eaa5cf97 | |||
| 0d597721bf | |||
| 003e0caada | |||
| 053637cfb4 | |||
| 8178213f1a | |||
| b4222a41de | |||
| f28e409ea5 | |||
| 287c6c06e1 | |||
| 8216bdb4b3 | |||
| aa15da50ab | |||
| 02759c6f83 | |||
| 6b0c49752c | |||
| 2e4f792fc3 | |||
| 17eba059f0 | |||
| e59a00922b | |||
| 872201c886 | |||
| 3352098284 | |||
| d0bbd7f24f | |||
| 7f87714b58 | |||
| 5594bee618 | |||
| c469ef23e6 | |||
| f6e74f2526 | |||
| 10b6e9c537 | |||
| 3f27af30b7 | |||
| 23db09f9b7 | |||
| d1b7681efc | |||
| 61ad405ad8 | |||
| aff98110e0 | |||
| 2f36db9142 | |||
| aa86ee1066 | |||
| dbbcce8165 | |||
| 1ed066ef0f | |||
| 763f7d45d8 | |||
| 2328f3afb5 | |||
| 2223245861 | |||
| 36226b01cd | |||
| da31f9cadd | |||
| 9da4857066 | |||
| 75c71135ba | |||
| 0cb5025a16 | |||
| 44d9f69434 | |||
| 3f343b283b | |||
| 03a28fc3c5 | |||
| 3513619221 | |||
| 0c9f5769d3 | |||
| 587a666ab6 | |||
| f26deea508 | |||
| b8e19040b5 | |||
| 7d9e0f4080 | |||
| 16ce7fbc7b | |||
| 639fce376a | |||
| 3cdbac5c22 | |||
| 3dcafdf403 | |||
| cd2fe9f8d9 | |||
| fd40596ce7 | |||
| 7ecda69703 | |||
| a3b76cd5c2 | |||
| 54df862998 | |||
| 301b7a4911 | |||
| e0a048abe6 | |||
| 671e3e19ff | |||
| 0c394c2e61 | |||
| 4ecbb5234c | |||
| 98f1700049 | |||
| 2f0b4a0187 | |||
| f66c6ed0c3 | |||
| 5d9785ac2d | |||
| bb97a8cccc | |||
| 571cf5b5b8 | |||
| 1974ed1c03 | |||
| 98275f7c87 | |||
| eca8726909 | |||
| baf125c450 | |||
| efcc710d91 | |||
| 5980ee4c86 | |||
| db9b7a22c2 | |||
| 5e24d4f322 | |||
| 2dd32cdce2 | |||
| 9cddd93dad | |||
| 4127898655 | |||
| 45d48483d0 | |||
| 852c25296a | |||
| aea631138e | |||
| 683fdbb02a | |||
| c3bbab35e2 | |||
| ba8941046e | |||
| d202f4e00d | |||
| 42da5d8d32 | |||
| 5af3533598 | |||
| 7843168fad | |||
| 8f51eb63b0 | |||
| 855f5f7af4 | |||
| c85dd2655c | |||
| fb0e4060cd | |||
| 707b4990a6 | |||
| 9c8b922069 | |||
| d4b421421d | |||
| 58e9646fa6 | |||
| 500f172561 | |||
| 68f6c90ea4 | |||
| 41e91f2922 | |||
| 999117cfeb | |||
| 6185df512f | |||
| 0cbf66c007 | |||
| cd378b721d | |||
| 547d38d1ef | |||
| dca56af5b9 | |||
| 224442772e | |||
| 003951fdf7 | |||
| d51b3da1b4 | |||
| 69f4af84db | |||
| 771759b252 | |||
| 20c7a71db6 | |||
| 8475ee0985 | |||
| f42811d3d4 | |||
| c3b1832cfb | |||
| eb6753afe1 | |||
| 5051cecb84 | |||
| cd03ede358 | |||
| 6563f8c738 | |||
| e5279b4827 | |||
| 79ff505963 | |||
| 8a67eba5fc | |||
| 6609a5f340 | |||
| d9972cb349 | |||
| 28d2539432 | |||
| f28386b71f | |||
| 53717076f5 | |||
| a9aa928629 | |||
| 8df121148d | |||
| 5e23c32ae8 | |||
| 9c0f6481c0 | |||
| 68ae45dd58 | |||
| 3091747438 | |||
| 2f266b8dd4 | |||
| ee20b87ee2 | |||
| 83e025d0bb | |||
| 5115c6e217 | |||
| 76f6a94de5 | |||
| 954830be18 | |||
| ea70299a45 | |||
| 88da071ed6 | |||
| 1dbf162a71 | |||
| 1c0964753b | |||
| daa1c7f577 | |||
| 854416ceb2 | |||
| 2230351e3e | |||
| 7da3244da2 | |||
| bfeb0c2988 | |||
| d4e75c1dec | |||
| 405bddcde0 | |||
| 8a27c45ab1 | |||
| 10b15896b3 | |||
| 0e97bbe37c | |||
| e0d7e90894 | |||
| 5d13f6aab6 | |||
| 1ebfbbe89e | |||
| 91ad43fdfc | |||
| 6fe6fc180d | |||
| d84d0bec38 | |||
| 7e7b1c6ee1 | |||
| effb354d1b | |||
| ba7d1ad35f | |||
| 3ca2b19502 | |||
| 8e0d91dcf5 | |||
| cd2c2587ae | |||
| 53044696ba | |||
| 6d6927213f | |||
| be1b5bce4f | |||
| 4b4fd0735b | |||
| c565b2a31f | |||
| 55f2261905 | |||
| 51912f2b83 | |||
| 7f4e2617ee | |||
| 960a385202 | |||
| 21f48d3485 | |||
| 7f9605e55f | |||
| cc409dc3f7 | |||
| af6091760c | |||
| e1d93c003c | |||
| ff9dd2dd03 | |||
| 7a306bb3d2 | |||
| 7ffc148358 | |||
| 50fef2edfa | |||
| aa40084010 | |||
| 740d788c7c | |||
| 4c2fa2c1b3 | |||
| 4350c7b7a9 | |||
| 595f14d98d | |||
| 2e95d6ea63 | |||
| 0da6abeb98 | |||
| e4e050e8e7 | |||
| 5bc082b75e | |||
| beedbd7646 | |||
| 507b069ffe | |||
| 71444b0427 | |||
| a08bba438e | |||
| df1e6711af | |||
| f6d4e934e3 | |||
| d5bd4c6735 | |||
| eb12ba6ed2 | |||
| 6e83c08535 | |||
| b6bfdec48d | |||
| f9ec796291 | |||
| 3beb1d0683 | |||
| 8836c7f0ca | |||
| ef5ce1d6e1 | |||
| 0ea1213139 | |||
| 51fe372f60 | |||
| eb8f9f8936 | |||
| afc1524874 | |||
| fbb975625c | |||
| 53e75d8209 | |||
| 5bdf970c10 | |||
| 50089f72c6 | |||
| 62e15e0208 | |||
| 3d8b02a7f3 | |||
| 20701d9cf1 | |||
| fa94442eb2 | |||
| 68ff77e172 | |||
| 102e9be3a8 | |||
| 92bf01a183 | |||
| 559504ae29 | |||
| 9b00b41a1e | |||
| b1f6ad17e1 | |||
| e7979fe9db | |||
| 7a276adbbc | |||
| db4997fdc4 | |||
| 44ebb841f0 | |||
| 09ae4e2096 | |||
| 0b46efe4ea | |||
| f1dda43e66 | |||
| ce483138d7 | |||
| 73cc39226d | |||
| 57257f63dd | |||
| 88b25790e8 | |||
| e01defc4aa | |||
| cb50c43e93 | |||
| 5908d15f91 | |||
| f66cfaec12 | |||
| 259f92c53b | |||
| a84f850e91 | |||
| 5a765e6f07 | |||
| 791889c659 | |||
| 5da63faf1f | |||
| 30d108fc35 | |||
| a09fefab5e | |||
| f74ca1c236 | |||
| 30e027092b | |||
| fd4ac7c9b9 | |||
| 4482049b94 | |||
| 5839380437 | |||
| 2152470fdc | |||
| 93b2a81495 | |||
| e139e952c0 | |||
| cf1c57ccb8 | |||
| f7a2138488 | |||
| 9614d03bef | |||
| 32a335c676 | |||
| 06e27fc1e0 | |||
| 1f40e8dcd9 | |||
| 77ff8cef1f | |||
| ef844fbccb | |||
| 070dc5a4c0 | |||
| 177ef1cdcc | |||
| 4b1ebf02e1 | |||
| 863e50203e | |||
| 01b8c209de | |||
| 30e92f2bc1 | |||
| 02accabb4a | |||
| fa00a41fe0 | |||
| 2e66666bdf | |||
| 4fe3c9a751 | |||
| 0a35e14590 | |||
| e979c176e3 | |||
| a0d9c3dc29 | |||
| efcb68351c | |||
| 94e8bf2e1c | |||
| 82d1a294a6 | |||
| de20274589 | |||
| 2f193e64c8 | |||
| 86751362cb | |||
| 4118323631 | |||
| 0d134f7f10 | |||
| 409724cfcd | |||
| 799a33be40 | |||
| 2fa9c66495 | |||
| ad818a8e7e | |||
| 581f72b3f8 | |||
| 1dd7e4347c | |||
| 36cc9398c7 | |||
| 68817feeec | |||
| 97661e2ca2 | |||
| 72def5ae6d | |||
| e638b155a1 | |||
| 32db18b0d6 | |||
| b653a5250d | |||
| 30329f7cad | |||
| 29a1478c86 | |||
| c882bf31ec | |||
| 17ccb8f083 | |||
| 0e7d2a8b0e | |||
| 3743543ef8 | |||
| 700dd7b45a | |||
| c2eb73fd8a | |||
| e1f4f7f95b | |||
| 37401409c6 | |||
| b282631cd5 | |||
| 9618d3b3f3 | |||
| c9f997d121 | |||
| f1dee2a089 | |||
| 8273277c91 | |||
| 9758844da3 | |||
| e41c7fbbc7 | |||
| 24db8a5a49 | |||
| 36e82b9873 | |||
| 8a32f2b8b1 | |||
| 277830bc3c | |||
| a8fa969114 | |||
| c3f3dced68 | |||
| 85fce59c0c | |||
| 8a6147d512 | |||
| e799b256b2 | |||
| b222dc0ca8 | |||
| c52c6b04ca | |||
| b95eed46bb | |||
| 7c36a543da | |||
| 90e000c18e | |||
| 1bb9d737d8 | |||
| 9a5db2ec51 | |||
| dbed29a044 | |||
| 681859531c | |||
| 8e1ad6b16a | |||
| 5448f1ba2d | |||
| e43da4e1a3 | |||
| eaa9da49cc | |||
| 40873b529c | |||
| 8cc4c19d73 | |||
| bb9c18faf1 | |||
| fabdfb76b9 | |||
| bce263a928 | |||
| 195920e476 | |||
| a821d895c5 | |||
| ab1b6ec27d | |||
| 6dc099809f | |||
| 03c8b75994 | |||
| 38887452ad | |||
| 7512edad59 | |||
| 944c895bcd | |||
| e7d87ee8e2 | |||
| cfdbd10635 | |||
| d3a2d8733f | |||
| a7e623d817 | |||
| 3f0c37cea4 | |||
| 2c96a6d22a | |||
| 57b4214a72 | |||
| 433b3d39d9 | |||
| 26441ed45c | |||
| 92cd38c2a0 | |||
| 3b5a06794f | |||
| d104409272 | |||
| e5f58c2898 | |||
| f83863ef01 | |||
| 837f069cf5 | |||
| 9f057dc29a | |||
| c4904f176c | |||
| d3a5aba703 | |||
| 9e283e427c | |||
| 133ba31d66 | |||
| 241a65a92a | |||
| 0b54795bab | |||
| 6208193de5 | |||
| c53321532f | |||
| 34f25e3e06 | |||
| c46244366e | |||
| 6518af04fc | |||
| bf137ff1f7 | |||
| 1877955b62 | |||
| 50d0875de2 | |||
| bf151e6b7d | |||
| 82893402d0 | |||
| 8049102787 | |||
| f42cc3d9fd | |||
| 5f9a5208db | |||
| 6df506d238 | |||
| 2bd3354256 | |||
| b55aaa1d18 | |||
| 34e19505bd | |||
| 6e06ec0904 | |||
| a5814074fe | |||
| d7479df5a2 | |||
| 34508aa0ae | |||
| ae096b2c9c | |||
| 95d036e34a | |||
| 4af5e8ec42 | |||
| 2a5f71bd5d | |||
| 97fb63dda1 | |||
| 87d42e3b3b | |||
| 0394129a4c | |||
| 3c499c834b | |||
| 17d6cc7d46 | |||
| 646bd7dc38 | |||
| 56e483782d | |||
| e1b9066b26 | |||
| 7114ce2516 | |||
| 9240c6570a | |||
| f80a44ccd7 | |||
| e6f5eb244e | |||
| ab62e83110 | |||
| aeefb9e536 | |||
| ee0efa536a | |||
| 2523130fdc | |||
| c024777184 | |||
| 5951d7cd2d | |||
| 011670c70b | |||
| 6cebd6c769 | |||
| 546ae5cbf1 | |||
| f543cc642e | |||
| 8ac3c5ea22 | |||
| 63918f0680 | |||
| bfb3d8b8a2 | |||
| e38ff99607 | |||
| b0e3d922c8 | |||
| a15bb8e994 | |||
| 6f487100cd | |||
| 0693a2315f | |||
| f360e886ff | |||
| 6ea08cc5dc | |||
| 347c706d6f | |||
| 5f5e6616c7 | |||
| 657bcadc7e | |||
| 107666cc60 | |||
| b37669184a | |||
| 163a01f224 | |||
| 3d58094199 | |||
| 463951a4f1 | |||
| 34804d5162 | |||
| 3895c33915 | |||
| 17f4eb1a56 | |||
| 0abdffdea6 | |||
| d32999f178 | |||
| f621feb843 | |||
| 8d277f029d | |||
| 1788a02338 | |||
| ba0800d16c | |||
| 4008c7d8f6 | |||
| 610a2e2afc | |||
| 6f3715d1eb | |||
| b78ecaa814 | |||
| e6f5399d53 | |||
| 0e5806cadd | |||
| 68c9d4afa7 | |||
| f0ea38fe49 | |||
| b0332f923e | |||
| 8a76c25394 | |||
| fd96126e3e | |||
| ff3fbedc18 | |||
| 8791419f8e | |||
| 5447b247a0 | |||
| aabbb10564 | |||
| 3ccd6c9a3e | |||
| c290240de7 | |||
| 8e799b174b | |||
| a9c3a93989 | |||
| 3ef8698f42 | |||
| fa4e843c30 | |||
| 9a4d11f4d9 | |||
| eed2b8d618 | |||
| 13f02c2aca | |||
| d50f8fbc8b | |||
| 155238a516 | |||
| 427fcdbdca | |||
| ca05d402a7 | |||
| c5a80b68ca | |||
| c1fb15b135 | |||
| 4b2c131836 | |||
| 9ca1e69b3c | |||
| 082d041d44 | |||
| 221f276c4b | |||
| 24cec21465 | |||
| 9f71ec6194 | |||
| bb36afc390 | |||
| b53bf0ff64 | |||
| 3ebc6f2436 | |||
| 2eef6778a6 | |||
| 81fabec810 | |||
| dc6e7924b5 | |||
| 48dec5a2c8 | |||
| 9b500e1da9 | |||
| a038820112 | |||
| 70a15973b6 | |||
| 09b6a00731 | |||
| 883c3cf0e9 | |||
| a46bb8183c | |||
| d5d5a7b012 | |||
| a120efdc91 | |||
| d48f4b06eb | |||
| f078912736 | |||
| 63b0f0dedd | |||
| 84c22dbf5f | |||
| b8cd1232be | |||
| a518ab07f4 | |||
| 9e5a1ee975 | |||
| 95bf3f0316 | |||
| d69dd513bc | |||
| 525cdf571a | |||
| 9cfe0a8804 | |||
| 50b54599ef | |||
| ed6bef6d24 | |||
| 71268636df | |||
| 568729ecd6 | |||
| 9139725be6 | |||
|
969a8da6bf
|
|||
| 2338b26329 | |||
| d4df206740 | |||
| 8a93cdd33c | |||
| 92b31de4a9 | |||
| 5452f3f623 | |||
| 256614dbaf | |||
| 049449b213 | |||
| 85b46336b1 | |||
| 590afa7b01 | |||
| 574292b798 | |||
| 21cf503a59 | |||
| 3630cdbfe0 | |||
| 0f3be229e6 | |||
| 8e5a024d3d | |||
| 410bb7c09d | |||
| 9de8b0f449 | |||
| d47c3a1222 | |||
| df99b3aa90 | |||
| 0090850e10 | |||
| 9efd64bd18 | |||
| b16c37e48b | |||
| 3ee2c00726 | |||
| d5a7e19f1a | |||
| 9b52415b35 | |||
| dbe24494d9 | |||
| 3eab5a5f70 | |||
| 548febfb22 | |||
| b40f72443a | |||
| 2c03496373 | |||
| b6a937c954 | |||
| 63776d40bd | |||
| cb3c7afade | |||
| 991022adfc | |||
| 2bc71a18a6 | |||
| 57ca864fbb | |||
| a09edfb612 | |||
| 7997a739ab | |||
| 248b258413 | |||
| 0423ed7fb4 | |||
| c29378c2f8 | |||
| 163fbd85e7 | |||
| 58bb86ebe1 | |||
| c5140ee8e8 | |||
| 6270fd8118 | |||
| 3fff706848 | |||
| c259defab5 | |||
| e5fee5c306 | |||
| 9d35b4bdfb | |||
| 9497d7cf64 | |||
| c7d3e602cb | |||
| 0076eb4ed4 | |||
| 6070bde413 | |||
| c7a6d426f0 | |||
| f66cf0f802 | |||
| e4b6c81024 | |||
| 44d784cd04 | |||
| 0394201113 | |||
| e270c16516 | |||
|
4c10538632
|
|||
| 71329c5532 | |||
| feb4bf9e87 | |||
| 5d5567e94c | |||
| 684e6fb9cb | |||
|
ee21fa6d03
|
|||
| 7a2974e54f | |||
| f4dfc1dd98 | |||
| 2eebfa9a7a | |||
| 10097ffeb8 | |||
| cbe1f54a2a | |||
| 4d8f081a59 | |||
| 29e79c9484 | |||
| ba35869b0a | |||
| 580688381e | |||
| e63d69a440 | |||
| be64fe04fb | |||
| 801ab20723 | |||
| d974a5e044 | |||
| 1be94ae0be | |||
| b883e6a485 | |||
| a0210379ae | |||
| e56dc207d1 | |||
| 523c9c9ad2 | |||
| 74bb2151c1 | |||
| f79d7b35a4 | |||
|
3b36496dac
|
|||
|
4ebd6c24a9
|
|||
|
05451d98b3
|
|||
|
22a4bce3c8
|
|||
| 76d499f00b | |||
| 46e711f0a5 | |||
| abffac3f82 | |||
| 27b275548e | |||
| 93ce253d1e | |||
| a5af312b39 | |||
| 4b5e8e8a43 | |||
| 443dd4d168 | |||
| 907479df84 | |||
| 9887a78e98 | |||
| f669371349 | |||
| 24c720c79a | |||
|
4485234980
|
|||
|
b6871c0b1f
|
@@ -14,7 +14,7 @@ IndentWidth: 4
|
|||||||
MaxEmptyLinesToKeep: 1
|
MaxEmptyLinesToKeep: 1
|
||||||
ObjCBlockIndentWidth: 4
|
ObjCBlockIndentWidth: 4
|
||||||
ObjCBreakBeforeNestedBlockParam: false
|
ObjCBreakBeforeNestedBlockParam: false
|
||||||
SortIncludes: false
|
SortIncludes: true
|
||||||
TabWidth: 4
|
TabWidth: 4
|
||||||
UseTab: Always
|
UseTab: Always
|
||||||
...
|
...
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
.svn
|
.git
|
||||||
db.sqlite
|
db.sqlite*
|
||||||
out/**/*.o
|
out/
|
||||||
out/**/*.d
|
|
||||||
|
|||||||
71
.gitea/workflows/build.yaml
Normal file
71
.gitea/workflows/build.yaml
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
name: Build Tilde Friends
|
||||||
|
run-name: ${{ gitea.actor }} running 🚀
|
||||||
|
on: [push]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
Build-All:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
container:
|
||||||
|
image: node:23-bookworm-slim
|
||||||
|
valid_volumes:
|
||||||
|
- '/opt/keys'
|
||||||
|
- '/opt/deps'
|
||||||
|
volumes:
|
||||||
|
- /opt/keys:/opt/keys
|
||||||
|
- /opt/deps:/opt/deps
|
||||||
|
steps:
|
||||||
|
- name: Install build dependencies
|
||||||
|
run: >
|
||||||
|
apt update && apt install -y \
|
||||||
|
build-essential \
|
||||||
|
clang-19 \
|
||||||
|
cmake \
|
||||||
|
curl \
|
||||||
|
docker.io \
|
||||||
|
doxygen \
|
||||||
|
file \
|
||||||
|
gcc-aarch64-linux-gnu \
|
||||||
|
git \
|
||||||
|
graphviz \
|
||||||
|
libgpgme11 \
|
||||||
|
libssl-dev \
|
||||||
|
mingw-w64 \
|
||||||
|
rsync \
|
||||||
|
unzip \
|
||||||
|
zip \
|
||||||
|
zlib1g-dev
|
||||||
|
- name: Get code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
submodules: true
|
||||||
|
- name: Setup environment
|
||||||
|
run: |
|
||||||
|
update-alternatives --install /usr/bin/clang clang /usr/bin/clang-19 100
|
||||||
|
update-alternatives --install /usr/bin/clang++ clang++ /usr/bin/clang++-19 100
|
||||||
|
ln -s /opt/keys .keys
|
||||||
|
ln -sf /opt/deps/ios_toolchain deps/
|
||||||
|
ln -sf /opt/deps/macos_toolchain deps/
|
||||||
|
- name: Build documentation
|
||||||
|
run: |
|
||||||
|
mkdir -p out/html/ ~/.ssh/
|
||||||
|
make -j`nproc` docs
|
||||||
|
echo 'pildefriends ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKD3Kde5vDO0TrMBDK0IGGeNGe/XinWAZkSQ/rXxwUjt' >> ~/.ssh/known_hosts
|
||||||
|
rsync -avP --delete -e "ssh -i /opt/keys/ssh.ed25519" out/html/ tfdocs@pildefriends:docs/html/
|
||||||
|
- name: Setup JDK
|
||||||
|
uses: actions/setup-java@v3
|
||||||
|
with:
|
||||||
|
java-version: '17'
|
||||||
|
distribution: 'temurin'
|
||||||
|
- name: Setup Android SDK
|
||||||
|
uses: android-actions/setup-android@v3
|
||||||
|
with:
|
||||||
|
packages: 'tools platform-tools build-tools;35.0.0 platforms;android-35 ndk;27.2.12479018'
|
||||||
|
- name: Docker build
|
||||||
|
run: DOCKER_BUILDKIT=1 docker build .
|
||||||
|
- name: Build
|
||||||
|
run: ANDROID_SDK=$HOME/.android/sdk make -j`nproc` all dist
|
||||||
|
- name: Upload artifacts
|
||||||
|
uses: actions/upload-artifact@v3
|
||||||
|
with:
|
||||||
|
name: dist
|
||||||
|
path: dist/*
|
||||||
13
.gitignore
vendored
13
.gitignore
vendored
@@ -1,11 +1,20 @@
|
|||||||
|
build/
|
||||||
|
*.core
|
||||||
db.*
|
db.*
|
||||||
deps/ios_toolchain/
|
deps/ios_toolchain
|
||||||
|
deps/macos_toolchain
|
||||||
deps/openssl/
|
deps/openssl/
|
||||||
dist/
|
dist/
|
||||||
|
.flatpak-builder
|
||||||
.keys
|
.keys
|
||||||
|
**/.DS_Store
|
||||||
|
logs/
|
||||||
**/node_modules
|
**/node_modules
|
||||||
out
|
out
|
||||||
|
repo/
|
||||||
|
result
|
||||||
*.swo
|
*.swo
|
||||||
*.swp
|
*.swp
|
||||||
|
tmp/
|
||||||
|
unsigned/
|
||||||
.zsign_cache/
|
.zsign_cache/
|
||||||
result
|
|
||||||
|
|||||||
6
.gitmodules
vendored
6
.gitmodules
vendored
@@ -19,3 +19,9 @@
|
|||||||
[submodule "deps/picohttpparser"]
|
[submodule "deps/picohttpparser"]
|
||||||
path = deps/picohttpparser
|
path = deps/picohttpparser
|
||||||
url = https://github.com/h2o/picohttpparser.git
|
url = https://github.com/h2o/picohttpparser.git
|
||||||
|
[submodule "deps/c-ares"]
|
||||||
|
path = deps/c-ares
|
||||||
|
url = https://github.com/c-ares/c-ares.git
|
||||||
|
[submodule "deps/zsign"]
|
||||||
|
path = deps/zsign
|
||||||
|
url = https://github.com/zhlynn/zsign.git
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ node_modules
|
|||||||
src
|
src
|
||||||
deps
|
deps
|
||||||
.clang-format
|
.clang-format
|
||||||
|
flake.lock
|
||||||
|
apps/trace/speedscope/**
|
||||||
|
|
||||||
# Minified files
|
# Minified files
|
||||||
**/*.min.css
|
**/*.min.css
|
||||||
|
|||||||
@@ -1,19 +1,15 @@
|
|||||||
FROM bitnami/minideb:bullseye AS build
|
FROM bitnami/minideb:bookworm AS build
|
||||||
|
|
||||||
RUN apt-get update && \
|
RUN apt-get update && \
|
||||||
apt-get install -y --no-install-recommends \
|
apt-get install -y --no-install-recommends \
|
||||||
gcc \
|
gcc \
|
||||||
libc6-dev \
|
libc6-dev \
|
||||||
libssl-dev \
|
|
||||||
make
|
make
|
||||||
|
|
||||||
COPY . /app
|
COPY . /app
|
||||||
RUN make -C /app -j $(nproc) release
|
RUN make -C /app -j $(nproc) release
|
||||||
|
|
||||||
FROM bitnami/minideb:bullseye
|
FROM bitnami/minideb:bookworm
|
||||||
RUN apt-get update && \
|
|
||||||
apt-get install -y --no-install-recommends \
|
|
||||||
libssl1.1
|
|
||||||
|
|
||||||
COPY --from=build /app/out/release/tildefriends /app/out/release/tildefriends
|
COPY --from=build /app/out/release/tildefriends /app/out/release/tildefriends
|
||||||
COPY --from=build /app/apps /app/apps
|
COPY --from=build /app/apps /app/apps
|
||||||
|
|||||||
379
Doxyfile
379
Doxyfile
@@ -1,4 +1,4 @@
|
|||||||
# Doxyfile 1.9.1
|
# Doxyfile 1.9.4
|
||||||
|
|
||||||
# This file describes the settings to be used by the documentation system
|
# This file describes the settings to be used by the documentation system
|
||||||
# doxygen (www.doxygen.org) for a project.
|
# doxygen (www.doxygen.org) for a project.
|
||||||
@@ -12,6 +12,15 @@
|
|||||||
# For lists, items can also be appended using:
|
# For lists, items can also be appended using:
|
||||||
# TAG += value [value, ...]
|
# TAG += value [value, ...]
|
||||||
# Values that contain spaces should be placed between quotes (\" \").
|
# Values that contain spaces should be placed between quotes (\" \").
|
||||||
|
#
|
||||||
|
# Note:
|
||||||
|
#
|
||||||
|
# Use doxygen to compare the used configuration file with the template
|
||||||
|
# configuration file:
|
||||||
|
# doxygen -x [configFile]
|
||||||
|
# Use doxygen to compare the used configuration file with the template
|
||||||
|
# configuration file without replacing the environment variables:
|
||||||
|
# doxygen -x_noenv [configFile]
|
||||||
|
|
||||||
#---------------------------------------------------------------------------
|
#---------------------------------------------------------------------------
|
||||||
# Project related configuration options
|
# Project related configuration options
|
||||||
@@ -60,16 +69,28 @@ PROJECT_LOGO =
|
|||||||
|
|
||||||
OUTPUT_DIRECTORY =
|
OUTPUT_DIRECTORY =
|
||||||
|
|
||||||
# If the CREATE_SUBDIRS tag is set to YES then doxygen will create 4096 sub-
|
# If the CREATE_SUBDIRS tag is set to YES then doxygen will create up to 4096
|
||||||
# directories (in 2 levels) under the output directory of each output format and
|
# sub-directories (in 2 levels) under the output directory of each output format
|
||||||
# will distribute the generated files over these directories. Enabling this
|
# and will distribute the generated files over these directories. Enabling this
|
||||||
# option can be useful when feeding doxygen a huge amount of source files, where
|
# option can be useful when feeding doxygen a huge amount of source files, where
|
||||||
# putting all generated files in the same directory would otherwise causes
|
# putting all generated files in the same directory would otherwise causes
|
||||||
# performance problems for the file system.
|
# performance problems for the file system. Adapt CREATE_SUBDIRS_LEVEL to
|
||||||
|
# control the number of sub-directories.
|
||||||
# The default value is: NO.
|
# The default value is: NO.
|
||||||
|
|
||||||
CREATE_SUBDIRS = NO
|
CREATE_SUBDIRS = NO
|
||||||
|
|
||||||
|
# Controls the number of sub-directories that will be created when
|
||||||
|
# CREATE_SUBDIRS tag is set to YES. Level 0 represents 16 directories, and every
|
||||||
|
# level increment doubles the number of directories, resulting in 4096
|
||||||
|
# directories at level 8 which is the default and also the maximum value. The
|
||||||
|
# sub-directories are organized in 2 levels, the first level always has a fixed
|
||||||
|
# numer of 16 directories.
|
||||||
|
# Minimum value: 0, maximum value: 8, default value: 8.
|
||||||
|
# This tag requires that the tag CREATE_SUBDIRS is set to YES.
|
||||||
|
|
||||||
|
CREATE_SUBDIRS_LEVEL = 8
|
||||||
|
|
||||||
# If the ALLOW_UNICODE_NAMES tag is set to YES, doxygen will allow non-ASCII
|
# If the ALLOW_UNICODE_NAMES tag is set to YES, doxygen will allow non-ASCII
|
||||||
# characters to appear in the names of generated files. If set to NO, non-ASCII
|
# characters to appear in the names of generated files. If set to NO, non-ASCII
|
||||||
# characters will be escaped, for example _xE3_x81_x84 will be used for Unicode
|
# characters will be escaped, for example _xE3_x81_x84 will be used for Unicode
|
||||||
@@ -81,26 +102,18 @@ ALLOW_UNICODE_NAMES = NO
|
|||||||
# The OUTPUT_LANGUAGE tag is used to specify the language in which all
|
# The OUTPUT_LANGUAGE tag is used to specify the language in which all
|
||||||
# documentation generated by doxygen is written. Doxygen will use this
|
# documentation generated by doxygen is written. Doxygen will use this
|
||||||
# information to generate all constant output in the proper language.
|
# information to generate all constant output in the proper language.
|
||||||
# Possible values are: Afrikaans, Arabic, Armenian, Brazilian, Catalan, Chinese,
|
# Possible values are: Afrikaans, Arabic, Armenian, Brazilian, Bulgarian,
|
||||||
# Chinese-Traditional, Croatian, Czech, Danish, Dutch, English (United States),
|
# Catalan, Chinese, Chinese-Traditional, Croatian, Czech, Danish, Dutch, English
|
||||||
# Esperanto, Farsi (Persian), Finnish, French, German, Greek, Hungarian,
|
# (United States), Esperanto, Farsi (Persian), Finnish, French, German, Greek,
|
||||||
# Indonesian, Italian, Japanese, Japanese-en (Japanese with English messages),
|
# Hindi, Hungarian, Indonesian, Italian, Japanese, Japanese-en (Japanese with
|
||||||
# Korean, Korean-en (Korean with English messages), Latvian, Lithuanian,
|
# English messages), Korean, Korean-en (Korean with English messages), Latvian,
|
||||||
# Macedonian, Norwegian, Persian (Farsi), Polish, Portuguese, Romanian, Russian,
|
# Lithuanian, Macedonian, Norwegian, Persian (Farsi), Polish, Portuguese,
|
||||||
# Serbian, Serbian-Cyrillic, Slovak, Slovene, Spanish, Swedish, Turkish,
|
# Romanian, Russian, Serbian, Serbian-Cyrillic, Slovak, Slovene, Spanish,
|
||||||
# Ukrainian and Vietnamese.
|
# Swedish, Turkish, Ukrainian and Vietnamese.
|
||||||
# The default value is: English.
|
# The default value is: English.
|
||||||
|
|
||||||
OUTPUT_LANGUAGE = English
|
OUTPUT_LANGUAGE = English
|
||||||
|
|
||||||
# The OUTPUT_TEXT_DIRECTION tag is used to specify the direction in which all
|
|
||||||
# documentation generated by doxygen is written. Doxygen will use this
|
|
||||||
# information to generate all generated output in the proper direction.
|
|
||||||
# Possible values are: None, LTR, RTL and Context.
|
|
||||||
# The default value is: None.
|
|
||||||
|
|
||||||
OUTPUT_TEXT_DIRECTION = None
|
|
||||||
|
|
||||||
# If the BRIEF_MEMBER_DESC tag is set to YES, doxygen will include brief member
|
# If the BRIEF_MEMBER_DESC tag is set to YES, doxygen will include brief member
|
||||||
# descriptions after the members that are listed in the file and class
|
# descriptions after the members that are listed in the file and class
|
||||||
# documentation (similar to Javadoc). Set to NO to disable this.
|
# documentation (similar to Javadoc). Set to NO to disable this.
|
||||||
@@ -258,16 +271,16 @@ TAB_SIZE = 4
|
|||||||
# the documentation. An alias has the form:
|
# the documentation. An alias has the form:
|
||||||
# name=value
|
# name=value
|
||||||
# For example adding
|
# For example adding
|
||||||
# "sideeffect=@par Side Effects:\n"
|
# "sideeffect=@par Side Effects:^^"
|
||||||
# will allow you to put the command \sideeffect (or @sideeffect) in the
|
# will allow you to put the command \sideeffect (or @sideeffect) in the
|
||||||
# documentation, which will result in a user-defined paragraph with heading
|
# documentation, which will result in a user-defined paragraph with heading
|
||||||
# "Side Effects:". You can put \n's in the value part of an alias to insert
|
# "Side Effects:". Note that you cannot put \n's in the value part of an alias
|
||||||
# newlines (in the resulting output). You can put ^^ in the value part of an
|
# to insert newlines (in the resulting output). You can put ^^ in the value part
|
||||||
# alias to insert a newline as if a physical newline was in the original file.
|
# of an alias to insert a newline as if a physical newline was in the original
|
||||||
# When you need a literal { or } or , in the value part of an alias you have to
|
# file. When you need a literal { or } or , in the value part of an alias you
|
||||||
# escape them by means of a backslash (\), this can lead to conflicts with the
|
# have to escape them by means of a backslash (\), this can lead to conflicts
|
||||||
# commands \{ and \} for these it is advised to use the version @{ and @} or use
|
# with the commands \{ and \} for these it is advised to use the version @{ and
|
||||||
# a double escape (\\{ and \\})
|
# @} or use a double escape (\\{ and \\})
|
||||||
|
|
||||||
ALIASES =
|
ALIASES =
|
||||||
|
|
||||||
@@ -312,8 +325,8 @@ OPTIMIZE_OUTPUT_SLICE = NO
|
|||||||
# extension. Doxygen has a built-in mapping, but you can override or extend it
|
# extension. Doxygen has a built-in mapping, but you can override or extend it
|
||||||
# using this tag. The format is ext=language, where ext is a file extension, and
|
# using this tag. The format is ext=language, where ext is a file extension, and
|
||||||
# language is one of the parsers supported by doxygen: IDL, Java, JavaScript,
|
# language is one of the parsers supported by doxygen: IDL, Java, JavaScript,
|
||||||
# Csharp (C#), C, C++, D, PHP, md (Markdown), Objective-C, Python, Slice, VHDL,
|
# Csharp (C#), C, C++, Lex, D, PHP, md (Markdown), Objective-C, Python, Slice,
|
||||||
# Fortran (fixed format Fortran: FortranFixed, free formatted Fortran:
|
# VHDL, Fortran (fixed format Fortran: FortranFixed, free formatted Fortran:
|
||||||
# FortranFree, unknown formatted Fortran: Fortran. In the later case the parser
|
# FortranFree, unknown formatted Fortran: Fortran. In the later case the parser
|
||||||
# tries to guess whether the code is fixed or free formatted code, this is the
|
# tries to guess whether the code is fixed or free formatted code, this is the
|
||||||
# default for Fortran type files). For instance to make doxygen treat .inc files
|
# default for Fortran type files). For instance to make doxygen treat .inc files
|
||||||
@@ -328,7 +341,7 @@ OPTIMIZE_OUTPUT_SLICE = NO
|
|||||||
#
|
#
|
||||||
# Note see also the list of default file extension mappings.
|
# Note see also the list of default file extension mappings.
|
||||||
|
|
||||||
EXTENSION_MAPPING =
|
EXTENSION_MAPPING = js=javascript
|
||||||
|
|
||||||
# If the MARKDOWN_SUPPORT tag is enabled then doxygen pre-processes all comments
|
# If the MARKDOWN_SUPPORT tag is enabled then doxygen pre-processes all comments
|
||||||
# according to the Markdown format, which allows for more readable
|
# according to the Markdown format, which allows for more readable
|
||||||
@@ -460,13 +473,13 @@ TYPEDEF_HIDES_STRUCT = NO
|
|||||||
|
|
||||||
LOOKUP_CACHE_SIZE = 0
|
LOOKUP_CACHE_SIZE = 0
|
||||||
|
|
||||||
# The NUM_PROC_THREADS specifies the number threads doxygen is allowed to use
|
# The NUM_PROC_THREADS specifies the number of threads doxygen is allowed to use
|
||||||
# during processing. When set to 0 doxygen will based this on the number of
|
# during processing. When set to 0 doxygen will based this on the number of
|
||||||
# cores available in the system. You can set it explicitly to a value larger
|
# cores available in the system. You can set it explicitly to a value larger
|
||||||
# than 0 to get more control over the balance between CPU load and processing
|
# than 0 to get more control over the balance between CPU load and processing
|
||||||
# speed. At this moment only the input processing can be done using multiple
|
# speed. At this moment only the input processing can be done using multiple
|
||||||
# threads. Since this is still an experimental feature the default is set to 1,
|
# threads. Since this is still an experimental feature the default is set to 1,
|
||||||
# which efficively disables parallel processing. Please report any issues you
|
# which effectively disables parallel processing. Please report any issues you
|
||||||
# encounter. Generating dot graphs in parallel is controlled by the
|
# encounter. Generating dot graphs in parallel is controlled by the
|
||||||
# DOT_NUM_THREADS setting.
|
# DOT_NUM_THREADS setting.
|
||||||
# Minimum value: 0, maximum value: 32, default value: 1.
|
# Minimum value: 0, maximum value: 32, default value: 1.
|
||||||
@@ -585,7 +598,7 @@ INTERNAL_DOCS = NO
|
|||||||
# filesystem is case sensitive (i.e. it supports files in the same directory
|
# filesystem is case sensitive (i.e. it supports files in the same directory
|
||||||
# whose names only differ in casing), the option must be set to YES to properly
|
# whose names only differ in casing), the option must be set to YES to properly
|
||||||
# deal with such files in case they appear in the input. For filesystems that
|
# deal with such files in case they appear in the input. For filesystems that
|
||||||
# are not case sensitive the option should be be set to NO to properly deal with
|
# are not case sensitive the option should be set to NO to properly deal with
|
||||||
# output files written for symbols that only differ in casing, such as for two
|
# output files written for symbols that only differ in casing, such as for two
|
||||||
# classes, one named CLASS and the other named Class, and to also support
|
# classes, one named CLASS and the other named Class, and to also support
|
||||||
# references to files without having to specify the exact matching casing. On
|
# references to files without having to specify the exact matching casing. On
|
||||||
@@ -610,6 +623,12 @@ HIDE_SCOPE_NAMES = NO
|
|||||||
|
|
||||||
HIDE_COMPOUND_REFERENCE= NO
|
HIDE_COMPOUND_REFERENCE= NO
|
||||||
|
|
||||||
|
# If the SHOW_HEADERFILE tag is set to YES then the documentation for a class
|
||||||
|
# will show which file needs to be included to use the class.
|
||||||
|
# The default value is: YES.
|
||||||
|
|
||||||
|
SHOW_HEADERFILE = YES
|
||||||
|
|
||||||
# If the SHOW_INCLUDE_FILES tag is set to YES then doxygen will put a list of
|
# If the SHOW_INCLUDE_FILES tag is set to YES then doxygen will put a list of
|
||||||
# the files that are included by a file in the documentation of that file.
|
# the files that are included by a file in the documentation of that file.
|
||||||
# The default value is: YES.
|
# The default value is: YES.
|
||||||
@@ -767,7 +786,8 @@ FILE_VERSION_FILTER =
|
|||||||
# output files in an output format independent way. To create the layout file
|
# output files in an output format independent way. To create the layout file
|
||||||
# that represents doxygen's defaults, run doxygen with the -l option. You can
|
# that represents doxygen's defaults, run doxygen with the -l option. You can
|
||||||
# optionally specify a file name after the option, if omitted DoxygenLayout.xml
|
# optionally specify a file name after the option, if omitted DoxygenLayout.xml
|
||||||
# will be used as the name of the layout file.
|
# will be used as the name of the layout file. See also section "Changing the
|
||||||
|
# layout of pages" for information.
|
||||||
#
|
#
|
||||||
# Note that if you run doxygen from a directory containing a file called
|
# Note that if you run doxygen from a directory containing a file called
|
||||||
# DoxygenLayout.xml, doxygen will parse it automatically even if the LAYOUT_FILE
|
# DoxygenLayout.xml, doxygen will parse it automatically even if the LAYOUT_FILE
|
||||||
@@ -813,18 +833,26 @@ WARNINGS = YES
|
|||||||
WARN_IF_UNDOCUMENTED = YES
|
WARN_IF_UNDOCUMENTED = YES
|
||||||
|
|
||||||
# If the WARN_IF_DOC_ERROR tag is set to YES, doxygen will generate warnings for
|
# If the WARN_IF_DOC_ERROR tag is set to YES, doxygen will generate warnings for
|
||||||
# potential errors in the documentation, such as not documenting some parameters
|
# potential errors in the documentation, such as documenting some parameters in
|
||||||
# in a documented function, or documenting parameters that don't exist or using
|
# a documented function twice, or documenting parameters that don't exist or
|
||||||
# markup commands wrongly.
|
# using markup commands wrongly.
|
||||||
# The default value is: YES.
|
# The default value is: YES.
|
||||||
|
|
||||||
WARN_IF_DOC_ERROR = YES
|
WARN_IF_DOC_ERROR = YES
|
||||||
|
|
||||||
|
# If WARN_IF_INCOMPLETE_DOC is set to YES, doxygen will warn about incomplete
|
||||||
|
# function parameter documentation. If set to NO, doxygen will accept that some
|
||||||
|
# parameters have no documentation without warning.
|
||||||
|
# The default value is: YES.
|
||||||
|
|
||||||
|
WARN_IF_INCOMPLETE_DOC = YES
|
||||||
|
|
||||||
# This WARN_NO_PARAMDOC option can be enabled to get warnings for functions that
|
# This WARN_NO_PARAMDOC option can be enabled to get warnings for functions that
|
||||||
# are documented, but have no documentation for their parameters or return
|
# are documented, but have no documentation for their parameters or return
|
||||||
# value. If set to NO, doxygen will only warn about wrong or incomplete
|
# value. If set to NO, doxygen will only warn about wrong parameter
|
||||||
# parameter documentation, but not about the absence of documentation. If
|
# documentation, but not about the absence of documentation. If EXTRACT_ALL is
|
||||||
# EXTRACT_ALL is set to YES then this flag will automatically be disabled.
|
# set to YES then this flag will automatically be disabled. See also
|
||||||
|
# WARN_IF_INCOMPLETE_DOC
|
||||||
# The default value is: NO.
|
# The default value is: NO.
|
||||||
|
|
||||||
WARN_NO_PARAMDOC = NO
|
WARN_NO_PARAMDOC = NO
|
||||||
@@ -844,13 +872,27 @@ WARN_AS_ERROR = NO
|
|||||||
# and the warning text. Optionally the format may contain $version, which will
|
# and the warning text. Optionally the format may contain $version, which will
|
||||||
# be replaced by the version of the file (if it could be obtained via
|
# be replaced by the version of the file (if it could be obtained via
|
||||||
# FILE_VERSION_FILTER)
|
# FILE_VERSION_FILTER)
|
||||||
|
# See also: WARN_LINE_FORMAT
|
||||||
# The default value is: $file:$line: $text.
|
# The default value is: $file:$line: $text.
|
||||||
|
|
||||||
WARN_FORMAT = "$file:$line: $text"
|
WARN_FORMAT = "$file:$line: $text"
|
||||||
|
|
||||||
|
# In the $text part of the WARN_FORMAT command it is possible that a reference
|
||||||
|
# to a more specific place is given. To make it easier to jump to this place
|
||||||
|
# (outside of doxygen) the user can define a custom "cut" / "paste" string.
|
||||||
|
# Example:
|
||||||
|
# WARN_LINE_FORMAT = "'vi $file +$line'"
|
||||||
|
# See also: WARN_FORMAT
|
||||||
|
# The default value is: at line $line of file $file.
|
||||||
|
|
||||||
|
WARN_LINE_FORMAT = "at line $line of file $file"
|
||||||
|
|
||||||
# The WARN_LOGFILE tag can be used to specify a file to which warning and error
|
# The WARN_LOGFILE tag can be used to specify a file to which warning and error
|
||||||
# messages should be written. If left blank the output is written to standard
|
# messages should be written. If left blank the output is written to standard
|
||||||
# error (stderr).
|
# error (stderr). In case the file specified cannot be opened for writing the
|
||||||
|
# warning and error messages are written to standard error. When as file - is
|
||||||
|
# specified the warning and error messages are written to standard output
|
||||||
|
# (stdout).
|
||||||
|
|
||||||
WARN_LOGFILE =
|
WARN_LOGFILE =
|
||||||
|
|
||||||
@@ -864,7 +906,13 @@ WARN_LOGFILE =
|
|||||||
# spaces. See also FILE_PATTERNS and EXTENSION_MAPPING
|
# spaces. See also FILE_PATTERNS and EXTENSION_MAPPING
|
||||||
# Note: If this tag is empty the current directory is searched.
|
# Note: If this tag is empty the current directory is searched.
|
||||||
|
|
||||||
INPUT = src/
|
INPUT = README.md \
|
||||||
|
core/app.js \
|
||||||
|
core/client.js \
|
||||||
|
core/core.js \
|
||||||
|
core/tfrpc.js \
|
||||||
|
docs/ \
|
||||||
|
src/
|
||||||
|
|
||||||
# This tag can be used to specify the character encoding of the source files
|
# This tag can be used to specify the character encoding of the source files
|
||||||
# that doxygen parses. Internally doxygen uses the UTF-8 encoding. Doxygen uses
|
# that doxygen parses. Internally doxygen uses the UTF-8 encoding. Doxygen uses
|
||||||
@@ -888,12 +936,14 @@ INPUT_ENCODING = UTF-8
|
|||||||
#
|
#
|
||||||
# If left blank the following patterns are tested:*.c, *.cc, *.cxx, *.cpp,
|
# If left blank the following patterns are tested:*.c, *.cc, *.cxx, *.cpp,
|
||||||
# *.c++, *.java, *.ii, *.ixx, *.ipp, *.i++, *.inl, *.idl, *.ddl, *.odl, *.h,
|
# *.c++, *.java, *.ii, *.ixx, *.ipp, *.i++, *.inl, *.idl, *.ddl, *.odl, *.h,
|
||||||
# *.hh, *.hxx, *.hpp, *.h++, *.cs, *.d, *.php, *.php4, *.php5, *.phtml, *.inc,
|
# *.hh, *.hxx, *.hpp, *.h++, *.l, *.cs, *.d, *.php, *.php4, *.php5, *.phtml,
|
||||||
# *.m, *.markdown, *.md, *.mm, *.dox (to be provided as doxygen C comment),
|
# *.inc, *.m, *.markdown, *.md, *.mm, *.dox (to be provided as doxygen C
|
||||||
# *.py, *.pyw, *.f90, *.f95, *.f03, *.f08, *.f18, *.f, *.for, *.vhd, *.vhdl,
|
# comment), *.py, *.pyw, *.f90, *.f95, *.f03, *.f08, *.f18, *.f, *.for, *.vhd,
|
||||||
# *.ucf, *.qsf and *.ice.
|
# *.vhdl, *.ucf, *.qsf and *.ice.
|
||||||
|
|
||||||
FILE_PATTERNS = *.h *.md
|
FILE_PATTERNS = *.h \
|
||||||
|
*.js \
|
||||||
|
*.md
|
||||||
|
|
||||||
# The RECURSIVE tag can be used to specify whether or not subdirectories should
|
# The RECURSIVE tag can be used to specify whether or not subdirectories should
|
||||||
# be searched for input files as well.
|
# be searched for input files as well.
|
||||||
@@ -930,7 +980,7 @@ EXCLUDE_PATTERNS =
|
|||||||
# (namespaces, classes, functions, etc.) that should be excluded from the
|
# (namespaces, classes, functions, etc.) that should be excluded from the
|
||||||
# output. The symbol name can be a fully qualified name, a word, or if the
|
# output. The symbol name can be a fully qualified name, a word, or if the
|
||||||
# wildcard * is used, a substring. Examples: ANamespace, AClass,
|
# wildcard * is used, a substring. Examples: ANamespace, AClass,
|
||||||
# AClass::ANamespace, ANamespace::*Test
|
# ANamespace::AClass, ANamespace::*Test
|
||||||
#
|
#
|
||||||
# Note that the wildcards are matched against the file with absolute path, so to
|
# Note that the wildcards are matched against the file with absolute path, so to
|
||||||
# exclude all test directories use the pattern */test/*
|
# exclude all test directories use the pattern */test/*
|
||||||
@@ -961,7 +1011,7 @@ EXAMPLE_RECURSIVE = NO
|
|||||||
# that contain images that are to be included in the documentation (see the
|
# that contain images that are to be included in the documentation (see the
|
||||||
# \image command).
|
# \image command).
|
||||||
|
|
||||||
IMAGE_PATH =
|
IMAGE_PATH = docs/images/
|
||||||
|
|
||||||
# The INPUT_FILTER tag can be used to specify a program that doxygen should
|
# The INPUT_FILTER tag can be used to specify a program that doxygen should
|
||||||
# invoke to filter for each input file. Doxygen will invoke the filter program
|
# invoke to filter for each input file. Doxygen will invoke the filter program
|
||||||
@@ -1017,7 +1067,7 @@ FILTER_SOURCE_PATTERNS =
|
|||||||
# (index.html). This can be useful if you have a project on for instance GitHub
|
# (index.html). This can be useful if you have a project on for instance GitHub
|
||||||
# and want to reuse the introduction page also for the doxygen output.
|
# and want to reuse the introduction page also for the doxygen output.
|
||||||
|
|
||||||
USE_MDFILE_AS_MAINPAGE =
|
USE_MDFILE_AS_MAINPAGE = README.md
|
||||||
|
|
||||||
#---------------------------------------------------------------------------
|
#---------------------------------------------------------------------------
|
||||||
# Configuration options related to source browsing
|
# Configuration options related to source browsing
|
||||||
@@ -1116,9 +1166,11 @@ VERBATIM_HEADERS = YES
|
|||||||
|
|
||||||
CLANG_ASSISTED_PARSING = NO
|
CLANG_ASSISTED_PARSING = NO
|
||||||
|
|
||||||
# If clang assisted parsing is enabled and the CLANG_ADD_INC_PATHS tag is set to
|
# If the CLANG_ASSISTED_PARSING tag is set to YES and the CLANG_ADD_INC_PATHS
|
||||||
# YES then doxygen will add the directory of each input to the include path.
|
# tag is set to YES then doxygen will add the directory of each input to the
|
||||||
|
# include path.
|
||||||
# The default value is: YES.
|
# The default value is: YES.
|
||||||
|
# This tag requires that the tag CLANG_ASSISTED_PARSING is set to YES.
|
||||||
|
|
||||||
CLANG_ADD_INC_PATHS = YES
|
CLANG_ADD_INC_PATHS = YES
|
||||||
|
|
||||||
@@ -1253,7 +1305,7 @@ HTML_EXTRA_FILES =
|
|||||||
|
|
||||||
# The HTML_COLORSTYLE_HUE tag controls the color of the HTML output. Doxygen
|
# The HTML_COLORSTYLE_HUE tag controls the color of the HTML output. Doxygen
|
||||||
# will adjust the colors in the style sheet and background images according to
|
# will adjust the colors in the style sheet and background images according to
|
||||||
# this color. Hue is specified as an angle on a colorwheel, see
|
# this color. Hue is specified as an angle on a color-wheel, see
|
||||||
# https://en.wikipedia.org/wiki/Hue for more information. For instance the value
|
# https://en.wikipedia.org/wiki/Hue for more information. For instance the value
|
||||||
# 0 represents red, 60 is yellow, 120 is green, 180 is cyan, 240 is blue, 300
|
# 0 represents red, 60 is yellow, 120 is green, 180 is cyan, 240 is blue, 300
|
||||||
# purple, and 360 is red again.
|
# purple, and 360 is red again.
|
||||||
@@ -1263,7 +1315,7 @@ HTML_EXTRA_FILES =
|
|||||||
HTML_COLORSTYLE_HUE = 220
|
HTML_COLORSTYLE_HUE = 220
|
||||||
|
|
||||||
# The HTML_COLORSTYLE_SAT tag controls the purity (or saturation) of the colors
|
# The HTML_COLORSTYLE_SAT tag controls the purity (or saturation) of the colors
|
||||||
# in the HTML output. For a value of 0 the output will use grayscales only. A
|
# in the HTML output. For a value of 0 the output will use gray-scales only. A
|
||||||
# value of 255 will produce the most vivid colors.
|
# value of 255 will produce the most vivid colors.
|
||||||
# Minimum value: 0, maximum value: 255, default value: 100.
|
# Minimum value: 0, maximum value: 255, default value: 100.
|
||||||
# This tag requires that the tag GENERATE_HTML is set to YES.
|
# This tag requires that the tag GENERATE_HTML is set to YES.
|
||||||
@@ -1288,7 +1340,7 @@ HTML_COLORSTYLE_GAMMA = 80
|
|||||||
# The default value is: NO.
|
# The default value is: NO.
|
||||||
# This tag requires that the tag GENERATE_HTML is set to YES.
|
# This tag requires that the tag GENERATE_HTML is set to YES.
|
||||||
|
|
||||||
HTML_TIMESTAMP = NO
|
#HTML_TIMESTAMP = NO
|
||||||
|
|
||||||
# If the HTML_DYNAMIC_MENUS tag is set to YES then the generated HTML
|
# If the HTML_DYNAMIC_MENUS tag is set to YES then the generated HTML
|
||||||
# documentation will contain a main index with vertical navigation menus that
|
# documentation will contain a main index with vertical navigation menus that
|
||||||
@@ -1345,6 +1397,13 @@ GENERATE_DOCSET = NO
|
|||||||
|
|
||||||
DOCSET_FEEDNAME = "Doxygen generated docs"
|
DOCSET_FEEDNAME = "Doxygen generated docs"
|
||||||
|
|
||||||
|
# This tag determines the URL of the docset feed. A documentation feed provides
|
||||||
|
# an umbrella under which multiple documentation sets from a single provider
|
||||||
|
# (such as a company or product suite) can be grouped.
|
||||||
|
# This tag requires that the tag GENERATE_DOCSET is set to YES.
|
||||||
|
|
||||||
|
DOCSET_FEEDURL =
|
||||||
|
|
||||||
# This tag specifies a string that should uniquely identify the documentation
|
# This tag specifies a string that should uniquely identify the documentation
|
||||||
# set bundle. This should be a reverse domain-name style string, e.g.
|
# set bundle. This should be a reverse domain-name style string, e.g.
|
||||||
# com.mycompany.MyDocSet. Doxygen will append .docset to the name.
|
# com.mycompany.MyDocSet. Doxygen will append .docset to the name.
|
||||||
@@ -1370,8 +1429,12 @@ DOCSET_PUBLISHER_NAME = Publisher
|
|||||||
# If the GENERATE_HTMLHELP tag is set to YES then doxygen generates three
|
# If the GENERATE_HTMLHELP tag is set to YES then doxygen generates three
|
||||||
# additional HTML index files: index.hhp, index.hhc, and index.hhk. The
|
# additional HTML index files: index.hhp, index.hhc, and index.hhk. The
|
||||||
# index.hhp is a project file that can be read by Microsoft's HTML Help Workshop
|
# index.hhp is a project file that can be read by Microsoft's HTML Help Workshop
|
||||||
# (see:
|
# on Windows. In the beginning of 2021 Microsoft took the original page, with
|
||||||
# https://www.microsoft.com/en-us/download/details.aspx?id=21138) on Windows.
|
# a.o. the download links, offline the HTML help workshop was already many years
|
||||||
|
# in maintenance mode). You can download the HTML help workshop from the web
|
||||||
|
# archives at Installation executable (see:
|
||||||
|
# http://web.archive.org/web/20160201063255/http://download.microsoft.com/downlo
|
||||||
|
# ad/0/A/9/0A939EF6-E31C-430F-A3DF-DFAE7960D564/htmlhelp.exe).
|
||||||
#
|
#
|
||||||
# The HTML Help Workshop contains a compiler that can convert all HTML output
|
# The HTML Help Workshop contains a compiler that can convert all HTML output
|
||||||
# generated by doxygen into a single compiled HTML file (.chm). Compiled HTML
|
# generated by doxygen into a single compiled HTML file (.chm). Compiled HTML
|
||||||
@@ -1530,15 +1593,27 @@ DISABLE_INDEX = NO
|
|||||||
# to work a browser that supports JavaScript, DHTML, CSS and frames is required
|
# to work a browser that supports JavaScript, DHTML, CSS and frames is required
|
||||||
# (i.e. any modern browser). Windows users are probably better off using the
|
# (i.e. any modern browser). Windows users are probably better off using the
|
||||||
# HTML help feature. Via custom style sheets (see HTML_EXTRA_STYLESHEET) one can
|
# HTML help feature. Via custom style sheets (see HTML_EXTRA_STYLESHEET) one can
|
||||||
# further fine-tune the look of the index. As an example, the default style
|
# further fine tune the look of the index (see "Fine-tuning the output"). As an
|
||||||
# sheet generated by doxygen has an example that shows how to put an image at
|
# example, the default style sheet generated by doxygen has an example that
|
||||||
# the root of the tree instead of the PROJECT_NAME. Since the tree basically has
|
# shows how to put an image at the root of the tree instead of the PROJECT_NAME.
|
||||||
# the same information as the tab index, you could consider setting
|
# Since the tree basically has the same information as the tab index, you could
|
||||||
# DISABLE_INDEX to YES when enabling this option.
|
# consider setting DISABLE_INDEX to YES when enabling this option.
|
||||||
# The default value is: NO.
|
# The default value is: NO.
|
||||||
# This tag requires that the tag GENERATE_HTML is set to YES.
|
# This tag requires that the tag GENERATE_HTML is set to YES.
|
||||||
|
|
||||||
GENERATE_TREEVIEW = NO
|
GENERATE_TREEVIEW = YES
|
||||||
|
|
||||||
|
# When both GENERATE_TREEVIEW and DISABLE_INDEX are set to YES, then the
|
||||||
|
# FULL_SIDEBAR option determines if the side bar is limited to only the treeview
|
||||||
|
# area (value NO) or if it should extend to the full height of the window (value
|
||||||
|
# YES). Setting this to YES gives a layout similar to
|
||||||
|
# https://docs.readthedocs.io with more room for contents, but less room for the
|
||||||
|
# project logo, title, and description. If either GENERATE_TREEVIEW or
|
||||||
|
# DISABLE_INDEX is set to NO, this option has no effect.
|
||||||
|
# The default value is: NO.
|
||||||
|
# This tag requires that the tag GENERATE_HTML is set to YES.
|
||||||
|
|
||||||
|
FULL_SIDEBAR = NO
|
||||||
|
|
||||||
# The ENUM_VALUES_PER_LINE tag can be used to set the number of enum values that
|
# The ENUM_VALUES_PER_LINE tag can be used to set the number of enum values that
|
||||||
# doxygen will group on one line in the generated HTML documentation.
|
# doxygen will group on one line in the generated HTML documentation.
|
||||||
@@ -1564,6 +1639,13 @@ TREEVIEW_WIDTH = 250
|
|||||||
|
|
||||||
EXT_LINKS_IN_WINDOW = NO
|
EXT_LINKS_IN_WINDOW = NO
|
||||||
|
|
||||||
|
# If the OBFUSCATE_EMAILS tag is set to YES, doxygen will obfuscate email
|
||||||
|
# addresses.
|
||||||
|
# The default value is: YES.
|
||||||
|
# This tag requires that the tag GENERATE_HTML is set to YES.
|
||||||
|
|
||||||
|
OBFUSCATE_EMAILS = YES
|
||||||
|
|
||||||
# If the HTML_FORMULA_FORMAT option is set to svg, doxygen will use the pdf2svg
|
# If the HTML_FORMULA_FORMAT option is set to svg, doxygen will use the pdf2svg
|
||||||
# tool (see https://github.com/dawbarton/pdf2svg) or inkscape (see
|
# tool (see https://github.com/dawbarton/pdf2svg) or inkscape (see
|
||||||
# https://inkscape.org) to generate formulas as SVG images instead of PNGs for
|
# https://inkscape.org) to generate formulas as SVG images instead of PNGs for
|
||||||
@@ -1593,7 +1675,7 @@ FORMULA_FONTSIZE = 10
|
|||||||
# The default value is: YES.
|
# The default value is: YES.
|
||||||
# This tag requires that the tag GENERATE_HTML is set to YES.
|
# This tag requires that the tag GENERATE_HTML is set to YES.
|
||||||
|
|
||||||
FORMULA_TRANSPARENT = YES
|
#FORMULA_TRANSPARENT = YES
|
||||||
|
|
||||||
# The FORMULA_MACROFILE can contain LaTeX \newcommand and \renewcommand commands
|
# The FORMULA_MACROFILE can contain LaTeX \newcommand and \renewcommand commands
|
||||||
# to create new LaTeX commands to be used in formulas as building blocks. See
|
# to create new LaTeX commands to be used in formulas as building blocks. See
|
||||||
@@ -1612,11 +1694,29 @@ FORMULA_MACROFILE =
|
|||||||
|
|
||||||
USE_MATHJAX = NO
|
USE_MATHJAX = NO
|
||||||
|
|
||||||
|
# With MATHJAX_VERSION it is possible to specify the MathJax version to be used.
|
||||||
|
# Note that the different versions of MathJax have different requirements with
|
||||||
|
# regards to the different settings, so it is possible that also other MathJax
|
||||||
|
# settings have to be changed when switching between the different MathJax
|
||||||
|
# versions.
|
||||||
|
# Possible values are: MathJax_2 and MathJax_3.
|
||||||
|
# The default value is: MathJax_2.
|
||||||
|
# This tag requires that the tag USE_MATHJAX is set to YES.
|
||||||
|
|
||||||
|
MATHJAX_VERSION = MathJax_2
|
||||||
|
|
||||||
# When MathJax is enabled you can set the default output format to be used for
|
# When MathJax is enabled you can set the default output format to be used for
|
||||||
# the MathJax output. See the MathJax site (see:
|
# the MathJax output. For more details about the output format see MathJax
|
||||||
# http://docs.mathjax.org/en/v2.7-latest/output.html) for more details.
|
# version 2 (see:
|
||||||
|
# http://docs.mathjax.org/en/v2.7-latest/output.html) and MathJax version 3
|
||||||
|
# (see:
|
||||||
|
# http://docs.mathjax.org/en/latest/web/components/output.html).
|
||||||
# Possible values are: HTML-CSS (which is slower, but has the best
|
# Possible values are: HTML-CSS (which is slower, but has the best
|
||||||
# compatibility), NativeMML (i.e. MathML) and SVG.
|
# compatibility. This is the name for Mathjax version 2, for MathJax version 3
|
||||||
|
# this will be translated into chtml), NativeMML (i.e. MathML. Only supported
|
||||||
|
# for NathJax 2. For MathJax version 3 chtml will be used instead.), chtml (This
|
||||||
|
# is the name for Mathjax version 3, for MathJax version 2 this will be
|
||||||
|
# translated into HTML-CSS) and SVG.
|
||||||
# The default value is: HTML-CSS.
|
# The default value is: HTML-CSS.
|
||||||
# This tag requires that the tag USE_MATHJAX is set to YES.
|
# This tag requires that the tag USE_MATHJAX is set to YES.
|
||||||
|
|
||||||
@@ -1629,15 +1729,21 @@ MATHJAX_FORMAT = HTML-CSS
|
|||||||
# MATHJAX_RELPATH should be ../mathjax. The default value points to the MathJax
|
# MATHJAX_RELPATH should be ../mathjax. The default value points to the MathJax
|
||||||
# Content Delivery Network so you can quickly see the result without installing
|
# Content Delivery Network so you can quickly see the result without installing
|
||||||
# MathJax. However, it is strongly recommended to install a local copy of
|
# MathJax. However, it is strongly recommended to install a local copy of
|
||||||
# MathJax from https://www.mathjax.org before deployment.
|
# MathJax from https://www.mathjax.org before deployment. The default value is:
|
||||||
# The default value is: https://cdn.jsdelivr.net/npm/mathjax@2.
|
# - in case of MathJax version 2: https://cdn.jsdelivr.net/npm/mathjax@2
|
||||||
|
# - in case of MathJax version 3: https://cdn.jsdelivr.net/npm/mathjax@3
|
||||||
# This tag requires that the tag USE_MATHJAX is set to YES.
|
# This tag requires that the tag USE_MATHJAX is set to YES.
|
||||||
|
|
||||||
MATHJAX_RELPATH = https://cdn.jsdelivr.net/npm/mathjax@2
|
MATHJAX_RELPATH = https://cdn.jsdelivr.net/npm/mathjax@2
|
||||||
|
|
||||||
# The MATHJAX_EXTENSIONS tag can be used to specify one or more MathJax
|
# The MATHJAX_EXTENSIONS tag can be used to specify one or more MathJax
|
||||||
# extension names that should be enabled during MathJax rendering. For example
|
# extension names that should be enabled during MathJax rendering. For example
|
||||||
|
# for MathJax version 2 (see https://docs.mathjax.org/en/v2.7-latest/tex.html
|
||||||
|
# #tex-and-latex-extensions):
|
||||||
# MATHJAX_EXTENSIONS = TeX/AMSmath TeX/AMSsymbols
|
# MATHJAX_EXTENSIONS = TeX/AMSmath TeX/AMSsymbols
|
||||||
|
# For example for MathJax version 3 (see
|
||||||
|
# http://docs.mathjax.org/en/latest/input/tex/extensions/index.html):
|
||||||
|
# MATHJAX_EXTENSIONS = ams
|
||||||
# This tag requires that the tag USE_MATHJAX is set to YES.
|
# This tag requires that the tag USE_MATHJAX is set to YES.
|
||||||
|
|
||||||
MATHJAX_EXTENSIONS =
|
MATHJAX_EXTENSIONS =
|
||||||
@@ -1817,29 +1923,31 @@ PAPER_TYPE = a4
|
|||||||
|
|
||||||
EXTRA_PACKAGES =
|
EXTRA_PACKAGES =
|
||||||
|
|
||||||
# The LATEX_HEADER tag can be used to specify a personal LaTeX header for the
|
# The LATEX_HEADER tag can be used to specify a user-defined LaTeX header for
|
||||||
# generated LaTeX document. The header should contain everything until the first
|
# the generated LaTeX document. The header should contain everything until the
|
||||||
# chapter. If it is left blank doxygen will generate a standard header. See
|
# first chapter. If it is left blank doxygen will generate a standard header. It
|
||||||
# section "Doxygen usage" for information on how to let doxygen write the
|
# is highly recommended to start with a default header using
|
||||||
# default header to a separate file.
|
# doxygen -w latex new_header.tex new_footer.tex new_stylesheet.sty
|
||||||
|
# and then modify the file new_header.tex. See also section "Doxygen usage" for
|
||||||
|
# information on how to generate the default header that doxygen normally uses.
|
||||||
#
|
#
|
||||||
# Note: Only use a user-defined header if you know what you are doing! The
|
# Note: Only use a user-defined header if you know what you are doing!
|
||||||
# following commands have a special meaning inside the header: $title,
|
# Note: The header is subject to change so you typically have to regenerate the
|
||||||
# $datetime, $date, $doxygenversion, $projectname, $projectnumber,
|
# default header when upgrading to a newer version of doxygen. The following
|
||||||
# $projectbrief, $projectlogo. Doxygen will replace $title with the empty
|
# commands have a special meaning inside the header (and footer): For a
|
||||||
# string, for the replacement values of the other commands the user is referred
|
# description of the possible markers and block names see the documentation.
|
||||||
# to HTML_HEADER.
|
|
||||||
# This tag requires that the tag GENERATE_LATEX is set to YES.
|
# This tag requires that the tag GENERATE_LATEX is set to YES.
|
||||||
|
|
||||||
LATEX_HEADER =
|
LATEX_HEADER =
|
||||||
|
|
||||||
# The LATEX_FOOTER tag can be used to specify a personal LaTeX footer for the
|
# The LATEX_FOOTER tag can be used to specify a user-defined LaTeX footer for
|
||||||
# generated LaTeX document. The footer should contain everything after the last
|
# the generated LaTeX document. The footer should contain everything after the
|
||||||
# chapter. If it is left blank doxygen will generate a standard footer. See
|
# last chapter. If it is left blank doxygen will generate a standard footer. See
|
||||||
# LATEX_HEADER for more information on how to generate a default footer and what
|
# LATEX_HEADER for more information on how to generate a default footer and what
|
||||||
# special commands can be used inside the footer.
|
# special commands can be used inside the footer. See also section "Doxygen
|
||||||
#
|
# usage" for information on how to generate the default footer that doxygen
|
||||||
# Note: Only use a user-defined footer if you know what you are doing!
|
# normally uses. Note: Only use a user-defined footer if you know what you are
|
||||||
|
# doing!
|
||||||
# This tag requires that the tag GENERATE_LATEX is set to YES.
|
# This tag requires that the tag GENERATE_LATEX is set to YES.
|
||||||
|
|
||||||
LATEX_FOOTER =
|
LATEX_FOOTER =
|
||||||
@@ -1884,8 +1992,7 @@ USE_PDFLATEX = YES
|
|||||||
|
|
||||||
# If the LATEX_BATCHMODE tag is set to YES, doxygen will add the \batchmode
|
# If the LATEX_BATCHMODE tag is set to YES, doxygen will add the \batchmode
|
||||||
# command to the generated LaTeX files. This will instruct LaTeX to keep running
|
# command to the generated LaTeX files. This will instruct LaTeX to keep running
|
||||||
# if errors occur, instead of asking the user for help. This option is also used
|
# if errors occur, instead of asking the user for help.
|
||||||
# when generating formulas in HTML.
|
|
||||||
# The default value is: NO.
|
# The default value is: NO.
|
||||||
# This tag requires that the tag GENERATE_LATEX is set to YES.
|
# This tag requires that the tag GENERATE_LATEX is set to YES.
|
||||||
|
|
||||||
@@ -1898,16 +2005,6 @@ LATEX_BATCHMODE = NO
|
|||||||
|
|
||||||
LATEX_HIDE_INDICES = NO
|
LATEX_HIDE_INDICES = NO
|
||||||
|
|
||||||
# If the LATEX_SOURCE_CODE tag is set to YES then doxygen will include source
|
|
||||||
# code with syntax highlighting in the LaTeX output.
|
|
||||||
#
|
|
||||||
# Note that which sources are shown also depends on other settings such as
|
|
||||||
# SOURCE_BROWSER.
|
|
||||||
# The default value is: NO.
|
|
||||||
# This tag requires that the tag GENERATE_LATEX is set to YES.
|
|
||||||
|
|
||||||
LATEX_SOURCE_CODE = NO
|
|
||||||
|
|
||||||
# The LATEX_BIB_STYLE tag can be used to specify the style to use for the
|
# The LATEX_BIB_STYLE tag can be used to specify the style to use for the
|
||||||
# bibliography, e.g. plainnat, or ieeetr. See
|
# bibliography, e.g. plainnat, or ieeetr. See
|
||||||
# https://en.wikipedia.org/wiki/BibTeX and \cite for more info.
|
# https://en.wikipedia.org/wiki/BibTeX and \cite for more info.
|
||||||
@@ -1922,7 +2019,7 @@ LATEX_BIB_STYLE = plain
|
|||||||
# The default value is: NO.
|
# The default value is: NO.
|
||||||
# This tag requires that the tag GENERATE_LATEX is set to YES.
|
# This tag requires that the tag GENERATE_LATEX is set to YES.
|
||||||
|
|
||||||
LATEX_TIMESTAMP = NO
|
#LATEX_TIMESTAMP = NO
|
||||||
|
|
||||||
# The LATEX_EMOJI_DIRECTORY tag is used to specify the (relative or absolute)
|
# The LATEX_EMOJI_DIRECTORY tag is used to specify the (relative or absolute)
|
||||||
# path from which the emoji images will be read. If a relative path is entered,
|
# path from which the emoji images will be read. If a relative path is entered,
|
||||||
@@ -1988,16 +2085,6 @@ RTF_STYLESHEET_FILE =
|
|||||||
|
|
||||||
RTF_EXTENSIONS_FILE =
|
RTF_EXTENSIONS_FILE =
|
||||||
|
|
||||||
# If the RTF_SOURCE_CODE tag is set to YES then doxygen will include source code
|
|
||||||
# with syntax highlighting in the RTF output.
|
|
||||||
#
|
|
||||||
# Note that which sources are shown also depends on other settings such as
|
|
||||||
# SOURCE_BROWSER.
|
|
||||||
# The default value is: NO.
|
|
||||||
# This tag requires that the tag GENERATE_RTF is set to YES.
|
|
||||||
|
|
||||||
RTF_SOURCE_CODE = NO
|
|
||||||
|
|
||||||
#---------------------------------------------------------------------------
|
#---------------------------------------------------------------------------
|
||||||
# Configuration options related to the man page output
|
# Configuration options related to the man page output
|
||||||
#---------------------------------------------------------------------------
|
#---------------------------------------------------------------------------
|
||||||
@@ -2094,15 +2181,6 @@ GENERATE_DOCBOOK = NO
|
|||||||
|
|
||||||
DOCBOOK_OUTPUT = docbook
|
DOCBOOK_OUTPUT = docbook
|
||||||
|
|
||||||
# If the DOCBOOK_PROGRAMLISTING tag is set to YES, doxygen will include the
|
|
||||||
# program listings (including syntax highlighting and cross-referencing
|
|
||||||
# information) to the DOCBOOK output. Note that enabling this will significantly
|
|
||||||
# increase the size of the DOCBOOK output.
|
|
||||||
# The default value is: NO.
|
|
||||||
# This tag requires that the tag GENERATE_DOCBOOK is set to YES.
|
|
||||||
|
|
||||||
DOCBOOK_PROGRAMLISTING = NO
|
|
||||||
|
|
||||||
#---------------------------------------------------------------------------
|
#---------------------------------------------------------------------------
|
||||||
# Configuration options for the AutoGen Definitions output
|
# Configuration options for the AutoGen Definitions output
|
||||||
#---------------------------------------------------------------------------
|
#---------------------------------------------------------------------------
|
||||||
@@ -2189,7 +2267,8 @@ SEARCH_INCLUDES = YES
|
|||||||
|
|
||||||
# The INCLUDE_PATH tag can be used to specify one or more directories that
|
# The INCLUDE_PATH tag can be used to specify one or more directories that
|
||||||
# contain include files that are not input files but should be processed by the
|
# contain include files that are not input files but should be processed by the
|
||||||
# preprocessor.
|
# preprocessor. Note that the INCLUDE_PATH is not recursive, so the setting of
|
||||||
|
# RECURSIVE has no effect here.
|
||||||
# This tag requires that the tag SEARCH_INCLUDES is set to YES.
|
# This tag requires that the tag SEARCH_INCLUDES is set to YES.
|
||||||
|
|
||||||
INCLUDE_PATH =
|
INCLUDE_PATH =
|
||||||
@@ -2281,15 +2360,6 @@ EXTERNAL_PAGES = YES
|
|||||||
# Configuration options related to the dot tool
|
# Configuration options related to the dot tool
|
||||||
#---------------------------------------------------------------------------
|
#---------------------------------------------------------------------------
|
||||||
|
|
||||||
# If the CLASS_DIAGRAMS tag is set to YES, doxygen will generate a class diagram
|
|
||||||
# (in HTML and LaTeX) for classes with base or super classes. Setting the tag to
|
|
||||||
# NO turns the diagrams off. Note that this option also works with HAVE_DOT
|
|
||||||
# disabled, but it is recommended to install and use dot, since it yields more
|
|
||||||
# powerful graphs.
|
|
||||||
# The default value is: YES.
|
|
||||||
|
|
||||||
CLASS_DIAGRAMS = YES
|
|
||||||
|
|
||||||
# You can include diagrams made with dia in doxygen documentation. Doxygen will
|
# You can include diagrams made with dia in doxygen documentation. Doxygen will
|
||||||
# then run dia to produce the diagram and insert it in the documentation. The
|
# then run dia to produce the diagram and insert it in the documentation. The
|
||||||
# DIA_PATH tag allows you to specify the directory where the dia binary resides.
|
# DIA_PATH tag allows you to specify the directory where the dia binary resides.
|
||||||
@@ -2308,7 +2378,7 @@ HIDE_UNDOC_RELATIONS = YES
|
|||||||
# http://www.graphviz.org/), a graph visualization toolkit from AT&T and Lucent
|
# http://www.graphviz.org/), a graph visualization toolkit from AT&T and Lucent
|
||||||
# Bell Labs. The other options in this section have no effect if this option is
|
# Bell Labs. The other options in this section have no effect if this option is
|
||||||
# set to NO
|
# set to NO
|
||||||
# The default value is: YES.
|
# The default value is: NO.
|
||||||
|
|
||||||
HAVE_DOT = YES
|
HAVE_DOT = YES
|
||||||
|
|
||||||
@@ -2330,27 +2400,30 @@ DOT_NUM_THREADS = 0
|
|||||||
# The default value is: Helvetica.
|
# The default value is: Helvetica.
|
||||||
# This tag requires that the tag HAVE_DOT is set to YES.
|
# This tag requires that the tag HAVE_DOT is set to YES.
|
||||||
|
|
||||||
DOT_FONTNAME = Helvetica
|
#DOT_FONTNAME = Helvetica
|
||||||
|
|
||||||
# The DOT_FONTSIZE tag can be used to set the size (in points) of the font of
|
# The DOT_FONTSIZE tag can be used to set the size (in points) of the font of
|
||||||
# dot graphs.
|
# dot graphs.
|
||||||
# Minimum value: 4, maximum value: 24, default value: 10.
|
# Minimum value: 4, maximum value: 24, default value: 10.
|
||||||
# This tag requires that the tag HAVE_DOT is set to YES.
|
# This tag requires that the tag HAVE_DOT is set to YES.
|
||||||
|
|
||||||
DOT_FONTSIZE = 10
|
#DOT_FONTSIZE = 10
|
||||||
|
|
||||||
# By default doxygen will tell dot to use the default font as specified with
|
# By default doxygen will tell dot to use the default font as specified with
|
||||||
# DOT_FONTNAME. If you specify a different font using DOT_FONTNAME you can set
|
# DOT_FONTNAME. If you specify a different font using DOT_FONTNAME you can set
|
||||||
# the path where dot can find it using this tag.
|
# the path where dot can find it using this tag.
|
||||||
# This tag requires that the tag HAVE_DOT is set to YES.
|
# This tag requires that the tag HAVE_DOT is set to YES.
|
||||||
|
|
||||||
DOT_FONTPATH =
|
#DOT_FONTPATH =
|
||||||
|
|
||||||
# If the CLASS_GRAPH tag is set to YES then doxygen will generate a graph for
|
# If the CLASS_GRAPH tag is set to YES (or GRAPH) then doxygen will generate a
|
||||||
# each documented class showing the direct and indirect inheritance relations.
|
# graph for each documented class showing the direct and indirect inheritance
|
||||||
# Setting this tag to YES will force the CLASS_DIAGRAMS tag to NO.
|
# relations. In case HAVE_DOT is set as well dot will be used to draw the graph,
|
||||||
|
# otherwise the built-in generator will be used. If the CLASS_GRAPH tag is set
|
||||||
|
# to TEXT the direct and indirect inheritance relations will be shown as texts /
|
||||||
|
# links.
|
||||||
|
# Possible values are: NO, YES, TEXT and GRAPH.
|
||||||
# The default value is: YES.
|
# The default value is: YES.
|
||||||
# This tag requires that the tag HAVE_DOT is set to YES.
|
|
||||||
|
|
||||||
CLASS_GRAPH = YES
|
CLASS_GRAPH = YES
|
||||||
|
|
||||||
@@ -2364,7 +2437,8 @@ CLASS_GRAPH = YES
|
|||||||
COLLABORATION_GRAPH = YES
|
COLLABORATION_GRAPH = YES
|
||||||
|
|
||||||
# If the GROUP_GRAPHS tag is set to YES then doxygen will generate a graph for
|
# If the GROUP_GRAPHS tag is set to YES then doxygen will generate a graph for
|
||||||
# groups, showing the direct groups dependencies.
|
# groups, showing the direct groups dependencies. See also the chapter Grouping
|
||||||
|
# in the manual.
|
||||||
# The default value is: YES.
|
# The default value is: YES.
|
||||||
# This tag requires that the tag HAVE_DOT is set to YES.
|
# This tag requires that the tag HAVE_DOT is set to YES.
|
||||||
|
|
||||||
@@ -2479,6 +2553,13 @@ GRAPHICAL_HIERARCHY = YES
|
|||||||
|
|
||||||
DIRECTORY_GRAPH = YES
|
DIRECTORY_GRAPH = YES
|
||||||
|
|
||||||
|
# The DIR_GRAPH_MAX_DEPTH tag can be used to limit the maximum number of levels
|
||||||
|
# of child directories generated in directory dependency graphs by dot.
|
||||||
|
# Minimum value: 1, maximum value: 25, default value: 1.
|
||||||
|
# This tag requires that the tag DIRECTORY_GRAPH is set to YES.
|
||||||
|
|
||||||
|
DIR_GRAPH_MAX_DEPTH = 1
|
||||||
|
|
||||||
# The DOT_IMAGE_FORMAT tag can be used to set the image format of the images
|
# The DOT_IMAGE_FORMAT tag can be used to set the image format of the images
|
||||||
# generated by dot. For an explanation of the image formats see the section
|
# generated by dot. For an explanation of the image formats see the section
|
||||||
# output formats in the documentation of the dot tool (Graphviz (see:
|
# output formats in the documentation of the dot tool (Graphviz (see:
|
||||||
@@ -2486,9 +2567,7 @@ DIRECTORY_GRAPH = YES
|
|||||||
# Note: If you choose svg you need to set HTML_FILE_EXTENSION to xhtml in order
|
# Note: If you choose svg you need to set HTML_FILE_EXTENSION to xhtml in order
|
||||||
# to make the SVG files visible in IE 9+ (other browsers do not have this
|
# to make the SVG files visible in IE 9+ (other browsers do not have this
|
||||||
# requirement).
|
# requirement).
|
||||||
# Possible values are: png, png:cairo, png:cairo:cairo, png:cairo:gd, png:gd,
|
# Possible values are: png, jpg, gif, svg, png:gd, png:gd:gd, png:cairo,
|
||||||
# png:gd:gd, jpg, jpg:cairo, jpg:cairo:gd, jpg:gd, jpg:gd:gd, gif, gif:cairo,
|
|
||||||
# gif:cairo:gd, gif:gd, gif:gd:gd, svg, png:gd, png:gd:gd, png:cairo,
|
|
||||||
# png:cairo:gd, png:cairo:cairo, png:cairo:gdiplus, png:gdiplus and
|
# png:cairo:gd, png:cairo:cairo, png:cairo:gdiplus, png:gdiplus and
|
||||||
# png:gdiplus:gdiplus.
|
# png:gdiplus:gdiplus.
|
||||||
# The default value is: png.
|
# The default value is: png.
|
||||||
@@ -2534,10 +2613,10 @@ MSCFILE_DIRS =
|
|||||||
DIAFILE_DIRS =
|
DIAFILE_DIRS =
|
||||||
|
|
||||||
# When using plantuml, the PLANTUML_JAR_PATH tag should be used to specify the
|
# When using plantuml, the PLANTUML_JAR_PATH tag should be used to specify the
|
||||||
# path where java can find the plantuml.jar file. If left blank, it is assumed
|
# path where java can find the plantuml.jar file or to the filename of jar file
|
||||||
# PlantUML is not used or called during a preprocessing step. Doxygen will
|
# to be used. If left blank, it is assumed PlantUML is not used or called during
|
||||||
# generate a warning when it encounters a \startuml command in this case and
|
# a preprocessing step. Doxygen will generate a warning when it encounters a
|
||||||
# will not generate output for the diagram.
|
# \startuml command in this case and will not generate output for the diagram.
|
||||||
|
|
||||||
PLANTUML_JAR_PATH =
|
PLANTUML_JAR_PATH =
|
||||||
|
|
||||||
@@ -2585,7 +2664,7 @@ MAX_DOT_GRAPH_DEPTH = 0
|
|||||||
# The default value is: NO.
|
# The default value is: NO.
|
||||||
# This tag requires that the tag HAVE_DOT is set to YES.
|
# This tag requires that the tag HAVE_DOT is set to YES.
|
||||||
|
|
||||||
DOT_TRANSPARENT = NO
|
#DOT_TRANSPARENT = NO
|
||||||
|
|
||||||
# Set the DOT_MULTI_TARGETS tag to YES to allow dot to generate multiple output
|
# Set the DOT_MULTI_TARGETS tag to YES to allow dot to generate multiple output
|
||||||
# files in one run (i.e. multiple -o and -T options on the command line). This
|
# files in one run (i.e. multiple -o and -T options on the command line). This
|
||||||
@@ -2599,6 +2678,8 @@ DOT_MULTI_TARGETS = NO
|
|||||||
# If the GENERATE_LEGEND tag is set to YES doxygen will generate a legend page
|
# If the GENERATE_LEGEND tag is set to YES doxygen will generate a legend page
|
||||||
# explaining the meaning of the various boxes and arrows in the dot generated
|
# explaining the meaning of the various boxes and arrows in the dot generated
|
||||||
# graphs.
|
# graphs.
|
||||||
|
# Note: This tag requires that UML_LOOK isn't set, i.e. the doxygen internal
|
||||||
|
# graphical representation for inheritance and collaboration diagrams is used.
|
||||||
# The default value is: YES.
|
# The default value is: YES.
|
||||||
# This tag requires that the tag HAVE_DOT is set to YES.
|
# This tag requires that the tag HAVE_DOT is set to YES.
|
||||||
|
|
||||||
@@ -2607,8 +2688,8 @@ GENERATE_LEGEND = YES
|
|||||||
# If the DOT_CLEANUP tag is set to YES, doxygen will remove the intermediate
|
# If the DOT_CLEANUP tag is set to YES, doxygen will remove the intermediate
|
||||||
# files that are used to generate the various graphs.
|
# files that are used to generate the various graphs.
|
||||||
#
|
#
|
||||||
# Note: This setting is not only used for dot files but also for msc and
|
# Note: This setting is not only used for dot files but also for msc temporary
|
||||||
# plantuml temporary files.
|
# files.
|
||||||
# The default value is: YES.
|
# The default value is: YES.
|
||||||
|
|
||||||
DOT_CLEANUP = YES
|
DOT_CLEANUP = YES
|
||||||
|
|||||||
769
GNUmakefile
769
GNUmakefile
File diff suppressed because it is too large
Load Diff
69
README.md
69
README.md
@@ -1,47 +1,68 @@
|
|||||||
# Tilde Friends
|
# Tilde Friends
|
||||||
|
|
||||||
Tilde Friends is a tool for making and sharing.
|
Tilde Friends participates in the Secure Scuttlebutt decentralized social
|
||||||
|
network while also functioning as a platform for making, sharing, and running
|
||||||
|
web applications.
|
||||||
|
|
||||||
A public instance lives at https://www.tildefriends.net/.
|
A public instance lives at https://www.tildefriends.net/.
|
||||||
|
|
||||||
It is both a peer-to-peer social network client, participating in Secure
|
|
||||||
Scuttlebutt, as well as a platform for writing and running web applications.
|
|
||||||
|
|
||||||
## Goals
|
## Goals
|
||||||
|
|
||||||
1. Make it easy and fun to run all sorts of web applications.
|
1. Be the fanciest, best-maintained Secure Scuttlebutt client in town.
|
||||||
2. Provide security that is easy to understand and protects your data.
|
1. Make it easy to make, share, and run all sorts of applications while
|
||||||
3. Make creating and sharing web applications accessible to anyone with a
|
respecting the privacy and safety of your data.
|
||||||
browser.
|
|
||||||
|
## Getting the Source
|
||||||
|
|
||||||
|
Tilde Friends uses git submodules, so either:
|
||||||
|
|
||||||
|
```
|
||||||
|
git clone --recurse-submodules https://dev.tildefriends.net/cory/tildefriends.git
|
||||||
|
```
|
||||||
|
|
||||||
|
or:
|
||||||
|
|
||||||
|
```
|
||||||
|
git clone https://dev.tildefriends.net/cory/tildefriends.git
|
||||||
|
cd tildefriends
|
||||||
|
git submodule update --init --recursive
|
||||||
|
```
|
||||||
|
|
||||||
|
The `.tar.xz` source releases are all-inclusive.
|
||||||
|
|
||||||
## Building
|
## Building
|
||||||
|
|
||||||
Builds on Linux (x86_64 and aarch64), MacOS, OpenBSD, and Haiku. Builds for
|
Builds on Linux (x86_64 and aarch64), MacOS, OpenBSD, and Haiku. It's possible
|
||||||
all of those host platforms plus mingw64, iOS, and android.
|
to build for Android, iOS, and Windows on Linux, if you have the right
|
||||||
|
dependencies in the right places.
|
||||||
|
|
||||||
1. Requires openssl (`libssl-dev`, in debian-speak). All other dependencies
|
### Requirements
|
||||||
are kept up to date in the tree.
|
|
||||||
2. To build, run `make debug` or `make release`. An executable will be
|
On MacOS, Xcode's command-line tools are expected to be available.
|
||||||
generated in a subdirectory of `out/`.
|
|
||||||
3. It's possible to build for Android, iOS, and Windows on Linux, if you have
|
### Build Commands
|
||||||
the right dependencies in the right places. `make windebug winrelease
|
|
||||||
iosdebug-ipa iosrelease-ipa release-apk`.
|
Run `make` with no arguments to see available build targets and options. `make
|
||||||
4. To build in docker, `docker build .`.
|
debug` is a good place to start.
|
||||||
5. `make format` will normalize formatting to the coding standard.
|
|
||||||
|
To build in docker, `docker build .`.
|
||||||
|
|
||||||
|
`make format` and `make prettier` will normalize formatting to the coding
|
||||||
|
standard.
|
||||||
|
|
||||||
## Running
|
## Running
|
||||||
|
|
||||||
By default, running the built `tildefriends` executable will start a web server
|
By default, running the built `out/debug/tildefriends` executable will start a
|
||||||
at <http://localhost:12345/>. `tildefriends -h` lists further options.
|
web server at <http://localhost:12345/>. `tildefriends -h` lists further
|
||||||
|
options.
|
||||||
|
|
||||||
The first user to create an account and log in will be granted administrative
|
The first user to create an account and log in will be granted administrative
|
||||||
privileges. Further administration can be done at
|
privileges. Further administration can be done in the `admin` app at
|
||||||
<http://localhost:12345/~core/admin/>.
|
<http://localhost:12345/~core/admin/>.
|
||||||
|
|
||||||
## Documentation
|
## Documentation
|
||||||
|
|
||||||
Docs are a work in progress:
|
Docs live here: <https://docs.tildefriends.net/>.
|
||||||
<https://www.tildefriends.net/~cory/wiki/#test-wiki/tf-app-quick-reference>.
|
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"type": "tildefriends-app",
|
"type": "tildefriends-app",
|
||||||
"emoji": "🎛",
|
"emoji": "🎛",
|
||||||
"previous": "&vrpS/vE7n588iYv1p8HafDxHB+YDHTrtUbJiu9nGA9I=.sha256"
|
"previous": "&kmKNyb/uaXNb24gCinJtfS8iWx4cLUWdtl0y2DwEUas=.sha256"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,8 @@
|
|||||||
<script>
|
<script>
|
||||||
const g_data = $data;
|
const g_data = $data;
|
||||||
</script>
|
</script>
|
||||||
<link rel="stylesheet" href="w3.css"></link>
|
<link rel="stylesheet" href="w3.css" />
|
||||||
|
<!-- prettier-ignore -->
|
||||||
<style>
|
<style>
|
||||||
/* 2018 Valiant Poppy */
|
/* 2018 Valiant Poppy */
|
||||||
.w3-theme-l5 {color:#000 !important; background-color:#fbf3f3 !important}
|
.w3-theme-l5 {color:#000 !important; background-color:#fbf3f3 !important}
|
||||||
|
|||||||
@@ -27,31 +27,55 @@ function global_settings_set(key, value) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function title_case(name) {
|
||||||
|
return name
|
||||||
|
.split('_')
|
||||||
|
.map((x) => x.charAt(0).toUpperCase() + x.substring(1))
|
||||||
|
.join(' ');
|
||||||
|
}
|
||||||
|
|
||||||
window.addEventListener('load', function () {
|
window.addEventListener('load', function () {
|
||||||
const permission_template = (permission) => html` <code>${permission}</code>`;
|
const permission_template = (permission) => html` <code>${permission}</code>`;
|
||||||
function input_template(key, description) {
|
function input_template(key, description) {
|
||||||
if (description.type === 'boolean') {
|
if (description.type === 'boolean') {
|
||||||
return html`
|
return html`
|
||||||
<li class="w3-row">
|
<li class="w3-row">
|
||||||
<label class="w3-quarter" for=${'gs_' + key} style="font-weight: bold">${key}</label>
|
<label class="w3-quarter" for=${'gs_' + key} style="font-weight: bold">${title_case(key)}</label>
|
||||||
<div class="w3-quarter w3-padding">${description.description}</div>
|
<div class="w3-quarter w3-padding">${description.description}</div>
|
||||||
<input class="w3-quarter w3-check" type="checkbox" ?checked=${description.value} id=${'gs_' + key}></input>
|
<div class="w3-quarter w3-padding w3-center"><input class="w3-check" type="checkbox" ?checked=${description.value} id=${'gs_' + key}></input></div>
|
||||||
<button class="w3-quarter w3-button w3-theme-action" @click=${(e) => global_settings_set(key, e.srcElement.previousElementSibling.checked)}>Set</button>
|
<button class="w3-quarter w3-button w3-theme-action" @click=${(e) => global_settings_set(key, e.srcElement.previousElementSibling.firstChild.checked)}>Set</button>
|
||||||
</li>
|
</li>
|
||||||
`;
|
`;
|
||||||
} else if (description.type === 'textarea') {
|
} else if (description.type === 'textarea') {
|
||||||
return html`
|
return html`
|
||||||
<li class="w3-row">
|
<li class="w3-row">
|
||||||
<label class="w3-quarter" for=${'gs_' + key} style="font-weight: bold">${key}</label>
|
<label class="w3-quarter" for=${'gs_' + key} style="font-weight: bold"
|
||||||
|
>${title_case(key)}</label
|
||||||
|
>
|
||||||
<div class="w3-rest w3-padding">${description.description}</div>
|
<div class="w3-rest w3-padding">${description.description}</div>
|
||||||
<textarea class="w3-input" style="vertical-align: top; resize: vertical" id=${'gs_' + key}>${description.value}</textarea>
|
<textarea
|
||||||
<button class="w3-button w3-right w3-quarter w3-theme-action" @click=${(e) => global_settings_set(key, e.srcElement.previousElementSibling.value)}>Set</button>
|
class="w3-input"
|
||||||
|
style="vertical-align: top; resize: vertical"
|
||||||
|
id=${'gs_' + key}
|
||||||
|
>
|
||||||
|
${description.value}</textarea
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
class="w3-button w3-right w3-quarter w3-theme-action"
|
||||||
|
@click=${(e) =>
|
||||||
|
global_settings_set(
|
||||||
|
key,
|
||||||
|
e.srcElement.previousElementSibling.value
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
Set
|
||||||
|
</button>
|
||||||
</li>
|
</li>
|
||||||
`;
|
`;
|
||||||
} else {
|
} else if (description.type != 'hidden') {
|
||||||
return html`
|
return html`
|
||||||
<li class="w3-row">
|
<li class="w3-row">
|
||||||
<label class="w3-quarter" for=${'gs_' + key} style="font-weight: bold">${key}</label>
|
<label class="w3-quarter" for=${'gs_' + key} style="font-weight: bold">${title_case(key)}</label>
|
||||||
<div class="w3-quarter w3-padding">${description.description}</div>
|
<div class="w3-quarter w3-padding">${description.description}</div>
|
||||||
<input class="w3-input w3-quarter" type="text" value="${description.value}" id=${'gs_' + key}></input>
|
<input class="w3-input w3-quarter" type="text" value="${description.value}" id=${'gs_' + key}></input>
|
||||||
<button class="w3-button w3-quarter w3-theme-action" @click=${(e) => global_settings_set(key, e.srcElement.previousElementSibling.value)}>Set</button>
|
<button class="w3-button w3-quarter w3-theme-action" @click=${(e) => global_settings_set(key, e.srcElement.previousElementSibling.value)}>Set</button>
|
||||||
@@ -61,13 +85,17 @@ window.addEventListener('load', function () {
|
|||||||
}
|
}
|
||||||
const user_template = (user, permissions) => html`
|
const user_template = (user, permissions) => html`
|
||||||
<li class="w3-card w3-margin">
|
<li class="w3-card w3-margin">
|
||||||
<button class="w3-button w3-theme-action" @click=${(e) => delete_user(user)}>Delete</button>
|
<button
|
||||||
|
class="w3-button w3-theme-action"
|
||||||
|
@click=${(e) => delete_user(user)}
|
||||||
|
>
|
||||||
|
Delete
|
||||||
|
</button>
|
||||||
${user}: ${permissions.map((x) => permission_template(x))}
|
${user}: ${permissions.map((x) => permission_template(x))}
|
||||||
</li>
|
</li>
|
||||||
`;
|
`;
|
||||||
const users_template = (users) =>
|
const users_template = (users) =>
|
||||||
html`
|
html` <header class="w3-container w3-theme-l2"><h2>Users</h2></header>
|
||||||
<header class="w3-container w3-theme-l2"><h2>Users</h2></header>
|
|
||||||
<ul class="w3-ul">
|
<ul class="w3-ul">
|
||||||
${Object.entries(users).map((u) => user_template(u[0], u[1]))}
|
${Object.entries(users).map((u) => user_template(u[0], u[1]))}
|
||||||
</ul>`;
|
</ul>`;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
/* W3.CSS 4.15 December 2020 by Jan Egil and Borge Refsnes */
|
/* W3.CSS 5.02 March 31 2025 by Jan Egil and Borge Refsnes */
|
||||||
html{box-sizing:border-box}*,*:before,*:after{box-sizing:inherit}
|
html{box-sizing:border-box}*,*:before,*:after{box-sizing:inherit}
|
||||||
/* Extract from normalize.css by Nicolas Gallagher and Jonathan Neal git.io/normalize */
|
/* Extract from normalize.css by Nicolas Gallagher and Jonathan Neal git.io/normalize */
|
||||||
html{-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%}body{margin:0}
|
html{-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%}body{margin:0}
|
||||||
@@ -108,6 +108,8 @@ hr{border:0;border-top:1px solid #eee;margin:20px 0}
|
|||||||
.w3-round-small{border-radius:2px}.w3-round,.w3-round-medium{border-radius:4px}.w3-round-large{border-radius:8px}.w3-round-xlarge{border-radius:16px}.w3-round-xxlarge{border-radius:32px}
|
.w3-round-small{border-radius:2px}.w3-round,.w3-round-medium{border-radius:4px}.w3-round-large{border-radius:8px}.w3-round-xlarge{border-radius:16px}.w3-round-xxlarge{border-radius:32px}
|
||||||
.w3-row-padding,.w3-row-padding>.w3-half,.w3-row-padding>.w3-third,.w3-row-padding>.w3-twothird,.w3-row-padding>.w3-threequarter,.w3-row-padding>.w3-quarter,.w3-row-padding>.w3-col{padding:0 8px}
|
.w3-row-padding,.w3-row-padding>.w3-half,.w3-row-padding>.w3-third,.w3-row-padding>.w3-twothird,.w3-row-padding>.w3-threequarter,.w3-row-padding>.w3-quarter,.w3-row-padding>.w3-col{padding:0 8px}
|
||||||
.w3-container,.w3-panel{padding:0.01em 16px}.w3-panel{margin-top:16px;margin-bottom:16px}
|
.w3-container,.w3-panel{padding:0.01em 16px}.w3-panel{margin-top:16px;margin-bottom:16px}
|
||||||
|
.w3-grid{display:grid}.w3-grid-padding{display:grid;gap:16px}.w3-flex{display:flex}
|
||||||
|
.w3-text-center{text-align:center}.w3-text-bold,.w3-bold{font-weight:bold}.w3-text-italic,.w3-italic{font-style:italic}
|
||||||
.w3-code,.w3-codespan{font-family:Consolas,"courier new";font-size:16px}
|
.w3-code,.w3-codespan{font-family:Consolas,"courier new";font-size:16px}
|
||||||
.w3-code{width:auto;background-color:#fff;padding:8px 12px;border-left:4px solid #4CAF50;word-wrap:break-word}
|
.w3-code{width:auto;background-color:#fff;padding:8px 12px;border-left:4px solid #4CAF50;word-wrap:break-word}
|
||||||
.w3-codespan{color:crimson;background-color:#f1f1f1;padding-left:4px;padding-right:4px;font-size:110%}
|
.w3-codespan{color:crimson;background-color:#f1f1f1;padding-left:4px;padding-right:4px;font-size:110%}
|
||||||
@@ -148,6 +150,7 @@ hr{border:0;border-top:1px solid #eee;margin:20px 0}
|
|||||||
.w3-button:hover{color:#000!important;background-color:#ccc!important}
|
.w3-button:hover{color:#000!important;background-color:#ccc!important}
|
||||||
.w3-transparent,.w3-hover-none:hover{background-color:transparent!important}
|
.w3-transparent,.w3-hover-none:hover{background-color:transparent!important}
|
||||||
.w3-hover-none:hover{box-shadow:none!important}
|
.w3-hover-none:hover{box-shadow:none!important}
|
||||||
|
.w3-rtl{direction:rtl}.w3-ltr{direction:ltr}
|
||||||
/* Colors */
|
/* Colors */
|
||||||
.w3-amber,.w3-hover-amber:hover{color:#000!important;background-color:#ffc107!important}
|
.w3-amber,.w3-hover-amber:hover{color:#000!important;background-color:#ffc107!important}
|
||||||
.w3-aqua,.w3-hover-aqua:hover{color:#000!important;background-color:#00ffff!important}
|
.w3-aqua,.w3-hover-aqua:hover{color:#000!important;background-color:#00ffff!important}
|
||||||
@@ -175,6 +178,19 @@ hr{border:0;border-top:1px solid #eee;margin:20px 0}
|
|||||||
.w3-grey,.w3-hover-grey:hover,.w3-gray,.w3-hover-gray:hover{color:#000!important;background-color:#9e9e9e!important}
|
.w3-grey,.w3-hover-grey:hover,.w3-gray,.w3-hover-gray:hover{color:#000!important;background-color:#9e9e9e!important}
|
||||||
.w3-light-grey,.w3-hover-light-grey:hover,.w3-light-gray,.w3-hover-light-gray:hover{color:#000!important;background-color:#f1f1f1!important}
|
.w3-light-grey,.w3-hover-light-grey:hover,.w3-light-gray,.w3-hover-light-gray:hover{color:#000!important;background-color:#f1f1f1!important}
|
||||||
.w3-dark-grey,.w3-hover-dark-grey:hover,.w3-dark-gray,.w3-hover-dark-gray:hover{color:#fff!important;background-color:#616161!important}
|
.w3-dark-grey,.w3-hover-dark-grey:hover,.w3-dark-gray,.w3-hover-dark-gray:hover{color:#fff!important;background-color:#616161!important}
|
||||||
|
.w3-asphalt,.w3-hover-asphalt:hover{color:#fff!important;background-color:#343a40!important}
|
||||||
|
.w3-crimson,.w3-hover-crimson:hover{color:#fff!important;background-color:#a20025!important}
|
||||||
|
.w3-cobalt,w3-hover-cobalt:hover{color:#fff!important;background-color:#0050ef!important}
|
||||||
|
.w3-emerald,.w3-hover-emerald:hover{color:#fff!important;background-color:#008a00!important}
|
||||||
|
.w3-olive,.w3-hover-olive:hover{color:#fff!important;background-color:#6d8764!important}
|
||||||
|
.w3-paper,.w3-hover-paper:hover{color:#000!important;background-color:#f8f9fa!important}
|
||||||
|
.w3-sienna,.w3-hover-sienna:hover{color:#fff!important;background-color:#a0522d!important}
|
||||||
|
.w3-taupe,.w3-hover-taupe:hover{color:#fff!important;background-color:#87794e!important}
|
||||||
|
.w3-danger{color:#fff!important;background-color:#dd0000!important}
|
||||||
|
.w3-note{color:#000!important;background-color:#fff599!important}
|
||||||
|
.w3-info{color:#fff!important;background-color:#0a6fc2!important}
|
||||||
|
.w3-warning{color:#000!important;background-color:#ffb305!important}
|
||||||
|
.w3-success{color:#fff!important;background-color:#008a00!important}
|
||||||
.w3-pale-red,.w3-hover-pale-red:hover{color:#000!important;background-color:#ffdddd!important}
|
.w3-pale-red,.w3-hover-pale-red:hover{color:#000!important;background-color:#ffdddd!important}
|
||||||
.w3-pale-green,.w3-hover-pale-green:hover{color:#000!important;background-color:#ddffdd!important}
|
.w3-pale-green,.w3-hover-pale-green:hover{color:#000!important;background-color:#ddffdd!important}
|
||||||
.w3-pale-yellow,.w3-hover-pale-yellow:hover{color:#000!important;background-color:#ffffcc!important}
|
.w3-pale-yellow,.w3-hover-pale-yellow:hover{color:#000!important;background-color:#ffffcc!important}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"type": "tildefriends-app",
|
"type": "tildefriends-app",
|
||||||
"emoji": "📜",
|
"emoji": "📜",
|
||||||
"previous": "&miGORZ8BwjHg2YO0t4bms6SI28XWPYqnqOZ8u9zsbZc=.sha256"
|
"previous": "&sJqeyYjHys6Z8IqqtZ2ij2ZC1E2xieu/FU/u2hE+O1U=.sha256"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ function* treeify(prefix, o) {
|
|||||||
|
|
||||||
function markdown(md) {
|
function markdown(md) {
|
||||||
let parsed = new commonmark.Parser().parse(md ?? '*undocumented*');
|
let parsed = new commonmark.Parser().parse(md ?? '*undocumented*');
|
||||||
return new commonmark.HtmlRenderer().render(parsed);
|
return new commonmark.HtmlRenderer({safe: true}).render(parsed);
|
||||||
}
|
}
|
||||||
|
|
||||||
function document(api) {
|
function document(api) {
|
||||||
@@ -55,6 +55,9 @@ app.setDocument(`<head>
|
|||||||
</head>
|
</head>
|
||||||
<body style="color:#fff">
|
<body style="color:#fff">
|
||||||
${markdown(docs.docs.global)}
|
${markdown(docs.docs.global)}
|
||||||
|
<!--
|
||||||
|
${Object.keys(docs.docs).filter(x => [...treeify('', globalThis)].indexOf(x) == -1).map(x => `<p>STALE: ${x}</p>`).join('')}
|
||||||
|
-->
|
||||||
${[...treeify('', globalThis)].map(x => document(x)).join('\n')}
|
${[...treeify('', globalThis)].map(x => document(x)).join('\n')}
|
||||||
<a id="Database"></a>
|
<a id="Database"></a>
|
||||||
${markdown(docs.docs.database)}
|
${markdown(docs.docs.database)}
|
||||||
|
|||||||
@@ -195,51 +195,6 @@ Call a function after some delay.
|
|||||||
* *Number* **timeout** Number of milliseconds to wait before calling the callback function.
|
* *Number* **timeout** Number of milliseconds to wait before calling the callback function.
|
||||||
`;
|
`;
|
||||||
|
|
||||||
docs['parseHttpRequest()'] = `
|
|
||||||
Parses an HTTP request.
|
|
||||||
### Parameters
|
|
||||||
* *Uint8Array* **request** The request data. Maybe be partial or contain extra data. The return value will
|
|
||||||
indicate when and where it is complete.
|
|
||||||
* *Number* **last_length** The length of the data passed on a previous attempt for the same request, or 0 initially.
|
|
||||||
### Returns
|
|
||||||
* *Integer* **-2** if the request is incomplete.
|
|
||||||
* *Integer* **-1** if the request could not be parsed.
|
|
||||||
* *Object* An object with **bytes_parsed**, **minor_version**, **path**, and **headers** fields on successful parse.
|
|
||||||
`;
|
|
||||||
|
|
||||||
docs['parseHttpResponse()'] = `
|
|
||||||
Parses an HTTP response.
|
|
||||||
### Parameters
|
|
||||||
* *Uint8Array* **response** The response data. Maybe be partial or contain extra data. The return value will
|
|
||||||
indicate when and where it is complete.
|
|
||||||
* *Number* **last_length** The length of the data passed on a previous attempt for the same response, or 0 initially.
|
|
||||||
### Returns
|
|
||||||
* *Integer* **-2** if the response is incomplete.
|
|
||||||
* *Integer* **-1** if the response could not be parsed.
|
|
||||||
* *Object* An object with **bytes_parsed**, **minor_version**, **status**, **message**, and **headers** fields on successful parse.
|
|
||||||
`;
|
|
||||||
|
|
||||||
docs['sha1Digest()'] = `
|
|
||||||
Calculates a SHA1 digest.
|
|
||||||
|
|
||||||
Completes synchronously.
|
|
||||||
### Parameters
|
|
||||||
* *String* **value** The value for which to calculate the digest.
|
|
||||||
### Returns
|
|
||||||
*String* The SHA1 digest of UTF-8 encoded \`value\`.
|
|
||||||
`;
|
|
||||||
|
|
||||||
docs['maskBytes()'] = `
|
|
||||||
Masks bytes for WebSocket communication.
|
|
||||||
|
|
||||||
Completes synchronously.
|
|
||||||
### Parameters
|
|
||||||
* *Uint8Array* **bytes** The byte array of data to mask.
|
|
||||||
* *Uint32* **mask** The mask to apply.
|
|
||||||
### Returns
|
|
||||||
*Uint32Array* The masked bytes.
|
|
||||||
`;
|
|
||||||
|
|
||||||
docs['exit()'] = `
|
docs['exit()'] = `
|
||||||
Exits the app. But why would you want to do that?
|
Exits the app. But why would you want to do that?
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"type": "tildefriends-app",
|
"type": "tildefriends-app",
|
||||||
"emoji": "💻",
|
"emoji": "💻",
|
||||||
"previous": "&icsPplXHgmpkbNWyo/WTvRdT/A8BXxW4lJixOtP4ueQ=.sha256"
|
"previous": "&sFRTDn/RpxP1NJeECXHrXKwCRUJsEOEDVaCMPl50zpM=.sha256"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,10 +19,6 @@ async function fetch_info(apps) {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
async function fetch_shared_apps() {
|
async function fetch_shared_apps() {
|
||||||
let messages = {};
|
let messages = {};
|
||||||
|
|
||||||
@@ -69,17 +65,17 @@ async function main() {
|
|||||||
const stylesheet = `
|
const stylesheet = `
|
||||||
body {
|
body {
|
||||||
color: whitesmoke;
|
color: whitesmoke;
|
||||||
font-family: sans-serif;
|
margin: 8px;
|
||||||
margin: 16px;
|
|
||||||
}
|
}
|
||||||
.container {
|
|
||||||
|
.iconbox {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(auto-fill, 64px);
|
grid-template-columns: repeat(auto-fill, minmax(96px, 1fr));
|
||||||
gap: 1em;
|
}
|
||||||
justify-content: space-around;
|
|
||||||
background-color: #ffffff10;
|
.iconbox::after {
|
||||||
border: 2px solid #073642;
|
content: "";
|
||||||
border-radius: 8px;
|
flex: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.app {
|
.app {
|
||||||
@@ -101,16 +97,28 @@ async function main() {
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
const body = `
|
const body = `
|
||||||
<h1 style="text-shadow: #808080 0 0 10px;">Welcome to Tilde Friends.</h1>
|
<h1>Welcome to Tilde Friends</h1>
|
||||||
|
|
||||||
<h2>your apps</h2>
|
<div class="w3-card-4 w3-margin-top">
|
||||||
<div id="apps" class="container"></div>
|
<header class="w3-container w3-light-blue">
|
||||||
|
<h2>Your Apps</h2>
|
||||||
|
</header>
|
||||||
|
<div id="apps" class="w3-indigo iconbox"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<h2>shared apps</h2>
|
<div class="w3-card-4 w3-margin-top">
|
||||||
<div id="shared_apps" class="container"></div>
|
<header class="w3-container w3-light-blue">
|
||||||
|
<h2>Shared Apps</h2>
|
||||||
|
</header>
|
||||||
|
<div id="shared_apps" class="w3-indigo iconbox"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<h2>core apps</h2>
|
<div class="w3-card-4 w3-margin-top">
|
||||||
<div id="core_apps" class="container"></div>
|
<header class="w3-container w3-light-blue">
|
||||||
|
<h2>Core Apps</h2>
|
||||||
|
</header>
|
||||||
|
<div id="core_apps" class="w3-indigo iconbox"></div>
|
||||||
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const script = `
|
const script = `
|
||||||
@@ -126,9 +134,13 @@ async function main() {
|
|||||||
|
|
||||||
// For each app in the provided list
|
// For each app in the provided list
|
||||||
for (let app of Object.keys(apps).sort()) {
|
for (let app of Object.keys(apps).sort()) {
|
||||||
|
|
||||||
// Create the item
|
// Create the item
|
||||||
let div = list.appendChild(document.createElement('div'));
|
let inline = document.createElement('div');
|
||||||
|
inline.style.display = 'inline-block';
|
||||||
|
inline.classList.add('w3-button');
|
||||||
|
list.appendChild(inline);
|
||||||
|
let div = document.createElement('div');
|
||||||
|
inline.appendChild(div);
|
||||||
div.classList.add('app');
|
div.classList.add('app');
|
||||||
|
|
||||||
// The app's icon
|
// The app's icon
|
||||||
@@ -161,12 +173,13 @@ async function main() {
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
|
<link type="text/css" rel="stylesheet" href="w3.css"></link>
|
||||||
<style>
|
<style>
|
||||||
${stylesheet}
|
${stylesheet}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body class="w3-darkgray">
|
||||||
${body}
|
${body}
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
|
|||||||
251
apps/apps/w3.css
Normal file
251
apps/apps/w3.css
Normal file
@@ -0,0 +1,251 @@
|
|||||||
|
/* W3.CSS 5.02 March 31 2025 by Jan Egil and Borge Refsnes */
|
||||||
|
html{box-sizing:border-box}*,*:before,*:after{box-sizing:inherit}
|
||||||
|
/* Extract from normalize.css by Nicolas Gallagher and Jonathan Neal git.io/normalize */
|
||||||
|
html{-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%}body{margin:0}
|
||||||
|
article,aside,details,figcaption,figure,footer,header,main,menu,nav,section{display:block}summary{display:list-item}
|
||||||
|
audio,canvas,progress,video{display:inline-block}progress{vertical-align:baseline}
|
||||||
|
audio:not([controls]){display:none;height:0}[hidden],template{display:none}
|
||||||
|
a{background-color:transparent}a:active,a:hover{outline-width:0}
|
||||||
|
abbr[title]{border-bottom:none;text-decoration:underline;text-decoration:underline dotted}
|
||||||
|
b,strong{font-weight:bolder}dfn{font-style:italic}mark{background:#ff0;color:#000}
|
||||||
|
small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}
|
||||||
|
sub{bottom:-0.25em}sup{top:-0.5em}figure{margin:1em 40px}img{border-style:none}
|
||||||
|
code,kbd,pre,samp{font-family:monospace,monospace;font-size:1em}hr{box-sizing:content-box;height:0;overflow:visible}
|
||||||
|
button,input,select,textarea,optgroup{font:inherit;margin:0}optgroup{font-weight:bold}
|
||||||
|
button,input{overflow:visible}button,select{text-transform:none}
|
||||||
|
button,[type=button],[type=reset],[type=submit]{-webkit-appearance:button}
|
||||||
|
button::-moz-focus-inner,[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner{border-style:none;padding:0}
|
||||||
|
button:-moz-focusring,[type=button]:-moz-focusring,[type=reset]:-moz-focusring,[type=submit]:-moz-focusring{outline:1px dotted ButtonText}
|
||||||
|
fieldset{border:1px solid #c0c0c0;margin:0 2px;padding:.35em .625em .75em}
|
||||||
|
legend{color:inherit;display:table;max-width:100%;padding:0;white-space:normal}textarea{overflow:auto}
|
||||||
|
[type=checkbox],[type=radio]{padding:0}
|
||||||
|
[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}
|
||||||
|
[type=search]{-webkit-appearance:textfield;outline-offset:-2px}
|
||||||
|
[type=search]::-webkit-search-decoration{-webkit-appearance:none}
|
||||||
|
::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}
|
||||||
|
/* End extract */
|
||||||
|
html,body{font-family:Verdana,sans-serif;font-size:15px;line-height:1.5}html{overflow-x:hidden}
|
||||||
|
h1{font-size:36px}h2{font-size:30px}h3{font-size:24px}h4{font-size:20px}h5{font-size:18px}h6{font-size:16px}
|
||||||
|
.w3-serif{font-family:serif}.w3-sans-serif{font-family:sans-serif}.w3-cursive{font-family:cursive}.w3-monospace{font-family:monospace}
|
||||||
|
h1,h2,h3,h4,h5,h6{font-family:"Segoe UI",Arial,sans-serif;font-weight:400;margin:10px 0}.w3-wide{letter-spacing:4px}
|
||||||
|
hr{border:0;border-top:1px solid #eee;margin:20px 0}
|
||||||
|
.w3-image{max-width:100%;height:auto}img{vertical-align:middle}a{color:inherit}
|
||||||
|
.w3-table,.w3-table-all{border-collapse:collapse;border-spacing:0;width:100%;display:table}.w3-table-all{border:1px solid #ccc}
|
||||||
|
.w3-bordered tr,.w3-table-all tr{border-bottom:1px solid #ddd}.w3-striped tbody tr:nth-child(even){background-color:#f1f1f1}
|
||||||
|
.w3-table-all tr:nth-child(odd){background-color:#fff}.w3-table-all tr:nth-child(even){background-color:#f1f1f1}
|
||||||
|
.w3-hoverable tbody tr:hover,.w3-ul.w3-hoverable li:hover{background-color:#ccc}.w3-centered tr th,.w3-centered tr td{text-align:center}
|
||||||
|
.w3-table td,.w3-table th,.w3-table-all td,.w3-table-all th{padding:8px 8px;display:table-cell;text-align:left;vertical-align:top}
|
||||||
|
.w3-table th:first-child,.w3-table td:first-child,.w3-table-all th:first-child,.w3-table-all td:first-child{padding-left:16px}
|
||||||
|
.w3-btn,.w3-button{border:none;display:inline-block;padding:8px 16px;vertical-align:middle;overflow:hidden;text-decoration:none;color:inherit;background-color:inherit;text-align:center;cursor:pointer;white-space:nowrap}
|
||||||
|
.w3-btn:hover{box-shadow:0 8px 16px 0 rgba(0,0,0,0.2),0 6px 20px 0 rgba(0,0,0,0.19)}
|
||||||
|
.w3-btn,.w3-button{-webkit-touch-callout:none;-webkit-user-select:none;-khtml-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}
|
||||||
|
.w3-disabled,.w3-btn:disabled,.w3-button:disabled{cursor:not-allowed;opacity:0.3}.w3-disabled *,:disabled *{pointer-events:none}
|
||||||
|
.w3-btn.w3-disabled:hover,.w3-btn:disabled:hover{box-shadow:none}
|
||||||
|
.w3-badge,.w3-tag{background-color:#000;color:#fff;display:inline-block;padding-left:8px;padding-right:8px;text-align:center}.w3-badge{border-radius:50%}
|
||||||
|
.w3-ul{list-style-type:none;padding:0;margin:0}.w3-ul li{padding:8px 16px;border-bottom:1px solid #ddd}.w3-ul li:last-child{border-bottom:none}
|
||||||
|
.w3-tooltip,.w3-display-container{position:relative}.w3-tooltip .w3-text{display:none}.w3-tooltip:hover .w3-text{display:inline-block}
|
||||||
|
.w3-ripple:active{opacity:0.5}.w3-ripple{transition:opacity 0s}
|
||||||
|
.w3-input{padding:8px;display:block;border:none;border-bottom:1px solid #ccc;width:100%}
|
||||||
|
.w3-select{padding:9px 0;width:100%;border:none;border-bottom:1px solid #ccc}
|
||||||
|
.w3-dropdown-click,.w3-dropdown-hover{position:relative;display:inline-block;cursor:pointer}
|
||||||
|
.w3-dropdown-hover:hover .w3-dropdown-content{display:block}
|
||||||
|
.w3-dropdown-hover:first-child,.w3-dropdown-click:hover{background-color:#ccc;color:#000}
|
||||||
|
.w3-dropdown-hover:hover > .w3-button:first-child,.w3-dropdown-click:hover > .w3-button:first-child{background-color:#ccc;color:#000}
|
||||||
|
.w3-dropdown-content{cursor:auto;color:#000;background-color:#fff;display:none;position:absolute;min-width:160px;margin:0;padding:0;z-index:1}
|
||||||
|
.w3-check,.w3-radio{width:24px;height:24px;position:relative;top:6px}
|
||||||
|
.w3-sidebar{height:100%;width:200px;background-color:#fff;position:fixed!important;z-index:1;overflow:auto}
|
||||||
|
.w3-bar-block .w3-dropdown-hover,.w3-bar-block .w3-dropdown-click{width:100%}
|
||||||
|
.w3-bar-block .w3-dropdown-hover .w3-dropdown-content,.w3-bar-block .w3-dropdown-click .w3-dropdown-content{min-width:100%}
|
||||||
|
.w3-bar-block .w3-dropdown-hover .w3-button,.w3-bar-block .w3-dropdown-click .w3-button{width:100%;text-align:left;padding:8px 16px}
|
||||||
|
.w3-main,#main{transition:margin-left .4s}
|
||||||
|
.w3-modal{z-index:3;display:none;padding-top:100px;position:fixed;left:0;top:0;width:100%;height:100%;overflow:auto;background-color:rgb(0,0,0);background-color:rgba(0,0,0,0.4)}
|
||||||
|
.w3-modal-content{margin:auto;background-color:#fff;position:relative;padding:0;outline:0;width:600px}
|
||||||
|
.w3-bar{width:100%;overflow:hidden}.w3-center .w3-bar{display:inline-block;width:auto}
|
||||||
|
.w3-bar .w3-bar-item{padding:8px 16px;float:left;width:auto;border:none;display:block;outline:0}
|
||||||
|
.w3-bar .w3-dropdown-hover,.w3-bar .w3-dropdown-click{position:static;float:left}
|
||||||
|
.w3-bar .w3-button{white-space:normal}
|
||||||
|
.w3-bar-block .w3-bar-item{width:100%;display:block;padding:8px 16px;text-align:left;border:none;white-space:normal;float:none;outline:0}
|
||||||
|
.w3-bar-block.w3-center .w3-bar-item{text-align:center}.w3-block{display:block;width:100%}
|
||||||
|
.w3-responsive{display:block;overflow-x:auto}
|
||||||
|
.w3-container:after,.w3-container:before,.w3-panel:after,.w3-panel:before,.w3-row:after,.w3-row:before,.w3-row-padding:after,.w3-row-padding:before,
|
||||||
|
.w3-cell-row:before,.w3-cell-row:after,.w3-clear:after,.w3-clear:before,.w3-bar:before,.w3-bar:after{content:"";display:table;clear:both}
|
||||||
|
.w3-col,.w3-half,.w3-third,.w3-twothird,.w3-threequarter,.w3-quarter{float:left;width:100%}
|
||||||
|
.w3-col.s1{width:8.33333%}.w3-col.s2{width:16.66666%}.w3-col.s3{width:24.99999%}.w3-col.s4{width:33.33333%}
|
||||||
|
.w3-col.s5{width:41.66666%}.w3-col.s6{width:49.99999%}.w3-col.s7{width:58.33333%}.w3-col.s8{width:66.66666%}
|
||||||
|
.w3-col.s9{width:74.99999%}.w3-col.s10{width:83.33333%}.w3-col.s11{width:91.66666%}.w3-col.s12{width:99.99999%}
|
||||||
|
@media (min-width:601px){.w3-col.m1{width:8.33333%}.w3-col.m2{width:16.66666%}.w3-col.m3,.w3-quarter{width:24.99999%}.w3-col.m4,.w3-third{width:33.33333%}
|
||||||
|
.w3-col.m5{width:41.66666%}.w3-col.m6,.w3-half{width:49.99999%}.w3-col.m7{width:58.33333%}.w3-col.m8,.w3-twothird{width:66.66666%}
|
||||||
|
.w3-col.m9,.w3-threequarter{width:74.99999%}.w3-col.m10{width:83.33333%}.w3-col.m11{width:91.66666%}.w3-col.m12{width:99.99999%}}
|
||||||
|
@media (min-width:993px){.w3-col.l1{width:8.33333%}.w3-col.l2{width:16.66666%}.w3-col.l3{width:24.99999%}.w3-col.l4{width:33.33333%}
|
||||||
|
.w3-col.l5{width:41.66666%}.w3-col.l6{width:49.99999%}.w3-col.l7{width:58.33333%}.w3-col.l8{width:66.66666%}
|
||||||
|
.w3-col.l9{width:74.99999%}.w3-col.l10{width:83.33333%}.w3-col.l11{width:91.66666%}.w3-col.l12{width:99.99999%}}
|
||||||
|
.w3-rest{overflow:hidden}.w3-stretch{margin-left:-16px;margin-right:-16px}
|
||||||
|
.w3-content,.w3-auto{margin-left:auto;margin-right:auto}.w3-content{max-width:980px}.w3-auto{max-width:1140px}
|
||||||
|
.w3-cell-row{display:table;width:100%}.w3-cell{display:table-cell}
|
||||||
|
.w3-cell-top{vertical-align:top}.w3-cell-middle{vertical-align:middle}.w3-cell-bottom{vertical-align:bottom}
|
||||||
|
.w3-hide{display:none!important}.w3-show-block,.w3-show{display:block!important}.w3-show-inline-block{display:inline-block!important}
|
||||||
|
@media (max-width:1205px){.w3-auto{max-width:95%}}
|
||||||
|
@media (max-width:600px){.w3-modal-content{margin:0 10px;width:auto!important}.w3-modal{padding-top:30px}
|
||||||
|
.w3-dropdown-hover.w3-mobile .w3-dropdown-content,.w3-dropdown-click.w3-mobile .w3-dropdown-content{position:relative}
|
||||||
|
.w3-hide-small{display:none!important}.w3-mobile{display:block;width:100%!important}.w3-bar-item.w3-mobile,.w3-dropdown-hover.w3-mobile,.w3-dropdown-click.w3-mobile{text-align:center}
|
||||||
|
.w3-dropdown-hover.w3-mobile,.w3-dropdown-hover.w3-mobile .w3-btn,.w3-dropdown-hover.w3-mobile .w3-button,.w3-dropdown-click.w3-mobile,.w3-dropdown-click.w3-mobile .w3-btn,.w3-dropdown-click.w3-mobile .w3-button{width:100%}}
|
||||||
|
@media (max-width:768px){.w3-modal-content{width:500px}.w3-modal{padding-top:50px}}
|
||||||
|
@media (min-width:993px){.w3-modal-content{width:900px}.w3-hide-large{display:none!important}.w3-sidebar.w3-collapse{display:block!important}}
|
||||||
|
@media (max-width:992px) and (min-width:601px){.w3-hide-medium{display:none!important}}
|
||||||
|
@media (max-width:992px){.w3-sidebar.w3-collapse{display:none}.w3-main{margin-left:0!important;margin-right:0!important}.w3-auto{max-width:100%}}
|
||||||
|
.w3-top,.w3-bottom{position:fixed;width:100%;z-index:1}.w3-top{top:0}.w3-bottom{bottom:0}
|
||||||
|
.w3-overlay{position:fixed;display:none;width:100%;height:100%;top:0;left:0;right:0;bottom:0;background-color:rgba(0,0,0,0.5);z-index:2}
|
||||||
|
.w3-display-topleft{position:absolute;left:0;top:0}.w3-display-topright{position:absolute;right:0;top:0}
|
||||||
|
.w3-display-bottomleft{position:absolute;left:0;bottom:0}.w3-display-bottomright{position:absolute;right:0;bottom:0}
|
||||||
|
.w3-display-middle{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);-ms-transform:translate(-50%,-50%)}
|
||||||
|
.w3-display-left{position:absolute;top:50%;left:0%;transform:translate(0%,-50%);-ms-transform:translate(-0%,-50%)}
|
||||||
|
.w3-display-right{position:absolute;top:50%;right:0%;transform:translate(0%,-50%);-ms-transform:translate(0%,-50%)}
|
||||||
|
.w3-display-topmiddle{position:absolute;left:50%;top:0;transform:translate(-50%,0%);-ms-transform:translate(-50%,0%)}
|
||||||
|
.w3-display-bottommiddle{position:absolute;left:50%;bottom:0;transform:translate(-50%,0%);-ms-transform:translate(-50%,0%)}
|
||||||
|
.w3-display-container:hover .w3-display-hover{display:block}.w3-display-container:hover span.w3-display-hover{display:inline-block}.w3-display-hover{display:none}
|
||||||
|
.w3-display-position{position:absolute}
|
||||||
|
.w3-circle{border-radius:50%}
|
||||||
|
.w3-round-small{border-radius:2px}.w3-round,.w3-round-medium{border-radius:4px}.w3-round-large{border-radius:8px}.w3-round-xlarge{border-radius:16px}.w3-round-xxlarge{border-radius:32px}
|
||||||
|
.w3-row-padding,.w3-row-padding>.w3-half,.w3-row-padding>.w3-third,.w3-row-padding>.w3-twothird,.w3-row-padding>.w3-threequarter,.w3-row-padding>.w3-quarter,.w3-row-padding>.w3-col{padding:0 8px}
|
||||||
|
.w3-container,.w3-panel{padding:0.01em 16px}.w3-panel{margin-top:16px;margin-bottom:16px}
|
||||||
|
.w3-grid{display:grid}.w3-grid-padding{display:grid;gap:16px}.w3-flex{display:flex}
|
||||||
|
.w3-text-center{text-align:center}.w3-text-bold,.w3-bold{font-weight:bold}.w3-text-italic,.w3-italic{font-style:italic}
|
||||||
|
.w3-code,.w3-codespan{font-family:Consolas,"courier new";font-size:16px}
|
||||||
|
.w3-code{width:auto;background-color:#fff;padding:8px 12px;border-left:4px solid #4CAF50;word-wrap:break-word}
|
||||||
|
.w3-codespan{color:crimson;background-color:#f1f1f1;padding-left:4px;padding-right:4px;font-size:110%}
|
||||||
|
.w3-card,.w3-card-2{box-shadow:0 2px 5px 0 rgba(0,0,0,0.16),0 2px 10px 0 rgba(0,0,0,0.12)}
|
||||||
|
.w3-card-4,.w3-hover-shadow:hover{box-shadow:0 4px 10px 0 rgba(0,0,0,0.2),0 4px 20px 0 rgba(0,0,0,0.19)}
|
||||||
|
.w3-spin{animation:w3-spin 2s infinite linear}@keyframes w3-spin{0%{transform:rotate(0deg)}100%{transform:rotate(359deg)}}
|
||||||
|
.w3-animate-fading{animation:fading 10s infinite}@keyframes fading{0%{opacity:0}50%{opacity:1}100%{opacity:0}}
|
||||||
|
.w3-animate-opacity{animation:opac 0.8s}@keyframes opac{from{opacity:0} to{opacity:1}}
|
||||||
|
.w3-animate-top{position:relative;animation:animatetop 0.4s}@keyframes animatetop{from{top:-300px;opacity:0} to{top:0;opacity:1}}
|
||||||
|
.w3-animate-left{position:relative;animation:animateleft 0.4s}@keyframes animateleft{from{left:-300px;opacity:0} to{left:0;opacity:1}}
|
||||||
|
.w3-animate-right{position:relative;animation:animateright 0.4s}@keyframes animateright{from{right:-300px;opacity:0} to{right:0;opacity:1}}
|
||||||
|
.w3-animate-bottom{position:relative;animation:animatebottom 0.4s}@keyframes animatebottom{from{bottom:-300px;opacity:0} to{bottom:0;opacity:1}}
|
||||||
|
.w3-animate-zoom {animation:animatezoom 0.6s}@keyframes animatezoom{from{transform:scale(0)} to{transform:scale(1)}}
|
||||||
|
.w3-animate-input{transition:width 0.4s ease-in-out}.w3-animate-input:focus{width:100%!important}
|
||||||
|
.w3-opacity,.w3-hover-opacity:hover{opacity:0.60}.w3-opacity-off,.w3-hover-opacity-off:hover{opacity:1}
|
||||||
|
.w3-opacity-max{opacity:0.25}.w3-opacity-min{opacity:0.75}
|
||||||
|
.w3-greyscale-max,.w3-grayscale-max,.w3-hover-greyscale:hover,.w3-hover-grayscale:hover{filter:grayscale(100%)}
|
||||||
|
.w3-greyscale,.w3-grayscale{filter:grayscale(75%)}.w3-greyscale-min,.w3-grayscale-min{filter:grayscale(50%)}
|
||||||
|
.w3-sepia{filter:sepia(75%)}.w3-sepia-max,.w3-hover-sepia:hover{filter:sepia(100%)}.w3-sepia-min{filter:sepia(50%)}
|
||||||
|
.w3-tiny{font-size:10px!important}.w3-small{font-size:12px!important}.w3-medium{font-size:15px!important}.w3-large{font-size:18px!important}
|
||||||
|
.w3-xlarge{font-size:24px!important}.w3-xxlarge{font-size:36px!important}.w3-xxxlarge{font-size:48px!important}.w3-jumbo{font-size:64px!important}
|
||||||
|
.w3-left-align{text-align:left!important}.w3-right-align{text-align:right!important}.w3-justify{text-align:justify!important}.w3-center{text-align:center!important}
|
||||||
|
.w3-border-0{border:0!important}.w3-border{border:1px solid #ccc!important}
|
||||||
|
.w3-border-top{border-top:1px solid #ccc!important}.w3-border-bottom{border-bottom:1px solid #ccc!important}
|
||||||
|
.w3-border-left{border-left:1px solid #ccc!important}.w3-border-right{border-right:1px solid #ccc!important}
|
||||||
|
.w3-topbar{border-top:6px solid #ccc!important}.w3-bottombar{border-bottom:6px solid #ccc!important}
|
||||||
|
.w3-leftbar{border-left:6px solid #ccc!important}.w3-rightbar{border-right:6px solid #ccc!important}
|
||||||
|
.w3-section,.w3-code{margin-top:16px!important;margin-bottom:16px!important}
|
||||||
|
.w3-margin{margin:16px!important}.w3-margin-top{margin-top:16px!important}.w3-margin-bottom{margin-bottom:16px!important}
|
||||||
|
.w3-margin-left{margin-left:16px!important}.w3-margin-right{margin-right:16px!important}
|
||||||
|
.w3-padding-small{padding:4px 8px!important}.w3-padding{padding:8px 16px!important}.w3-padding-large{padding:12px 24px!important}
|
||||||
|
.w3-padding-16{padding-top:16px!important;padding-bottom:16px!important}.w3-padding-24{padding-top:24px!important;padding-bottom:24px!important}
|
||||||
|
.w3-padding-32{padding-top:32px!important;padding-bottom:32px!important}.w3-padding-48{padding-top:48px!important;padding-bottom:48px!important}
|
||||||
|
.w3-padding-64{padding-top:64px!important;padding-bottom:64px!important}
|
||||||
|
.w3-padding-top-64{padding-top:64px!important}.w3-padding-top-48{padding-top:48px!important}
|
||||||
|
.w3-padding-top-32{padding-top:32px!important}.w3-padding-top-24{padding-top:24px!important}
|
||||||
|
.w3-left{float:left!important}.w3-right{float:right!important}
|
||||||
|
.w3-button:hover{color:#000!important;background-color:#ccc!important}
|
||||||
|
.w3-transparent,.w3-hover-none:hover{background-color:transparent!important}
|
||||||
|
.w3-hover-none:hover{box-shadow:none!important}
|
||||||
|
.w3-rtl{direction:rtl}.w3-ltr{direction:ltr}
|
||||||
|
/* Colors */
|
||||||
|
.w3-amber,.w3-hover-amber:hover{color:#000!important;background-color:#ffc107!important}
|
||||||
|
.w3-aqua,.w3-hover-aqua:hover{color:#000!important;background-color:#00ffff!important}
|
||||||
|
.w3-blue,.w3-hover-blue:hover{color:#fff!important;background-color:#2196F3!important}
|
||||||
|
.w3-light-blue,.w3-hover-light-blue:hover{color:#000!important;background-color:#87CEEB!important}
|
||||||
|
.w3-brown,.w3-hover-brown:hover{color:#fff!important;background-color:#795548!important}
|
||||||
|
.w3-cyan,.w3-hover-cyan:hover{color:#000!important;background-color:#00bcd4!important}
|
||||||
|
.w3-blue-grey,.w3-hover-blue-grey:hover,.w3-blue-gray,.w3-hover-blue-gray:hover{color:#fff!important;background-color:#607d8b!important}
|
||||||
|
.w3-green,.w3-hover-green:hover{color:#fff!important;background-color:#4CAF50!important}
|
||||||
|
.w3-light-green,.w3-hover-light-green:hover{color:#000!important;background-color:#8bc34a!important}
|
||||||
|
.w3-indigo,.w3-hover-indigo:hover{color:#fff!important;background-color:#3f51b5!important}
|
||||||
|
.w3-khaki,.w3-hover-khaki:hover{color:#000!important;background-color:#f0e68c!important}
|
||||||
|
.w3-lime,.w3-hover-lime:hover{color:#000!important;background-color:#cddc39!important}
|
||||||
|
.w3-orange,.w3-hover-orange:hover{color:#000!important;background-color:#ff9800!important}
|
||||||
|
.w3-deep-orange,.w3-hover-deep-orange:hover{color:#fff!important;background-color:#ff5722!important}
|
||||||
|
.w3-pink,.w3-hover-pink:hover{color:#fff!important;background-color:#e91e63!important}
|
||||||
|
.w3-purple,.w3-hover-purple:hover{color:#fff!important;background-color:#9c27b0!important}
|
||||||
|
.w3-deep-purple,.w3-hover-deep-purple:hover{color:#fff!important;background-color:#673ab7!important}
|
||||||
|
.w3-red,.w3-hover-red:hover{color:#fff!important;background-color:#f44336!important}
|
||||||
|
.w3-sand,.w3-hover-sand:hover{color:#000!important;background-color:#fdf5e6!important}
|
||||||
|
.w3-teal,.w3-hover-teal:hover{color:#fff!important;background-color:#009688!important}
|
||||||
|
.w3-yellow,.w3-hover-yellow:hover{color:#000!important;background-color:#ffeb3b!important}
|
||||||
|
.w3-white,.w3-hover-white:hover{color:#000!important;background-color:#fff!important}
|
||||||
|
.w3-black,.w3-hover-black:hover{color:#fff!important;background-color:#000!important}
|
||||||
|
.w3-grey,.w3-hover-grey:hover,.w3-gray,.w3-hover-gray:hover{color:#000!important;background-color:#9e9e9e!important}
|
||||||
|
.w3-light-grey,.w3-hover-light-grey:hover,.w3-light-gray,.w3-hover-light-gray:hover{color:#000!important;background-color:#f1f1f1!important}
|
||||||
|
.w3-dark-grey,.w3-hover-dark-grey:hover,.w3-dark-gray,.w3-hover-dark-gray:hover{color:#fff!important;background-color:#616161!important}
|
||||||
|
.w3-asphalt,.w3-hover-asphalt:hover{color:#fff!important;background-color:#343a40!important}
|
||||||
|
.w3-crimson,.w3-hover-crimson:hover{color:#fff!important;background-color:#a20025!important}
|
||||||
|
.w3-cobalt,w3-hover-cobalt:hover{color:#fff!important;background-color:#0050ef!important}
|
||||||
|
.w3-emerald,.w3-hover-emerald:hover{color:#fff!important;background-color:#008a00!important}
|
||||||
|
.w3-olive,.w3-hover-olive:hover{color:#fff!important;background-color:#6d8764!important}
|
||||||
|
.w3-paper,.w3-hover-paper:hover{color:#000!important;background-color:#f8f9fa!important}
|
||||||
|
.w3-sienna,.w3-hover-sienna:hover{color:#fff!important;background-color:#a0522d!important}
|
||||||
|
.w3-taupe,.w3-hover-taupe:hover{color:#fff!important;background-color:#87794e!important}
|
||||||
|
.w3-danger{color:#fff!important;background-color:#dd0000!important}
|
||||||
|
.w3-note{color:#000!important;background-color:#fff599!important}
|
||||||
|
.w3-info{color:#fff!important;background-color:#0a6fc2!important}
|
||||||
|
.w3-warning{color:#000!important;background-color:#ffb305!important}
|
||||||
|
.w3-success{color:#fff!important;background-color:#008a00!important}
|
||||||
|
.w3-pale-red,.w3-hover-pale-red:hover{color:#000!important;background-color:#ffdddd!important}
|
||||||
|
.w3-pale-green,.w3-hover-pale-green:hover{color:#000!important;background-color:#ddffdd!important}
|
||||||
|
.w3-pale-yellow,.w3-hover-pale-yellow:hover{color:#000!important;background-color:#ffffcc!important}
|
||||||
|
.w3-pale-blue,.w3-hover-pale-blue:hover{color:#000!important;background-color:#ddffff!important}
|
||||||
|
.w3-text-amber,.w3-hover-text-amber:hover{color:#ffc107!important}
|
||||||
|
.w3-text-aqua,.w3-hover-text-aqua:hover{color:#00ffff!important}
|
||||||
|
.w3-text-blue,.w3-hover-text-blue:hover{color:#2196F3!important}
|
||||||
|
.w3-text-light-blue,.w3-hover-text-light-blue:hover{color:#87CEEB!important}
|
||||||
|
.w3-text-brown,.w3-hover-text-brown:hover{color:#795548!important}
|
||||||
|
.w3-text-cyan,.w3-hover-text-cyan:hover{color:#00bcd4!important}
|
||||||
|
.w3-text-blue-grey,.w3-hover-text-blue-grey:hover,.w3-text-blue-gray,.w3-hover-text-blue-gray:hover{color:#607d8b!important}
|
||||||
|
.w3-text-green,.w3-hover-text-green:hover{color:#4CAF50!important}
|
||||||
|
.w3-text-light-green,.w3-hover-text-light-green:hover{color:#8bc34a!important}
|
||||||
|
.w3-text-indigo,.w3-hover-text-indigo:hover{color:#3f51b5!important}
|
||||||
|
.w3-text-khaki,.w3-hover-text-khaki:hover{color:#b4aa50!important}
|
||||||
|
.w3-text-lime,.w3-hover-text-lime:hover{color:#cddc39!important}
|
||||||
|
.w3-text-orange,.w3-hover-text-orange:hover{color:#ff9800!important}
|
||||||
|
.w3-text-deep-orange,.w3-hover-text-deep-orange:hover{color:#ff5722!important}
|
||||||
|
.w3-text-pink,.w3-hover-text-pink:hover{color:#e91e63!important}
|
||||||
|
.w3-text-purple,.w3-hover-text-purple:hover{color:#9c27b0!important}
|
||||||
|
.w3-text-deep-purple,.w3-hover-text-deep-purple:hover{color:#673ab7!important}
|
||||||
|
.w3-text-red,.w3-hover-text-red:hover{color:#f44336!important}
|
||||||
|
.w3-text-sand,.w3-hover-text-sand:hover{color:#fdf5e6!important}
|
||||||
|
.w3-text-teal,.w3-hover-text-teal:hover{color:#009688!important}
|
||||||
|
.w3-text-yellow,.w3-hover-text-yellow:hover{color:#d2be0e!important}
|
||||||
|
.w3-text-white,.w3-hover-text-white:hover{color:#fff!important}
|
||||||
|
.w3-text-black,.w3-hover-text-black:hover{color:#000!important}
|
||||||
|
.w3-text-grey,.w3-hover-text-grey:hover,.w3-text-gray,.w3-hover-text-gray:hover{color:#757575!important}
|
||||||
|
.w3-text-light-grey,.w3-hover-text-light-grey:hover,.w3-text-light-gray,.w3-hover-text-light-gray:hover{color:#f1f1f1!important}
|
||||||
|
.w3-text-dark-grey,.w3-hover-text-dark-grey:hover,.w3-text-dark-gray,.w3-hover-text-dark-gray:hover{color:#3a3a3a!important}
|
||||||
|
.w3-border-amber,.w3-hover-border-amber:hover{border-color:#ffc107!important}
|
||||||
|
.w3-border-aqua,.w3-hover-border-aqua:hover{border-color:#00ffff!important}
|
||||||
|
.w3-border-blue,.w3-hover-border-blue:hover{border-color:#2196F3!important}
|
||||||
|
.w3-border-light-blue,.w3-hover-border-light-blue:hover{border-color:#87CEEB!important}
|
||||||
|
.w3-border-brown,.w3-hover-border-brown:hover{border-color:#795548!important}
|
||||||
|
.w3-border-cyan,.w3-hover-border-cyan:hover{border-color:#00bcd4!important}
|
||||||
|
.w3-border-blue-grey,.w3-hover-border-blue-grey:hover,.w3-border-blue-gray,.w3-hover-border-blue-gray:hover{border-color:#607d8b!important}
|
||||||
|
.w3-border-green,.w3-hover-border-green:hover{border-color:#4CAF50!important}
|
||||||
|
.w3-border-light-green,.w3-hover-border-light-green:hover{border-color:#8bc34a!important}
|
||||||
|
.w3-border-indigo,.w3-hover-border-indigo:hover{border-color:#3f51b5!important}
|
||||||
|
.w3-border-khaki,.w3-hover-border-khaki:hover{border-color:#f0e68c!important}
|
||||||
|
.w3-border-lime,.w3-hover-border-lime:hover{border-color:#cddc39!important}
|
||||||
|
.w3-border-orange,.w3-hover-border-orange:hover{border-color:#ff9800!important}
|
||||||
|
.w3-border-deep-orange,.w3-hover-border-deep-orange:hover{border-color:#ff5722!important}
|
||||||
|
.w3-border-pink,.w3-hover-border-pink:hover{border-color:#e91e63!important}
|
||||||
|
.w3-border-purple,.w3-hover-border-purple:hover{border-color:#9c27b0!important}
|
||||||
|
.w3-border-deep-purple,.w3-hover-border-deep-purple:hover{border-color:#673ab7!important}
|
||||||
|
.w3-border-red,.w3-hover-border-red:hover{border-color:#f44336!important}
|
||||||
|
.w3-border-sand,.w3-hover-border-sand:hover{border-color:#fdf5e6!important}
|
||||||
|
.w3-border-teal,.w3-hover-border-teal:hover{border-color:#009688!important}
|
||||||
|
.w3-border-yellow,.w3-hover-border-yellow:hover{border-color:#ffeb3b!important}
|
||||||
|
.w3-border-white,.w3-hover-border-white:hover{border-color:#fff!important}
|
||||||
|
.w3-border-black,.w3-hover-border-black:hover{border-color:#000!important}
|
||||||
|
.w3-border-grey,.w3-hover-border-grey:hover,.w3-border-gray,.w3-hover-border-gray:hover{border-color:#9e9e9e!important}
|
||||||
|
.w3-border-light-grey,.w3-hover-border-light-grey:hover,.w3-border-light-gray,.w3-hover-border-light-gray:hover{border-color:#f1f1f1!important}
|
||||||
|
.w3-border-dark-grey,.w3-hover-border-dark-grey:hover,.w3-border-dark-gray,.w3-hover-border-dark-gray:hover{border-color:#616161!important}
|
||||||
|
.w3-border-pale-red,.w3-hover-border-pale-red:hover{border-color:#ffe7e7!important}.w3-border-pale-green,.w3-hover-border-pale-green:hover{border-color:#e7ffe7!important}
|
||||||
|
.w3-border-pale-yellow,.w3-hover-border-pale-yellow:hover{border-color:#ffffcc!important}.w3-border-pale-blue,.w3-hover-border-pale-blue:hover{border-color:#e7ffff!important}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"type": "tildefriends-app",
|
"type": "tildefriends-app",
|
||||||
"emoji": "🪵",
|
"emoji": "🪵",
|
||||||
"previous": "&TIrBnpN3iz3O9L9MCCteAcVJZjA83EKdcfu4SCM76VE=.sha256"
|
"previous": "&3jabNEk6W2uolzTvfXX6fcWF50N3501vtgZ6ZxFVJ1s=.sha256"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -52,8 +52,8 @@ export async function get_blog_message(id) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function markdown(md) {
|
export function markdown(md) {
|
||||||
let reader = new commonmark.Parser({safe: true});
|
let reader = new commonmark.Parser();
|
||||||
let writer = new commonmark.HtmlRenderer();
|
let writer = new commonmark.HtmlRenderer({safe: true});
|
||||||
let parsed = reader.parse(md || '');
|
let parsed = reader.parse(md || '');
|
||||||
let walker = parsed.walker();
|
let walker = parsed.walker();
|
||||||
let event, node;
|
let event, node;
|
||||||
|
|||||||
2
apps/blog/commonmark.min.js
vendored
2
apps/blog/commonmark.min.js
vendored
File diff suppressed because one or more lines are too long
42
apps/blog/lit-all.min.js
vendored
42
apps/blog/lit-all.min.js
vendored
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1,4 +1,5 @@
|
|||||||
{
|
{
|
||||||
"type": "tildefriends-app",
|
"type": "tildefriends-app",
|
||||||
"emoji": "💽"
|
"emoji": "💽",
|
||||||
|
"previous": "&uQzkIe/Aj8yNhLKe3hEq+5fEJsGwIUx8NVBjJKwoV2U=.sha256"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -51,6 +51,19 @@ async function key_list(db) {
|
|||||||
app.setDocument(doc);
|
app.setDocument(doc);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function load() {
|
||||||
|
if (core.user?.credentials?.session) {
|
||||||
|
database_list();
|
||||||
|
} else {
|
||||||
|
app.setDocument(`<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<body style="background: #888">
|
||||||
|
<h1>Must be signed in to examine databases.</h1>
|
||||||
|
</body>
|
||||||
|
</html>`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
core.register('message', async function (message) {
|
core.register('message', async function (message) {
|
||||||
if (message.event == 'hashChange') {
|
if (message.event == 'hashChange') {
|
||||||
let hash = message.hash.substring(1);
|
let hash = message.hash.substring(1);
|
||||||
@@ -62,9 +75,9 @@ core.register('message', async function (message) {
|
|||||||
} else if (hash.length) {
|
} else if (hash.length) {
|
||||||
key_list(await database(hash.split(':').slice(1).join(':')));
|
key_list(await database(hash.split(':').slice(1).join(':')));
|
||||||
} else {
|
} else {
|
||||||
database_list();
|
load();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
database_list();
|
load();
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
{
|
{
|
||||||
"type": "tildefriends-app",
|
"type": "tildefriends-app",
|
||||||
"emoji": "➡️"
|
"emoji": "➡️",
|
||||||
|
"previous": "&YDDSzbRD8NFAykYlZnk4r4hAK5qXjT5LmKE6rhS1s+A=.sha256"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ async function contacts_internal(id, last_row_id, following, max_row_id) {
|
|||||||
result.blocking = result.blocking || {};
|
result.blocking = result.blocking || {};
|
||||||
let contacts = await query(
|
let contacts = await query(
|
||||||
`
|
`
|
||||||
SELECT content FROM messages
|
SELECT json(content) AS content FROM messages
|
||||||
WHERE author = ? AND
|
WHERE author = ? AND
|
||||||
rowid > ? AND
|
rowid > ? AND
|
||||||
rowid <= ? AND
|
rowid <= ? AND
|
||||||
@@ -189,50 +189,6 @@ async function fetch_about(db, ids, users) {
|
|||||||
return Object.assign({}, users);
|
return Object.assign({}, users);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getAbout(db, id) {
|
|
||||||
if (g_about_cache[id]) {
|
|
||||||
return g_about_cache[id];
|
|
||||||
}
|
|
||||||
let o = await db.get(id + ':about');
|
|
||||||
const k_version = 4;
|
|
||||||
let f = o ? JSON.parse(o) : o;
|
|
||||||
if (!f || f.version != k_version) {
|
|
||||||
f = {about: {}, sequence: 0, version: k_version};
|
|
||||||
}
|
|
||||||
await ssb.sqlAsync(
|
|
||||||
'SELECT ' +
|
|
||||||
' sequence, ' +
|
|
||||||
' content ' +
|
|
||||||
'FROM messages ' +
|
|
||||||
'WHERE ' +
|
|
||||||
' author = ?1 AND ' +
|
|
||||||
' sequence > ?2 AND ' +
|
|
||||||
" json_extract(content, '$.type') = 'about' AND " +
|
|
||||||
" json_extract(content, '$.about') = ?1 " +
|
|
||||||
'UNION SELECT MAX(sequence) as sequence, NULL FROM messages WHERE author = ?1 ' +
|
|
||||||
'ORDER BY sequence',
|
|
||||||
[id, f.sequence],
|
|
||||||
function (row) {
|
|
||||||
f.sequence = row.sequence;
|
|
||||||
if (row.content) {
|
|
||||||
let about = {};
|
|
||||||
try {
|
|
||||||
about = JSON.parse(row.content);
|
|
||||||
} catch {}
|
|
||||||
delete about.about;
|
|
||||||
delete about.type;
|
|
||||||
f.about = Object.assign(f.about, about);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
let j = JSON.stringify(f);
|
|
||||||
if (o != j) {
|
|
||||||
await db.set(id + ':about', j);
|
|
||||||
}
|
|
||||||
g_about_cache[id] = f.about;
|
|
||||||
return f.about;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getSize(db, id) {
|
async function getSize(db, id) {
|
||||||
let size = 0;
|
let size = 0;
|
||||||
await ssb.sqlAsync(
|
await ssb.sqlAsync(
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"type": "tildefriends-app",
|
"type": "tildefriends-app",
|
||||||
"emoji": "🪪",
|
"emoji": "🪪",
|
||||||
"previous": "&de7q4A59auHP/34bXgeNH05JZoxsGr5TjwXPvehWH30=.sha256"
|
"previous": "&5kw/2PgcySwOYCmAkjHTR2xTkIx3i7UjQmtQ8MfgWw8=.sha256"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import * as tfrpc from '/tfrpc.js';
|
import * as tfrpc from '/tfrpc.js';
|
||||||
|
|
||||||
|
const is_admin = core.user?.credentials?.permissions?.administration;
|
||||||
|
|
||||||
tfrpc.register(async function get_private_key(id) {
|
tfrpc.register(async function get_private_key(id) {
|
||||||
return bip39Words(await ssb.getPrivateKey(id));
|
return bip39Words(await ssb.getPrivateKey(id));
|
||||||
});
|
});
|
||||||
@@ -15,9 +17,13 @@ tfrpc.register(async function delete_id(id) {
|
|||||||
tfrpc.register(async function reload() {
|
tfrpc.register(async function reload() {
|
||||||
await main();
|
await main();
|
||||||
});
|
});
|
||||||
|
tfrpc.register(async function make_server(id) {
|
||||||
|
return await ssb.swapWithServerIdentity(id);
|
||||||
|
});
|
||||||
|
|
||||||
async function main() {
|
async function main() {
|
||||||
let ids = await ssb.getIdentities();
|
let ids = await ssb.getIdentities();
|
||||||
|
let server_id = await ssb.getServerIdentity();
|
||||||
await app.setDocument(
|
await app.setDocument(
|
||||||
`
|
`
|
||||||
<head>
|
<head>
|
||||||
@@ -78,7 +84,7 @@ async function main() {
|
|||||||
alert('Successfully created: ' + id);
|
alert('Successfully created: ' + id);
|
||||||
await tfrpc.rpc.reload();
|
await tfrpc.rpc.reload();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
alert('Error creating identity: ' + e);
|
alert('Error creating identity: ' + e.message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
handler.hide_id = function hide_id(event, element) {
|
handler.hide_id = function hide_id(event, element) {
|
||||||
@@ -98,6 +104,16 @@ async function main() {
|
|||||||
alert('Error deleting ID: ' + e);
|
alert('Error deleting ID: ' + e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
handler.make_server = async function make_server(event) {
|
||||||
|
let id = event.srcElement.dataset.id;
|
||||||
|
try {
|
||||||
|
if (confirm('Are you sure you want to make "' + id + '" the server identity?\\n\\nFor it to take effect, you will need to both sign in again and restart Tilde Friends.')) {
|
||||||
|
await tfrpc.rpc.make_server(id);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
alert('Error making server ID: ' + e);
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
<header class="w3-theme w3-padding"><h1>SSB Identity Management</h1></header>
|
<header class="w3-theme w3-padding"><h1>SSB Identity Management</h1></header>
|
||||||
<div class="w3-card-4 w3-margin">
|
<div class="w3-card-4 w3-margin">
|
||||||
@@ -116,12 +132,15 @@ async function main() {
|
|||||||
<div class="w3-card-4 w3-margin">
|
<div class="w3-card-4 w3-margin">
|
||||||
<header class="w3-container w3-theme-l2"><h2>Identities</h2></header>
|
<header class="w3-container w3-theme-l2"><h2>Identities</h2></header>
|
||||||
<ul class="w3-ul">` +
|
<ul class="w3-ul">` +
|
||||||
ids
|
(ids ?? [])
|
||||||
.map(
|
.map(
|
||||||
(id) => `<li style="overflow: hidden; text-wrap: nowrap; text-overflow: ellipsis">
|
(
|
||||||
|
id
|
||||||
|
) => `<li style="overflow: hidden; text-wrap: nowrap; text-overflow: ellipsis">
|
||||||
<button onclick="handler.export_id(event)" data-id="${id}" class="w3-button w3-theme">Export Identity</button>
|
<button onclick="handler.export_id(event)" data-id="${id}" class="w3-button w3-theme">Export Identity</button>
|
||||||
<button onclick="handler.delete_id(event)" data-id="${id}" class="w3-button w3-theme">Delete Identity</button>
|
<button onclick="handler.delete_id(event)" data-id="${id}" class="w3-button w3-theme">Delete Identity</button>
|
||||||
${id}
|
${is_admin && id != server_id ? `<button onclick="handler.make_server(event)" data-id="${id}" class="w3-button w3-theme">Make Server Identity</button>` : ''}
|
||||||
|
${id}${id == server_id ? ' <div class="w3-tag w3-theme-l4 w3-round">🖥 local server</div>' : ''}
|
||||||
</li>`
|
</li>`
|
||||||
)
|
)
|
||||||
.join('\n') +
|
.join('\n') +
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
/* W3.CSS 4.15 December 2020 by Jan Egil and Borge Refsnes */
|
/* W3.CSS 5.02 March 31 2025 by Jan Egil and Borge Refsnes */
|
||||||
html{box-sizing:border-box}*,*:before,*:after{box-sizing:inherit}
|
html{box-sizing:border-box}*,*:before,*:after{box-sizing:inherit}
|
||||||
/* Extract from normalize.css by Nicolas Gallagher and Jonathan Neal git.io/normalize */
|
/* Extract from normalize.css by Nicolas Gallagher and Jonathan Neal git.io/normalize */
|
||||||
html{-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%}body{margin:0}
|
html{-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%}body{margin:0}
|
||||||
@@ -108,6 +108,8 @@ hr{border:0;border-top:1px solid #eee;margin:20px 0}
|
|||||||
.w3-round-small{border-radius:2px}.w3-round,.w3-round-medium{border-radius:4px}.w3-round-large{border-radius:8px}.w3-round-xlarge{border-radius:16px}.w3-round-xxlarge{border-radius:32px}
|
.w3-round-small{border-radius:2px}.w3-round,.w3-round-medium{border-radius:4px}.w3-round-large{border-radius:8px}.w3-round-xlarge{border-radius:16px}.w3-round-xxlarge{border-radius:32px}
|
||||||
.w3-row-padding,.w3-row-padding>.w3-half,.w3-row-padding>.w3-third,.w3-row-padding>.w3-twothird,.w3-row-padding>.w3-threequarter,.w3-row-padding>.w3-quarter,.w3-row-padding>.w3-col{padding:0 8px}
|
.w3-row-padding,.w3-row-padding>.w3-half,.w3-row-padding>.w3-third,.w3-row-padding>.w3-twothird,.w3-row-padding>.w3-threequarter,.w3-row-padding>.w3-quarter,.w3-row-padding>.w3-col{padding:0 8px}
|
||||||
.w3-container,.w3-panel{padding:0.01em 16px}.w3-panel{margin-top:16px;margin-bottom:16px}
|
.w3-container,.w3-panel{padding:0.01em 16px}.w3-panel{margin-top:16px;margin-bottom:16px}
|
||||||
|
.w3-grid{display:grid}.w3-grid-padding{display:grid;gap:16px}.w3-flex{display:flex}
|
||||||
|
.w3-text-center{text-align:center}.w3-text-bold,.w3-bold{font-weight:bold}.w3-text-italic,.w3-italic{font-style:italic}
|
||||||
.w3-code,.w3-codespan{font-family:Consolas,"courier new";font-size:16px}
|
.w3-code,.w3-codespan{font-family:Consolas,"courier new";font-size:16px}
|
||||||
.w3-code{width:auto;background-color:#fff;padding:8px 12px;border-left:4px solid #4CAF50;word-wrap:break-word}
|
.w3-code{width:auto;background-color:#fff;padding:8px 12px;border-left:4px solid #4CAF50;word-wrap:break-word}
|
||||||
.w3-codespan{color:crimson;background-color:#f1f1f1;padding-left:4px;padding-right:4px;font-size:110%}
|
.w3-codespan{color:crimson;background-color:#f1f1f1;padding-left:4px;padding-right:4px;font-size:110%}
|
||||||
@@ -148,6 +150,7 @@ hr{border:0;border-top:1px solid #eee;margin:20px 0}
|
|||||||
.w3-button:hover{color:#000!important;background-color:#ccc!important}
|
.w3-button:hover{color:#000!important;background-color:#ccc!important}
|
||||||
.w3-transparent,.w3-hover-none:hover{background-color:transparent!important}
|
.w3-transparent,.w3-hover-none:hover{background-color:transparent!important}
|
||||||
.w3-hover-none:hover{box-shadow:none!important}
|
.w3-hover-none:hover{box-shadow:none!important}
|
||||||
|
.w3-rtl{direction:rtl}.w3-ltr{direction:ltr}
|
||||||
/* Colors */
|
/* Colors */
|
||||||
.w3-amber,.w3-hover-amber:hover{color:#000!important;background-color:#ffc107!important}
|
.w3-amber,.w3-hover-amber:hover{color:#000!important;background-color:#ffc107!important}
|
||||||
.w3-aqua,.w3-hover-aqua:hover{color:#000!important;background-color:#00ffff!important}
|
.w3-aqua,.w3-hover-aqua:hover{color:#000!important;background-color:#00ffff!important}
|
||||||
@@ -175,6 +178,19 @@ hr{border:0;border-top:1px solid #eee;margin:20px 0}
|
|||||||
.w3-grey,.w3-hover-grey:hover,.w3-gray,.w3-hover-gray:hover{color:#000!important;background-color:#9e9e9e!important}
|
.w3-grey,.w3-hover-grey:hover,.w3-gray,.w3-hover-gray:hover{color:#000!important;background-color:#9e9e9e!important}
|
||||||
.w3-light-grey,.w3-hover-light-grey:hover,.w3-light-gray,.w3-hover-light-gray:hover{color:#000!important;background-color:#f1f1f1!important}
|
.w3-light-grey,.w3-hover-light-grey:hover,.w3-light-gray,.w3-hover-light-gray:hover{color:#000!important;background-color:#f1f1f1!important}
|
||||||
.w3-dark-grey,.w3-hover-dark-grey:hover,.w3-dark-gray,.w3-hover-dark-gray:hover{color:#fff!important;background-color:#616161!important}
|
.w3-dark-grey,.w3-hover-dark-grey:hover,.w3-dark-gray,.w3-hover-dark-gray:hover{color:#fff!important;background-color:#616161!important}
|
||||||
|
.w3-asphalt,.w3-hover-asphalt:hover{color:#fff!important;background-color:#343a40!important}
|
||||||
|
.w3-crimson,.w3-hover-crimson:hover{color:#fff!important;background-color:#a20025!important}
|
||||||
|
.w3-cobalt,w3-hover-cobalt:hover{color:#fff!important;background-color:#0050ef!important}
|
||||||
|
.w3-emerald,.w3-hover-emerald:hover{color:#fff!important;background-color:#008a00!important}
|
||||||
|
.w3-olive,.w3-hover-olive:hover{color:#fff!important;background-color:#6d8764!important}
|
||||||
|
.w3-paper,.w3-hover-paper:hover{color:#000!important;background-color:#f8f9fa!important}
|
||||||
|
.w3-sienna,.w3-hover-sienna:hover{color:#fff!important;background-color:#a0522d!important}
|
||||||
|
.w3-taupe,.w3-hover-taupe:hover{color:#fff!important;background-color:#87794e!important}
|
||||||
|
.w3-danger{color:#fff!important;background-color:#dd0000!important}
|
||||||
|
.w3-note{color:#000!important;background-color:#fff599!important}
|
||||||
|
.w3-info{color:#fff!important;background-color:#0a6fc2!important}
|
||||||
|
.w3-warning{color:#000!important;background-color:#ffb305!important}
|
||||||
|
.w3-success{color:#fff!important;background-color:#008a00!important}
|
||||||
.w3-pale-red,.w3-hover-pale-red:hover{color:#000!important;background-color:#ffdddd!important}
|
.w3-pale-red,.w3-hover-pale-red:hover{color:#000!important;background-color:#ffdddd!important}
|
||||||
.w3-pale-green,.w3-hover-pale-green:hover{color:#000!important;background-color:#ddffdd!important}
|
.w3-pale-green,.w3-hover-pale-green:hover{color:#000!important;background-color:#ddffdd!important}
|
||||||
.w3-pale-yellow,.w3-hover-pale-yellow:hover{color:#000!important;background-color:#ffffcc!important}
|
.w3-pale-yellow,.w3-hover-pale-yellow:hover{color:#000!important;background-color:#ffffcc!important}
|
||||||
|
|||||||
5
apps/intro.json
Normal file
5
apps/intro.json
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"type": "tildefriends-app",
|
||||||
|
"emoji": "💡",
|
||||||
|
"previous": "&eN6DNPpQUNhGvxneLuLPgsOXR6qyFZ7u+MAz0b4fa7k=.sha256"
|
||||||
|
}
|
||||||
16
apps/intro/app.js
Normal file
16
apps/intro/app.js
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import * as tfrpc from '/tfrpc.js';
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
await app.setDocument(utf8Decode(getFile('index.html')));
|
||||||
|
}
|
||||||
|
|
||||||
|
tfrpc.register(async function complete() {
|
||||||
|
if (
|
||||||
|
core.user?.credentials?.permissions?.administration &&
|
||||||
|
(await core.globalSettingsGet('index')) == '/~core/intro/'
|
||||||
|
) {
|
||||||
|
return await core.globalSettingsSet('index', '/~core/ssb/');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
main();
|
||||||
286
apps/intro/index.html
Normal file
286
apps/intro/index.html
Normal file
@@ -0,0 +1,286 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html style="height: 100%; margin: 0; padding: 0; box-sizing: border-box">
|
||||||
|
<head>
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<link rel="stylesheet" type="text/css" href="w3.css" />
|
||||||
|
<style>
|
||||||
|
.slide {
|
||||||
|
display: none;
|
||||||
|
margin-left: auto;
|
||||||
|
margin-right: auto;
|
||||||
|
}
|
||||||
|
.dot {
|
||||||
|
width: 1em;
|
||||||
|
height: 1em;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.w3-left,
|
||||||
|
.w3-right {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body
|
||||||
|
style="
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
max-width: 100%;
|
||||||
|
max-height: 100%;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
"
|
||||||
|
class="w3-flex w3-dark-gray w3-center"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style="
|
||||||
|
flex: 1 1 auto;
|
||||||
|
overflow: auto;
|
||||||
|
contain: content;
|
||||||
|
padding-top: 16px;
|
||||||
|
padding-bottom: 16px;
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<div class="slide">
|
||||||
|
<div
|
||||||
|
class="w3-content w3-xlarge w3-card-4 w3-blue w3-panel w3-padding-32 w3-round-xlarge"
|
||||||
|
style="margin: 32px"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<div>Welcome to</div>
|
||||||
|
<div>~😎 Tilde Friends.</div>
|
||||||
|
</div>
|
||||||
|
<footer>
|
||||||
|
<button class="w3-button w3-yellow proceed">Next</button>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="slide w3-card-4 w3-gray" style="width: 90%">
|
||||||
|
<header class="w3-container w3-blue w3-xlarge">
|
||||||
|
<h1>This brief tutorial will introduce:</h1>
|
||||||
|
</header>
|
||||||
|
<ul class="w3-large w3-left-align">
|
||||||
|
<li><b>Secure Scuttlebutt</b>, a decentralized social network.</li>
|
||||||
|
<li>
|
||||||
|
<b>Tilde Friends</b>, the application platform that you are using
|
||||||
|
right now.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<b>How to get started</b> if you want to get gossiping right away.
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<footer class="w3-center w3-xlarge w3-padding">
|
||||||
|
<button class="w3-button w3-yellow proceed">Onward</button>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
<div class="slide w3-gray" style="width: 90%">
|
||||||
|
<div class="w3-card-4 w3-xlarge">
|
||||||
|
<header class="w3-container w3-blue">
|
||||||
|
<h1>💻Secure Scuttlebutt in a Nutshell🦀</h1>
|
||||||
|
</header>
|
||||||
|
<div class="w3-container w3-large w3-left-align">
|
||||||
|
<p>
|
||||||
|
Secure Scuttlebutt is a social network whose technical operation
|
||||||
|
attempts to mirror human social interaction.
|
||||||
|
</p>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
You can create your own account and post to your own feed on
|
||||||
|
your own device. This is all <b>local</b> with no external
|
||||||
|
communication. This puts you fully in control of your own words
|
||||||
|
and actions.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
Before you can interact with others, you need to
|
||||||
|
<b>connect over the network</b>, either directly to your friends
|
||||||
|
(i.e., peer-to-peer between your phones on coffee shop Wi-Fi) or
|
||||||
|
to 🚪<i>rooms</i> and 🍻<i>pubs</i> (hint: search the web for
|
||||||
|
<i>#ssbroom</i>).
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
Who you choose to <b>follow</b> determines what you see, with
|
||||||
|
most people choosing to see messages from friends and friends of
|
||||||
|
those friends. If you encounter content you'd rather not see,
|
||||||
|
<b>block</b> the offending account to improve the experience for
|
||||||
|
you and your followers.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
Your feed is an <b>immutable</b> log of your activity. Post with
|
||||||
|
care, because like your words in real life, posts can't be taken
|
||||||
|
back.
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<footer class="w3-center w3-xlarge w3-padding">
|
||||||
|
<a
|
||||||
|
class="w3-button w3-light-gray"
|
||||||
|
href="https://scuttlebutt.nz/"
|
||||||
|
target="_blank"
|
||||||
|
>See scuttlebutt.nz</a
|
||||||
|
>
|
||||||
|
<button class="w3-button w3-yellow proceed">Got It</button>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="slide w3-gray" style="width: 90%">
|
||||||
|
<div class="w3-card-4 w3-xlarge">
|
||||||
|
<header class="w3-container w3-blue w3-center">
|
||||||
|
<h1>~😎 Let's Talk Tilde Friends ~😎</h1>
|
||||||
|
</header>
|
||||||
|
<div class="w3-container w3-large w3-left-align">
|
||||||
|
<p>
|
||||||
|
Tilde Friends is an application platform that is an application of
|
||||||
|
its own.
|
||||||
|
</p>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
This intro is a Tilde Friends app. You can click <b>edit</b> at
|
||||||
|
the top to look under the hood and make changes.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
It is already possible to make and share new applications using
|
||||||
|
only Tilde Friends and Secure Scuttlebutt without having to set
|
||||||
|
up development environments, configure web servers, register
|
||||||
|
domain names, or pay for hosting services.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
But it's also set up so that you can't just break an app that
|
||||||
|
everybody is using or do malicious things with personal content.
|
||||||
|
There are <b>protections</b> in place like an operating system.
|
||||||
|
The intent is also for it to be <b>safe</b> to run strange apps
|
||||||
|
without worrying about adverse effects.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
But this is all a big 🚧work in progress🚧 and
|
||||||
|
<b>experiment</b>. Let's see where it takes us.
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<footer class="w3-center w3-xlarge w3-padding">
|
||||||
|
<button class="w3-button w3-yellow proceed">Okay</button>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="slide w3-gray" style="width: 90%">
|
||||||
|
<div class="w3-card-4 w3-xlarge">
|
||||||
|
<header class="w3-container w3-blue w3-center">
|
||||||
|
<h1>🦀Let's Get this Tilde Friends Party Started🎉</h1>
|
||||||
|
</header>
|
||||||
|
<div class="w3-container w3-large w3-left-align">
|
||||||
|
<p>The button below will take you to the Secure Scuttlebutt app.</p>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
Remember:
|
||||||
|
<ol>
|
||||||
|
<li>You are in charge. This is all on your device.</li>
|
||||||
|
<li>
|
||||||
|
Make network connections to exchange messages with others.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
Follow more accounts to see more content, and block those
|
||||||
|
posting content you'd rather not see.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
Be respectful, and consider the consequences of what you
|
||||||
|
post.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
This is all under active development. Exercise patience, and
|
||||||
|
report issues encountered.
|
||||||
|
</li>
|
||||||
|
</ol>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
To see this tutorial again later, select <b>apps</b> ->
|
||||||
|
<b>Core Apps</b> -> <b>intro</b>.
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<footer class="w3-center w3-xlarge w3-padding">
|
||||||
|
<button class="w3-button w3-yellow" id="complete">Let's Go!</button>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="w3-text-white w3-xlarge w3-center w3-flex"
|
||||||
|
style="
|
||||||
|
width: 100%;
|
||||||
|
flex: 0 1;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<div class="w3-jumbo" id="left" style="flex: 1 0; cursor: pointer">
|
||||||
|
❮
|
||||||
|
</div>
|
||||||
|
<span class="w3-badge dot w3-border w3-hover-yellow"></span>
|
||||||
|
<span class="w3-badge dot w3-border w3-hover-yellow"></span>
|
||||||
|
<span class="w3-badge dot w3-border w3-hover-yellow"></span>
|
||||||
|
<span class="w3-badge dot w3-border w3-hover-yellow"></span>
|
||||||
|
<span class="w3-badge dot w3-border w3-hover-yellow"></span>
|
||||||
|
<div class="w3-jumbo" style="flex: 1 0; cursor: pointer" id="right">
|
||||||
|
❯
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<script type="module">
|
||||||
|
import * as tfrpc from '/static/tfrpc.js';
|
||||||
|
|
||||||
|
let index = 0;
|
||||||
|
function set(i) {
|
||||||
|
show(i - index);
|
||||||
|
}
|
||||||
|
function show(delta) {
|
||||||
|
let slides = [...document.getElementsByClassName('slide')];
|
||||||
|
let dots = [...document.getElementsByClassName('dot')];
|
||||||
|
index = (index + delta + slides.length) % slides.length;
|
||||||
|
for (let slide of slides) {
|
||||||
|
slide.style.display =
|
||||||
|
slides.indexOf(slide) == index ? 'block' : 'none';
|
||||||
|
}
|
||||||
|
for (let dot of dots) {
|
||||||
|
if (dots.indexOf(dot) == index) {
|
||||||
|
dot.classList.add('w3-white');
|
||||||
|
} else {
|
||||||
|
dot.classList.remove('w3-white');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
document.getElementById('left').style.visibility =
|
||||||
|
index == 0 ? 'hidden' : 'visible';
|
||||||
|
document.getElementById('right').style.visibility =
|
||||||
|
index == slides.length - 1 ? 'hidden' : 'visible';
|
||||||
|
}
|
||||||
|
|
||||||
|
let dots = [...document.getElementsByClassName('dot')];
|
||||||
|
for (let dot of dots) {
|
||||||
|
dot.onclick = () => set(dots.indexOf(dot));
|
||||||
|
}
|
||||||
|
for (let button of document.getElementsByClassName('proceed')) {
|
||||||
|
button.onclick = () => show(1);
|
||||||
|
}
|
||||||
|
document.getElementById('left').onclick = () => show(-1);
|
||||||
|
document.getElementById('right').onclick = () => show(1);
|
||||||
|
document.getElementById('complete').onclick = function () {
|
||||||
|
console.log('completing');
|
||||||
|
tfrpc.rpc.complete().finally(function () {
|
||||||
|
console.log('completed');
|
||||||
|
let a = document.createElement('a');
|
||||||
|
a.href = '/~core/ssb/';
|
||||||
|
a.target = '_top';
|
||||||
|
document.body.appendChild(a);
|
||||||
|
a.click();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
window.addEventListener('keyup', function (event) {
|
||||||
|
if (event.key == 'ArrowLeft') {
|
||||||
|
show(-1);
|
||||||
|
} else if (event.key == 'ArrowRight') {
|
||||||
|
show(1);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
show(0);
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
251
apps/intro/w3.css
Normal file
251
apps/intro/w3.css
Normal file
@@ -0,0 +1,251 @@
|
|||||||
|
/* W3.CSS 5.02 March 31 2025 by Jan Egil and Borge Refsnes */
|
||||||
|
html{box-sizing:border-box}*,*:before,*:after{box-sizing:inherit}
|
||||||
|
/* Extract from normalize.css by Nicolas Gallagher and Jonathan Neal git.io/normalize */
|
||||||
|
html{-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%}body{margin:0}
|
||||||
|
article,aside,details,figcaption,figure,footer,header,main,menu,nav,section{display:block}summary{display:list-item}
|
||||||
|
audio,canvas,progress,video{display:inline-block}progress{vertical-align:baseline}
|
||||||
|
audio:not([controls]){display:none;height:0}[hidden],template{display:none}
|
||||||
|
a{background-color:transparent}a:active,a:hover{outline-width:0}
|
||||||
|
abbr[title]{border-bottom:none;text-decoration:underline;text-decoration:underline dotted}
|
||||||
|
b,strong{font-weight:bolder}dfn{font-style:italic}mark{background:#ff0;color:#000}
|
||||||
|
small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}
|
||||||
|
sub{bottom:-0.25em}sup{top:-0.5em}figure{margin:1em 40px}img{border-style:none}
|
||||||
|
code,kbd,pre,samp{font-family:monospace,monospace;font-size:1em}hr{box-sizing:content-box;height:0;overflow:visible}
|
||||||
|
button,input,select,textarea,optgroup{font:inherit;margin:0}optgroup{font-weight:bold}
|
||||||
|
button,input{overflow:visible}button,select{text-transform:none}
|
||||||
|
button,[type=button],[type=reset],[type=submit]{-webkit-appearance:button}
|
||||||
|
button::-moz-focus-inner,[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner{border-style:none;padding:0}
|
||||||
|
button:-moz-focusring,[type=button]:-moz-focusring,[type=reset]:-moz-focusring,[type=submit]:-moz-focusring{outline:1px dotted ButtonText}
|
||||||
|
fieldset{border:1px solid #c0c0c0;margin:0 2px;padding:.35em .625em .75em}
|
||||||
|
legend{color:inherit;display:table;max-width:100%;padding:0;white-space:normal}textarea{overflow:auto}
|
||||||
|
[type=checkbox],[type=radio]{padding:0}
|
||||||
|
[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}
|
||||||
|
[type=search]{-webkit-appearance:textfield;outline-offset:-2px}
|
||||||
|
[type=search]::-webkit-search-decoration{-webkit-appearance:none}
|
||||||
|
::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}
|
||||||
|
/* End extract */
|
||||||
|
html,body{font-family:Verdana,sans-serif;font-size:15px;line-height:1.5}html{overflow-x:hidden}
|
||||||
|
h1{font-size:36px}h2{font-size:30px}h3{font-size:24px}h4{font-size:20px}h5{font-size:18px}h6{font-size:16px}
|
||||||
|
.w3-serif{font-family:serif}.w3-sans-serif{font-family:sans-serif}.w3-cursive{font-family:cursive}.w3-monospace{font-family:monospace}
|
||||||
|
h1,h2,h3,h4,h5,h6{font-family:"Segoe UI",Arial,sans-serif;font-weight:400;margin:10px 0}.w3-wide{letter-spacing:4px}
|
||||||
|
hr{border:0;border-top:1px solid #eee;margin:20px 0}
|
||||||
|
.w3-image{max-width:100%;height:auto}img{vertical-align:middle}a{color:inherit}
|
||||||
|
.w3-table,.w3-table-all{border-collapse:collapse;border-spacing:0;width:100%;display:table}.w3-table-all{border:1px solid #ccc}
|
||||||
|
.w3-bordered tr,.w3-table-all tr{border-bottom:1px solid #ddd}.w3-striped tbody tr:nth-child(even){background-color:#f1f1f1}
|
||||||
|
.w3-table-all tr:nth-child(odd){background-color:#fff}.w3-table-all tr:nth-child(even){background-color:#f1f1f1}
|
||||||
|
.w3-hoverable tbody tr:hover,.w3-ul.w3-hoverable li:hover{background-color:#ccc}.w3-centered tr th,.w3-centered tr td{text-align:center}
|
||||||
|
.w3-table td,.w3-table th,.w3-table-all td,.w3-table-all th{padding:8px 8px;display:table-cell;text-align:left;vertical-align:top}
|
||||||
|
.w3-table th:first-child,.w3-table td:first-child,.w3-table-all th:first-child,.w3-table-all td:first-child{padding-left:16px}
|
||||||
|
.w3-btn,.w3-button{border:none;display:inline-block;padding:8px 16px;vertical-align:middle;overflow:hidden;text-decoration:none;color:inherit;background-color:inherit;text-align:center;cursor:pointer;white-space:nowrap}
|
||||||
|
.w3-btn:hover{box-shadow:0 8px 16px 0 rgba(0,0,0,0.2),0 6px 20px 0 rgba(0,0,0,0.19)}
|
||||||
|
.w3-btn,.w3-button{-webkit-touch-callout:none;-webkit-user-select:none;-khtml-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}
|
||||||
|
.w3-disabled,.w3-btn:disabled,.w3-button:disabled{cursor:not-allowed;opacity:0.3}.w3-disabled *,:disabled *{pointer-events:none}
|
||||||
|
.w3-btn.w3-disabled:hover,.w3-btn:disabled:hover{box-shadow:none}
|
||||||
|
.w3-badge,.w3-tag{background-color:#000;color:#fff;display:inline-block;padding-left:8px;padding-right:8px;text-align:center}.w3-badge{border-radius:50%}
|
||||||
|
.w3-ul{list-style-type:none;padding:0;margin:0}.w3-ul li{padding:8px 16px;border-bottom:1px solid #ddd}.w3-ul li:last-child{border-bottom:none}
|
||||||
|
.w3-tooltip,.w3-display-container{position:relative}.w3-tooltip .w3-text{display:none}.w3-tooltip:hover .w3-text{display:inline-block}
|
||||||
|
.w3-ripple:active{opacity:0.5}.w3-ripple{transition:opacity 0s}
|
||||||
|
.w3-input{padding:8px;display:block;border:none;border-bottom:1px solid #ccc;width:100%}
|
||||||
|
.w3-select{padding:9px 0;width:100%;border:none;border-bottom:1px solid #ccc}
|
||||||
|
.w3-dropdown-click,.w3-dropdown-hover{position:relative;display:inline-block;cursor:pointer}
|
||||||
|
.w3-dropdown-hover:hover .w3-dropdown-content{display:block}
|
||||||
|
.w3-dropdown-hover:first-child,.w3-dropdown-click:hover{background-color:#ccc;color:#000}
|
||||||
|
.w3-dropdown-hover:hover > .w3-button:first-child,.w3-dropdown-click:hover > .w3-button:first-child{background-color:#ccc;color:#000}
|
||||||
|
.w3-dropdown-content{cursor:auto;color:#000;background-color:#fff;display:none;position:absolute;min-width:160px;margin:0;padding:0;z-index:1}
|
||||||
|
.w3-check,.w3-radio{width:24px;height:24px;position:relative;top:6px}
|
||||||
|
.w3-sidebar{height:100%;width:200px;background-color:#fff;position:fixed!important;z-index:1;overflow:auto}
|
||||||
|
.w3-bar-block .w3-dropdown-hover,.w3-bar-block .w3-dropdown-click{width:100%}
|
||||||
|
.w3-bar-block .w3-dropdown-hover .w3-dropdown-content,.w3-bar-block .w3-dropdown-click .w3-dropdown-content{min-width:100%}
|
||||||
|
.w3-bar-block .w3-dropdown-hover .w3-button,.w3-bar-block .w3-dropdown-click .w3-button{width:100%;text-align:left;padding:8px 16px}
|
||||||
|
.w3-main,#main{transition:margin-left .4s}
|
||||||
|
.w3-modal{z-index:3;display:none;padding-top:100px;position:fixed;left:0;top:0;width:100%;height:100%;overflow:auto;background-color:rgb(0,0,0);background-color:rgba(0,0,0,0.4)}
|
||||||
|
.w3-modal-content{margin:auto;background-color:#fff;position:relative;padding:0;outline:0;width:600px}
|
||||||
|
.w3-bar{width:100%;overflow:hidden}.w3-center .w3-bar{display:inline-block;width:auto}
|
||||||
|
.w3-bar .w3-bar-item{padding:8px 16px;float:left;width:auto;border:none;display:block;outline:0}
|
||||||
|
.w3-bar .w3-dropdown-hover,.w3-bar .w3-dropdown-click{position:static;float:left}
|
||||||
|
.w3-bar .w3-button{white-space:normal}
|
||||||
|
.w3-bar-block .w3-bar-item{width:100%;display:block;padding:8px 16px;text-align:left;border:none;white-space:normal;float:none;outline:0}
|
||||||
|
.w3-bar-block.w3-center .w3-bar-item{text-align:center}.w3-block{display:block;width:100%}
|
||||||
|
.w3-responsive{display:block;overflow-x:auto}
|
||||||
|
.w3-container:after,.w3-container:before,.w3-panel:after,.w3-panel:before,.w3-row:after,.w3-row:before,.w3-row-padding:after,.w3-row-padding:before,
|
||||||
|
.w3-cell-row:before,.w3-cell-row:after,.w3-clear:after,.w3-clear:before,.w3-bar:before,.w3-bar:after{content:"";display:table;clear:both}
|
||||||
|
.w3-col,.w3-half,.w3-third,.w3-twothird,.w3-threequarter,.w3-quarter{float:left;width:100%}
|
||||||
|
.w3-col.s1{width:8.33333%}.w3-col.s2{width:16.66666%}.w3-col.s3{width:24.99999%}.w3-col.s4{width:33.33333%}
|
||||||
|
.w3-col.s5{width:41.66666%}.w3-col.s6{width:49.99999%}.w3-col.s7{width:58.33333%}.w3-col.s8{width:66.66666%}
|
||||||
|
.w3-col.s9{width:74.99999%}.w3-col.s10{width:83.33333%}.w3-col.s11{width:91.66666%}.w3-col.s12{width:99.99999%}
|
||||||
|
@media (min-width:601px){.w3-col.m1{width:8.33333%}.w3-col.m2{width:16.66666%}.w3-col.m3,.w3-quarter{width:24.99999%}.w3-col.m4,.w3-third{width:33.33333%}
|
||||||
|
.w3-col.m5{width:41.66666%}.w3-col.m6,.w3-half{width:49.99999%}.w3-col.m7{width:58.33333%}.w3-col.m8,.w3-twothird{width:66.66666%}
|
||||||
|
.w3-col.m9,.w3-threequarter{width:74.99999%}.w3-col.m10{width:83.33333%}.w3-col.m11{width:91.66666%}.w3-col.m12{width:99.99999%}}
|
||||||
|
@media (min-width:993px){.w3-col.l1{width:8.33333%}.w3-col.l2{width:16.66666%}.w3-col.l3{width:24.99999%}.w3-col.l4{width:33.33333%}
|
||||||
|
.w3-col.l5{width:41.66666%}.w3-col.l6{width:49.99999%}.w3-col.l7{width:58.33333%}.w3-col.l8{width:66.66666%}
|
||||||
|
.w3-col.l9{width:74.99999%}.w3-col.l10{width:83.33333%}.w3-col.l11{width:91.66666%}.w3-col.l12{width:99.99999%}}
|
||||||
|
.w3-rest{overflow:hidden}.w3-stretch{margin-left:-16px;margin-right:-16px}
|
||||||
|
.w3-content,.w3-auto{margin-left:auto;margin-right:auto}.w3-content{max-width:980px}.w3-auto{max-width:1140px}
|
||||||
|
.w3-cell-row{display:table;width:100%}.w3-cell{display:table-cell}
|
||||||
|
.w3-cell-top{vertical-align:top}.w3-cell-middle{vertical-align:middle}.w3-cell-bottom{vertical-align:bottom}
|
||||||
|
.w3-hide{display:none!important}.w3-show-block,.w3-show{display:block!important}.w3-show-inline-block{display:inline-block!important}
|
||||||
|
@media (max-width:1205px){.w3-auto{max-width:95%}}
|
||||||
|
@media (max-width:600px){.w3-modal-content{margin:0 10px;width:auto!important}.w3-modal{padding-top:30px}
|
||||||
|
.w3-dropdown-hover.w3-mobile .w3-dropdown-content,.w3-dropdown-click.w3-mobile .w3-dropdown-content{position:relative}
|
||||||
|
.w3-hide-small{display:none!important}.w3-mobile{display:block;width:100%!important}.w3-bar-item.w3-mobile,.w3-dropdown-hover.w3-mobile,.w3-dropdown-click.w3-mobile{text-align:center}
|
||||||
|
.w3-dropdown-hover.w3-mobile,.w3-dropdown-hover.w3-mobile .w3-btn,.w3-dropdown-hover.w3-mobile .w3-button,.w3-dropdown-click.w3-mobile,.w3-dropdown-click.w3-mobile .w3-btn,.w3-dropdown-click.w3-mobile .w3-button{width:100%}}
|
||||||
|
@media (max-width:768px){.w3-modal-content{width:500px}.w3-modal{padding-top:50px}}
|
||||||
|
@media (min-width:993px){.w3-modal-content{width:900px}.w3-hide-large{display:none!important}.w3-sidebar.w3-collapse{display:block!important}}
|
||||||
|
@media (max-width:992px) and (min-width:601px){.w3-hide-medium{display:none!important}}
|
||||||
|
@media (max-width:992px){.w3-sidebar.w3-collapse{display:none}.w3-main{margin-left:0!important;margin-right:0!important}.w3-auto{max-width:100%}}
|
||||||
|
.w3-top,.w3-bottom{position:fixed;width:100%;z-index:1}.w3-top{top:0}.w3-bottom{bottom:0}
|
||||||
|
.w3-overlay{position:fixed;display:none;width:100%;height:100%;top:0;left:0;right:0;bottom:0;background-color:rgba(0,0,0,0.5);z-index:2}
|
||||||
|
.w3-display-topleft{position:absolute;left:0;top:0}.w3-display-topright{position:absolute;right:0;top:0}
|
||||||
|
.w3-display-bottomleft{position:absolute;left:0;bottom:0}.w3-display-bottomright{position:absolute;right:0;bottom:0}
|
||||||
|
.w3-display-middle{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);-ms-transform:translate(-50%,-50%)}
|
||||||
|
.w3-display-left{position:absolute;top:50%;left:0%;transform:translate(0%,-50%);-ms-transform:translate(-0%,-50%)}
|
||||||
|
.w3-display-right{position:absolute;top:50%;right:0%;transform:translate(0%,-50%);-ms-transform:translate(0%,-50%)}
|
||||||
|
.w3-display-topmiddle{position:absolute;left:50%;top:0;transform:translate(-50%,0%);-ms-transform:translate(-50%,0%)}
|
||||||
|
.w3-display-bottommiddle{position:absolute;left:50%;bottom:0;transform:translate(-50%,0%);-ms-transform:translate(-50%,0%)}
|
||||||
|
.w3-display-container:hover .w3-display-hover{display:block}.w3-display-container:hover span.w3-display-hover{display:inline-block}.w3-display-hover{display:none}
|
||||||
|
.w3-display-position{position:absolute}
|
||||||
|
.w3-circle{border-radius:50%}
|
||||||
|
.w3-round-small{border-radius:2px}.w3-round,.w3-round-medium{border-radius:4px}.w3-round-large{border-radius:8px}.w3-round-xlarge{border-radius:16px}.w3-round-xxlarge{border-radius:32px}
|
||||||
|
.w3-row-padding,.w3-row-padding>.w3-half,.w3-row-padding>.w3-third,.w3-row-padding>.w3-twothird,.w3-row-padding>.w3-threequarter,.w3-row-padding>.w3-quarter,.w3-row-padding>.w3-col{padding:0 8px}
|
||||||
|
.w3-container,.w3-panel{padding:0.01em 16px}.w3-panel{margin-top:16px;margin-bottom:16px}
|
||||||
|
.w3-grid{display:grid}.w3-grid-padding{display:grid;gap:16px}.w3-flex{display:flex}
|
||||||
|
.w3-text-center{text-align:center}.w3-text-bold,.w3-bold{font-weight:bold}.w3-text-italic,.w3-italic{font-style:italic}
|
||||||
|
.w3-code,.w3-codespan{font-family:Consolas,"courier new";font-size:16px}
|
||||||
|
.w3-code{width:auto;background-color:#fff;padding:8px 12px;border-left:4px solid #4CAF50;word-wrap:break-word}
|
||||||
|
.w3-codespan{color:crimson;background-color:#f1f1f1;padding-left:4px;padding-right:4px;font-size:110%}
|
||||||
|
.w3-card,.w3-card-2{box-shadow:0 2px 5px 0 rgba(0,0,0,0.16),0 2px 10px 0 rgba(0,0,0,0.12)}
|
||||||
|
.w3-card-4,.w3-hover-shadow:hover{box-shadow:0 4px 10px 0 rgba(0,0,0,0.2),0 4px 20px 0 rgba(0,0,0,0.19)}
|
||||||
|
.w3-spin{animation:w3-spin 2s infinite linear}@keyframes w3-spin{0%{transform:rotate(0deg)}100%{transform:rotate(359deg)}}
|
||||||
|
.w3-animate-fading{animation:fading 10s infinite}@keyframes fading{0%{opacity:0}50%{opacity:1}100%{opacity:0}}
|
||||||
|
.w3-animate-opacity{animation:opac 0.8s}@keyframes opac{from{opacity:0} to{opacity:1}}
|
||||||
|
.w3-animate-top{position:relative;animation:animatetop 0.4s}@keyframes animatetop{from{top:-300px;opacity:0} to{top:0;opacity:1}}
|
||||||
|
.w3-animate-left{position:relative;animation:animateleft 0.4s}@keyframes animateleft{from{left:-300px;opacity:0} to{left:0;opacity:1}}
|
||||||
|
.w3-animate-right{position:relative;animation:animateright 0.4s}@keyframes animateright{from{right:-300px;opacity:0} to{right:0;opacity:1}}
|
||||||
|
.w3-animate-bottom{position:relative;animation:animatebottom 0.4s}@keyframes animatebottom{from{bottom:-300px;opacity:0} to{bottom:0;opacity:1}}
|
||||||
|
.w3-animate-zoom {animation:animatezoom 0.6s}@keyframes animatezoom{from{transform:scale(0)} to{transform:scale(1)}}
|
||||||
|
.w3-animate-input{transition:width 0.4s ease-in-out}.w3-animate-input:focus{width:100%!important}
|
||||||
|
.w3-opacity,.w3-hover-opacity:hover{opacity:0.60}.w3-opacity-off,.w3-hover-opacity-off:hover{opacity:1}
|
||||||
|
.w3-opacity-max{opacity:0.25}.w3-opacity-min{opacity:0.75}
|
||||||
|
.w3-greyscale-max,.w3-grayscale-max,.w3-hover-greyscale:hover,.w3-hover-grayscale:hover{filter:grayscale(100%)}
|
||||||
|
.w3-greyscale,.w3-grayscale{filter:grayscale(75%)}.w3-greyscale-min,.w3-grayscale-min{filter:grayscale(50%)}
|
||||||
|
.w3-sepia{filter:sepia(75%)}.w3-sepia-max,.w3-hover-sepia:hover{filter:sepia(100%)}.w3-sepia-min{filter:sepia(50%)}
|
||||||
|
.w3-tiny{font-size:10px!important}.w3-small{font-size:12px!important}.w3-medium{font-size:15px!important}.w3-large{font-size:18px!important}
|
||||||
|
.w3-xlarge{font-size:24px!important}.w3-xxlarge{font-size:36px!important}.w3-xxxlarge{font-size:48px!important}.w3-jumbo{font-size:64px!important}
|
||||||
|
.w3-left-align{text-align:left!important}.w3-right-align{text-align:right!important}.w3-justify{text-align:justify!important}.w3-center{text-align:center!important}
|
||||||
|
.w3-border-0{border:0!important}.w3-border{border:1px solid #ccc!important}
|
||||||
|
.w3-border-top{border-top:1px solid #ccc!important}.w3-border-bottom{border-bottom:1px solid #ccc!important}
|
||||||
|
.w3-border-left{border-left:1px solid #ccc!important}.w3-border-right{border-right:1px solid #ccc!important}
|
||||||
|
.w3-topbar{border-top:6px solid #ccc!important}.w3-bottombar{border-bottom:6px solid #ccc!important}
|
||||||
|
.w3-leftbar{border-left:6px solid #ccc!important}.w3-rightbar{border-right:6px solid #ccc!important}
|
||||||
|
.w3-section,.w3-code{margin-top:16px!important;margin-bottom:16px!important}
|
||||||
|
.w3-margin{margin:16px!important}.w3-margin-top{margin-top:16px!important}.w3-margin-bottom{margin-bottom:16px!important}
|
||||||
|
.w3-margin-left{margin-left:16px!important}.w3-margin-right{margin-right:16px!important}
|
||||||
|
.w3-padding-small{padding:4px 8px!important}.w3-padding{padding:8px 16px!important}.w3-padding-large{padding:12px 24px!important}
|
||||||
|
.w3-padding-16{padding-top:16px!important;padding-bottom:16px!important}.w3-padding-24{padding-top:24px!important;padding-bottom:24px!important}
|
||||||
|
.w3-padding-32{padding-top:32px!important;padding-bottom:32px!important}.w3-padding-48{padding-top:48px!important;padding-bottom:48px!important}
|
||||||
|
.w3-padding-64{padding-top:64px!important;padding-bottom:64px!important}
|
||||||
|
.w3-padding-top-64{padding-top:64px!important}.w3-padding-top-48{padding-top:48px!important}
|
||||||
|
.w3-padding-top-32{padding-top:32px!important}.w3-padding-top-24{padding-top:24px!important}
|
||||||
|
.w3-left{float:left!important}.w3-right{float:right!important}
|
||||||
|
.w3-button:hover{color:#000!important;background-color:#ccc!important}
|
||||||
|
.w3-transparent,.w3-hover-none:hover{background-color:transparent!important}
|
||||||
|
.w3-hover-none:hover{box-shadow:none!important}
|
||||||
|
.w3-rtl{direction:rtl}.w3-ltr{direction:ltr}
|
||||||
|
/* Colors */
|
||||||
|
.w3-amber,.w3-hover-amber:hover{color:#000!important;background-color:#ffc107!important}
|
||||||
|
.w3-aqua,.w3-hover-aqua:hover{color:#000!important;background-color:#00ffff!important}
|
||||||
|
.w3-blue,.w3-hover-blue:hover{color:#fff!important;background-color:#2196F3!important}
|
||||||
|
.w3-light-blue,.w3-hover-light-blue:hover{color:#000!important;background-color:#87CEEB!important}
|
||||||
|
.w3-brown,.w3-hover-brown:hover{color:#fff!important;background-color:#795548!important}
|
||||||
|
.w3-cyan,.w3-hover-cyan:hover{color:#000!important;background-color:#00bcd4!important}
|
||||||
|
.w3-blue-grey,.w3-hover-blue-grey:hover,.w3-blue-gray,.w3-hover-blue-gray:hover{color:#fff!important;background-color:#607d8b!important}
|
||||||
|
.w3-green,.w3-hover-green:hover{color:#fff!important;background-color:#4CAF50!important}
|
||||||
|
.w3-light-green,.w3-hover-light-green:hover{color:#000!important;background-color:#8bc34a!important}
|
||||||
|
.w3-indigo,.w3-hover-indigo:hover{color:#fff!important;background-color:#3f51b5!important}
|
||||||
|
.w3-khaki,.w3-hover-khaki:hover{color:#000!important;background-color:#f0e68c!important}
|
||||||
|
.w3-lime,.w3-hover-lime:hover{color:#000!important;background-color:#cddc39!important}
|
||||||
|
.w3-orange,.w3-hover-orange:hover{color:#000!important;background-color:#ff9800!important}
|
||||||
|
.w3-deep-orange,.w3-hover-deep-orange:hover{color:#fff!important;background-color:#ff5722!important}
|
||||||
|
.w3-pink,.w3-hover-pink:hover{color:#fff!important;background-color:#e91e63!important}
|
||||||
|
.w3-purple,.w3-hover-purple:hover{color:#fff!important;background-color:#9c27b0!important}
|
||||||
|
.w3-deep-purple,.w3-hover-deep-purple:hover{color:#fff!important;background-color:#673ab7!important}
|
||||||
|
.w3-red,.w3-hover-red:hover{color:#fff!important;background-color:#f44336!important}
|
||||||
|
.w3-sand,.w3-hover-sand:hover{color:#000!important;background-color:#fdf5e6!important}
|
||||||
|
.w3-teal,.w3-hover-teal:hover{color:#fff!important;background-color:#009688!important}
|
||||||
|
.w3-yellow,.w3-hover-yellow:hover{color:#000!important;background-color:#ffeb3b!important}
|
||||||
|
.w3-white,.w3-hover-white:hover{color:#000!important;background-color:#fff!important}
|
||||||
|
.w3-black,.w3-hover-black:hover{color:#fff!important;background-color:#000!important}
|
||||||
|
.w3-grey,.w3-hover-grey:hover,.w3-gray,.w3-hover-gray:hover{color:#000!important;background-color:#9e9e9e!important}
|
||||||
|
.w3-light-grey,.w3-hover-light-grey:hover,.w3-light-gray,.w3-hover-light-gray:hover{color:#000!important;background-color:#f1f1f1!important}
|
||||||
|
.w3-dark-grey,.w3-hover-dark-grey:hover,.w3-dark-gray,.w3-hover-dark-gray:hover{color:#fff!important;background-color:#616161!important}
|
||||||
|
.w3-asphalt,.w3-hover-asphalt:hover{color:#fff!important;background-color:#343a40!important}
|
||||||
|
.w3-crimson,.w3-hover-crimson:hover{color:#fff!important;background-color:#a20025!important}
|
||||||
|
.w3-cobalt,w3-hover-cobalt:hover{color:#fff!important;background-color:#0050ef!important}
|
||||||
|
.w3-emerald,.w3-hover-emerald:hover{color:#fff!important;background-color:#008a00!important}
|
||||||
|
.w3-olive,.w3-hover-olive:hover{color:#fff!important;background-color:#6d8764!important}
|
||||||
|
.w3-paper,.w3-hover-paper:hover{color:#000!important;background-color:#f8f9fa!important}
|
||||||
|
.w3-sienna,.w3-hover-sienna:hover{color:#fff!important;background-color:#a0522d!important}
|
||||||
|
.w3-taupe,.w3-hover-taupe:hover{color:#fff!important;background-color:#87794e!important}
|
||||||
|
.w3-danger{color:#fff!important;background-color:#dd0000!important}
|
||||||
|
.w3-note{color:#000!important;background-color:#fff599!important}
|
||||||
|
.w3-info{color:#fff!important;background-color:#0a6fc2!important}
|
||||||
|
.w3-warning{color:#000!important;background-color:#ffb305!important}
|
||||||
|
.w3-success{color:#fff!important;background-color:#008a00!important}
|
||||||
|
.w3-pale-red,.w3-hover-pale-red:hover{color:#000!important;background-color:#ffdddd!important}
|
||||||
|
.w3-pale-green,.w3-hover-pale-green:hover{color:#000!important;background-color:#ddffdd!important}
|
||||||
|
.w3-pale-yellow,.w3-hover-pale-yellow:hover{color:#000!important;background-color:#ffffcc!important}
|
||||||
|
.w3-pale-blue,.w3-hover-pale-blue:hover{color:#000!important;background-color:#ddffff!important}
|
||||||
|
.w3-text-amber,.w3-hover-text-amber:hover{color:#ffc107!important}
|
||||||
|
.w3-text-aqua,.w3-hover-text-aqua:hover{color:#00ffff!important}
|
||||||
|
.w3-text-blue,.w3-hover-text-blue:hover{color:#2196F3!important}
|
||||||
|
.w3-text-light-blue,.w3-hover-text-light-blue:hover{color:#87CEEB!important}
|
||||||
|
.w3-text-brown,.w3-hover-text-brown:hover{color:#795548!important}
|
||||||
|
.w3-text-cyan,.w3-hover-text-cyan:hover{color:#00bcd4!important}
|
||||||
|
.w3-text-blue-grey,.w3-hover-text-blue-grey:hover,.w3-text-blue-gray,.w3-hover-text-blue-gray:hover{color:#607d8b!important}
|
||||||
|
.w3-text-green,.w3-hover-text-green:hover{color:#4CAF50!important}
|
||||||
|
.w3-text-light-green,.w3-hover-text-light-green:hover{color:#8bc34a!important}
|
||||||
|
.w3-text-indigo,.w3-hover-text-indigo:hover{color:#3f51b5!important}
|
||||||
|
.w3-text-khaki,.w3-hover-text-khaki:hover{color:#b4aa50!important}
|
||||||
|
.w3-text-lime,.w3-hover-text-lime:hover{color:#cddc39!important}
|
||||||
|
.w3-text-orange,.w3-hover-text-orange:hover{color:#ff9800!important}
|
||||||
|
.w3-text-deep-orange,.w3-hover-text-deep-orange:hover{color:#ff5722!important}
|
||||||
|
.w3-text-pink,.w3-hover-text-pink:hover{color:#e91e63!important}
|
||||||
|
.w3-text-purple,.w3-hover-text-purple:hover{color:#9c27b0!important}
|
||||||
|
.w3-text-deep-purple,.w3-hover-text-deep-purple:hover{color:#673ab7!important}
|
||||||
|
.w3-text-red,.w3-hover-text-red:hover{color:#f44336!important}
|
||||||
|
.w3-text-sand,.w3-hover-text-sand:hover{color:#fdf5e6!important}
|
||||||
|
.w3-text-teal,.w3-hover-text-teal:hover{color:#009688!important}
|
||||||
|
.w3-text-yellow,.w3-hover-text-yellow:hover{color:#d2be0e!important}
|
||||||
|
.w3-text-white,.w3-hover-text-white:hover{color:#fff!important}
|
||||||
|
.w3-text-black,.w3-hover-text-black:hover{color:#000!important}
|
||||||
|
.w3-text-grey,.w3-hover-text-grey:hover,.w3-text-gray,.w3-hover-text-gray:hover{color:#757575!important}
|
||||||
|
.w3-text-light-grey,.w3-hover-text-light-grey:hover,.w3-text-light-gray,.w3-hover-text-light-gray:hover{color:#f1f1f1!important}
|
||||||
|
.w3-text-dark-grey,.w3-hover-text-dark-grey:hover,.w3-text-dark-gray,.w3-hover-text-dark-gray:hover{color:#3a3a3a!important}
|
||||||
|
.w3-border-amber,.w3-hover-border-amber:hover{border-color:#ffc107!important}
|
||||||
|
.w3-border-aqua,.w3-hover-border-aqua:hover{border-color:#00ffff!important}
|
||||||
|
.w3-border-blue,.w3-hover-border-blue:hover{border-color:#2196F3!important}
|
||||||
|
.w3-border-light-blue,.w3-hover-border-light-blue:hover{border-color:#87CEEB!important}
|
||||||
|
.w3-border-brown,.w3-hover-border-brown:hover{border-color:#795548!important}
|
||||||
|
.w3-border-cyan,.w3-hover-border-cyan:hover{border-color:#00bcd4!important}
|
||||||
|
.w3-border-blue-grey,.w3-hover-border-blue-grey:hover,.w3-border-blue-gray,.w3-hover-border-blue-gray:hover{border-color:#607d8b!important}
|
||||||
|
.w3-border-green,.w3-hover-border-green:hover{border-color:#4CAF50!important}
|
||||||
|
.w3-border-light-green,.w3-hover-border-light-green:hover{border-color:#8bc34a!important}
|
||||||
|
.w3-border-indigo,.w3-hover-border-indigo:hover{border-color:#3f51b5!important}
|
||||||
|
.w3-border-khaki,.w3-hover-border-khaki:hover{border-color:#f0e68c!important}
|
||||||
|
.w3-border-lime,.w3-hover-border-lime:hover{border-color:#cddc39!important}
|
||||||
|
.w3-border-orange,.w3-hover-border-orange:hover{border-color:#ff9800!important}
|
||||||
|
.w3-border-deep-orange,.w3-hover-border-deep-orange:hover{border-color:#ff5722!important}
|
||||||
|
.w3-border-pink,.w3-hover-border-pink:hover{border-color:#e91e63!important}
|
||||||
|
.w3-border-purple,.w3-hover-border-purple:hover{border-color:#9c27b0!important}
|
||||||
|
.w3-border-deep-purple,.w3-hover-border-deep-purple:hover{border-color:#673ab7!important}
|
||||||
|
.w3-border-red,.w3-hover-border-red:hover{border-color:#f44336!important}
|
||||||
|
.w3-border-sand,.w3-hover-border-sand:hover{border-color:#fdf5e6!important}
|
||||||
|
.w3-border-teal,.w3-hover-border-teal:hover{border-color:#009688!important}
|
||||||
|
.w3-border-yellow,.w3-hover-border-yellow:hover{border-color:#ffeb3b!important}
|
||||||
|
.w3-border-white,.w3-hover-border-white:hover{border-color:#fff!important}
|
||||||
|
.w3-border-black,.w3-hover-border-black:hover{border-color:#000!important}
|
||||||
|
.w3-border-grey,.w3-hover-border-grey:hover,.w3-border-gray,.w3-hover-border-gray:hover{border-color:#9e9e9e!important}
|
||||||
|
.w3-border-light-grey,.w3-hover-border-light-grey:hover,.w3-border-light-gray,.w3-hover-border-light-gray:hover{border-color:#f1f1f1!important}
|
||||||
|
.w3-border-dark-grey,.w3-hover-border-dark-grey:hover,.w3-border-dark-gray,.w3-hover-border-dark-gray:hover{border-color:#616161!important}
|
||||||
|
.w3-border-pale-red,.w3-hover-border-pale-red:hover{border-color:#ffe7e7!important}.w3-border-pale-green,.w3-hover-border-pale-green:hover{border-color:#e7ffe7!important}
|
||||||
|
.w3-border-pale-yellow,.w3-hover-border-pale-yellow:hover{border-color:#ffffcc!important}.w3-border-pale-blue,.w3-hover-border-pale-blue:hover{border-color:#e7ffff!important}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"type": "tildefriends-app",
|
"type": "tildefriends-app",
|
||||||
"emoji": "🦟",
|
"emoji": "🦟",
|
||||||
"previous": "&cUqvSDUls3jn0haD85LPFAGdkc8wFuy347TtATNcJgg=.sha256"
|
"previous": "&O0huuEgL/UQC9bkUfhPOyZFo9eRiz+koOkba6QwCGNA=.sha256"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -67,9 +67,6 @@ tfrpc.register(function getHash(id, message) {
|
|||||||
tfrpc.register(function setHash(hash) {
|
tfrpc.register(function setHash(hash) {
|
||||||
return app.setHash(hash);
|
return app.setHash(hash);
|
||||||
});
|
});
|
||||||
ssb.addEventListener('message', async function (id) {
|
|
||||||
await tfrpc.rpc.notifyNewMessage(id);
|
|
||||||
});
|
|
||||||
tfrpc.register(async function store_blob(blob) {
|
tfrpc.register(async function store_blob(blob) {
|
||||||
if (Array.isArray(blob)) {
|
if (Array.isArray(blob)) {
|
||||||
blob = Uint8Array.from(blob);
|
blob = Uint8Array.from(blob);
|
||||||
@@ -91,10 +88,12 @@ tfrpc.register(function getActiveIdentity() {
|
|||||||
tfrpc.register(async function try_decrypt(id, content) {
|
tfrpc.register(async function try_decrypt(id, content) {
|
||||||
return await ssb.privateMessageDecrypt(id, content);
|
return await ssb.privateMessageDecrypt(id, content);
|
||||||
});
|
});
|
||||||
ssb.addEventListener('broadcasts', async function () {
|
core.register('onMessage', async function (id) {
|
||||||
|
await tfrpc.rpc.notifyNewMessage(id);
|
||||||
|
});
|
||||||
|
core.register('onBroadcastsChanged', async function () {
|
||||||
await tfrpc.rpc.set('broadcasts', await ssb.getBroadcasts());
|
await tfrpc.rpc.set('broadcasts', await ssb.getBroadcasts());
|
||||||
});
|
});
|
||||||
|
|
||||||
core.register('onConnectionsChanged', async function () {
|
core.register('onConnectionsChanged', async function () {
|
||||||
await tfrpc.rpc.set('connections', await ssb.connections());
|
await tfrpc.rpc.set('connections', await ssb.connections());
|
||||||
});
|
});
|
||||||
|
|||||||
2
apps/issues/commonmark.min.js
vendored
2
apps/issues/commonmark.min.js
vendored
File diff suppressed because one or more lines are too long
42
apps/issues/lit-all.min.js
vendored
42
apps/issues/lit-all.min.js
vendored
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1,5 +1,11 @@
|
|||||||
import * as linkify from './commonmark-linkify.js';
|
import * as linkify from './commonmark-linkify.js';
|
||||||
|
|
||||||
|
var reUnsafeProtocol = /^javascript:|vbscript:|file:|data:/i;
|
||||||
|
var reSafeDataProtocol = /^data:image\/(?:png|gif|jpeg|webp)/i;
|
||||||
|
var potentiallyUnsafe = function (url) {
|
||||||
|
return reUnsafeProtocol.test(url) && !reSafeDataProtocol.test(url);
|
||||||
|
};
|
||||||
|
|
||||||
function image(node, entering) {
|
function image(node, entering) {
|
||||||
if (
|
if (
|
||||||
node.firstChild?.type === 'text' &&
|
node.firstChild?.type === 'text' &&
|
||||||
@@ -61,8 +67,8 @@ function image(node, entering) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function markdown(md) {
|
export function markdown(md) {
|
||||||
var reader = new commonmark.Parser({safe: true});
|
var reader = new commonmark.Parser();
|
||||||
var writer = new commonmark.HtmlRenderer();
|
var writer = new commonmark.HtmlRenderer({safe: true});
|
||||||
writer.image = image;
|
writer.image = image;
|
||||||
var parsed = reader.parse(md || '');
|
var parsed = reader.parse(md || '');
|
||||||
parsed = linkify.transform(parsed);
|
parsed = linkify.transform(parsed);
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"type": "tildefriends-app",
|
"type": "tildefriends-app",
|
||||||
"emoji": "📝",
|
"emoji": "📝",
|
||||||
"previous": "&b//KqE4Vx6kOSBRODK1p/8wjOLKZJ+CBB5IkaBt5YsM=.sha256"
|
"previous": "&5LpOTEnor/rYFk3axyfmmehAoq9aEwNQRH4jwNhRQ7o=.sha256"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ function new_message() {
|
|||||||
return g_new_message_promise;
|
return g_new_message_promise;
|
||||||
}
|
}
|
||||||
|
|
||||||
ssb.addEventListener('message', function (id) {
|
core.register('onMessage', function (id) {
|
||||||
let resolve = g_new_message_resolve;
|
let resolve = g_new_message_resolve;
|
||||||
g_new_message_promise = new Promise(function (resolve, reject) {
|
g_new_message_promise = new Promise(function (resolve, reject) {
|
||||||
g_new_message_resolve = resolve;
|
g_new_message_resolve = resolve;
|
||||||
|
|||||||
2
apps/journal/commonmark.min.js
vendored
2
apps/journal/commonmark.min.js
vendored
File diff suppressed because one or more lines are too long
42
apps/journal/lit-all.min.js
vendored
42
apps/journal/lit-all.min.js
vendored
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -18,8 +18,8 @@ class TfJournalEntryElement extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
markdown(md) {
|
markdown(md) {
|
||||||
var reader = new commonmark.Parser({safe: true});
|
var reader = new commonmark.Parser();
|
||||||
var writer = new commonmark.HtmlRenderer();
|
var writer = new commonmark.HtmlRenderer({safe: true});
|
||||||
var parsed = reader.parse(md || '');
|
var parsed = reader.parse(md || '');
|
||||||
return writer.render(parsed);
|
return writer.render(parsed);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"type": "tildefriends-app",
|
"type": "tildefriends-app",
|
||||||
"emoji": "🚪",
|
"emoji": "🚪",
|
||||||
"previous": "&HXCdDG8gGYXElTyEFbg85jqa6lDXNL2ENPIA9UoJNbI=.sha256"
|
"previous": "&DJwkqNfYWtW9yBtJQMseEXm7l04Enpi+yAxZulLq9Vk=.sha256"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
async function main() {
|
async function main() {
|
||||||
let host = core.url.match(/.*\/\/(.*?)\//)[1];
|
print(core.url);
|
||||||
let id = (await ssb.getServerIdentity()).substring(1);
|
let host = core.url.match(/.*?\/\/([^:/]*)/)[1];
|
||||||
let room = `net:${host}:${ssb.port}~shs:${id}:SSB+Room+SK3TLYC2T86EHQCUHBUHASCASE18JBV24=`;
|
let port = await ssb.port();
|
||||||
|
let id = (await ssb.getServerIdentity()).substring(1).split('.')[0];
|
||||||
|
let room = `net:${host}:${port}~shs:${id}:SSB+Room+PSK3TLYC2T86EHQCUHBUHASCASE18JBV24=`;
|
||||||
await app.setDocument(`
|
await app.setDocument(`
|
||||||
<body style="color: #fff">
|
<body style="color: #fff">
|
||||||
<h1>Server</h1>
|
<h1>Server</h1>
|
||||||
|
|||||||
42
apps/sneaker/lit-all.min.js
vendored
42
apps/sneaker/lit-all.min.js
vendored
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"type": "tildefriends-app",
|
"type": "tildefriends-app",
|
||||||
"emoji": "🐌",
|
"emoji": "🦀",
|
||||||
"previous": "&raSj7ozmSDNGmB6TtjDk7oOiTc33ZN+RrBMASJ2F4cA=.sha256"
|
"previous": "&7dPNAI4sffljUTiwGr3XEUeB8sBD72CFkWMk/o0Z2pw=.sha256"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import * as tfrpc from '/tfrpc.js';
|
|||||||
|
|
||||||
let g_database;
|
let g_database;
|
||||||
let g_hash;
|
let g_hash;
|
||||||
|
let g_sql_cache = {};
|
||||||
|
|
||||||
tfrpc.register(async function localStorageGet(key) {
|
tfrpc.register(async function localStorageGet(key) {
|
||||||
return app.localStorageGet(key);
|
return app.localStorageGet(key);
|
||||||
@@ -21,9 +22,6 @@ tfrpc.register(async function createIdentity() {
|
|||||||
tfrpc.register(async function getServerIdentity() {
|
tfrpc.register(async function getServerIdentity() {
|
||||||
return ssb.getServerIdentity();
|
return ssb.getServerIdentity();
|
||||||
});
|
});
|
||||||
tfrpc.register(async function setServerFollowingMe(id, following) {
|
|
||||||
return ssb.setServerFollowingMe(id, following);
|
|
||||||
});
|
|
||||||
tfrpc.register(async function getIdentities() {
|
tfrpc.register(async function getIdentities() {
|
||||||
return ssb.getIdentities();
|
return ssb.getIdentities();
|
||||||
});
|
});
|
||||||
@@ -54,11 +52,38 @@ tfrpc.register(async function connect(token) {
|
|||||||
tfrpc.register(async function closeConnection(id) {
|
tfrpc.register(async function closeConnection(id) {
|
||||||
await ssb.closeConnection(id);
|
await ssb.closeConnection(id);
|
||||||
});
|
});
|
||||||
tfrpc.register(async function query(sql, args) {
|
tfrpc.register(async function query(sql, args, options) {
|
||||||
|
let start = new Date();
|
||||||
let result = [];
|
let result = [];
|
||||||
|
let key = options?.cacheable ? JSON.stringify([sql, args]) : undefined;
|
||||||
|
let entry = key ? g_sql_cache[key] : undefined;
|
||||||
|
const k_ideal_count = 64;
|
||||||
|
if (entry) {
|
||||||
|
result = entry.result;
|
||||||
|
} else {
|
||||||
await ssb.sqlAsync(sql, args, function callback(row) {
|
await ssb.sqlAsync(sql, args, function callback(row) {
|
||||||
result.push(row);
|
result.push(row);
|
||||||
});
|
});
|
||||||
|
if (key) {
|
||||||
|
g_sql_cache[key] = {
|
||||||
|
result: result,
|
||||||
|
time: new Date().valueOf(),
|
||||||
|
};
|
||||||
|
if (Object.keys(g_sql_cache).length > k_ideal_count * 2) {
|
||||||
|
let aged = Object.entries(g_sql_cache).map(([k, v]) => [v.time, k]);
|
||||||
|
aged.sort();
|
||||||
|
for (let i = 0; i < aged.length / 2; i++) {
|
||||||
|
delete g_sql_cache[aged[i][1]];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let end = new Date();
|
||||||
|
print(
|
||||||
|
(end - start) / 1000,
|
||||||
|
entry ? 'from cache' : 'from db',
|
||||||
|
sql.replaceAll(/\s+/g, ' ').trim()
|
||||||
|
);
|
||||||
return result;
|
return result;
|
||||||
});
|
});
|
||||||
tfrpc.register(async function appendMessage(id, message) {
|
tfrpc.register(async function appendMessage(id, message) {
|
||||||
@@ -76,9 +101,12 @@ tfrpc.register(function getHash(id, message) {
|
|||||||
tfrpc.register(function setHash(hash) {
|
tfrpc.register(function setHash(hash) {
|
||||||
return app.setHash(hash);
|
return app.setHash(hash);
|
||||||
});
|
});
|
||||||
ssb.addEventListener('message', async function (id) {
|
core.register('onMessage', async function (id) {
|
||||||
await tfrpc.rpc.notifyNewMessage(id);
|
await tfrpc.rpc.notifyNewMessage(id);
|
||||||
});
|
});
|
||||||
|
core.register('onBlob', async function (id) {
|
||||||
|
await tfrpc.rpc.notifyNewBlob(id);
|
||||||
|
});
|
||||||
tfrpc.register(async function store_blob(blob) {
|
tfrpc.register(async function store_blob(blob) {
|
||||||
if (Array.isArray(blob)) {
|
if (Array.isArray(blob)) {
|
||||||
blob = Uint8Array.from(blob);
|
blob = Uint8Array.from(blob);
|
||||||
@@ -103,7 +131,23 @@ tfrpc.register(async function encrypt(id, recipients, content) {
|
|||||||
tfrpc.register(async function getActiveIdentity() {
|
tfrpc.register(async function getActiveIdentity() {
|
||||||
return await ssb.getActiveIdentity();
|
return await ssb.getActiveIdentity();
|
||||||
});
|
});
|
||||||
ssb.addEventListener('broadcasts', async function () {
|
tfrpc.register(async function sync() {
|
||||||
|
return await ssb.sync();
|
||||||
|
});
|
||||||
|
tfrpc.register(async function url() {
|
||||||
|
return core.url;
|
||||||
|
});
|
||||||
|
tfrpc.register(async function globalSettingsGet(key) {
|
||||||
|
return core.globalSettingsGet(key);
|
||||||
|
});
|
||||||
|
tfrpc.register(async function globalSettingsSet(key, value) {
|
||||||
|
return core.globalSettingsSet(key, value);
|
||||||
|
});
|
||||||
|
tfrpc.register(function isAdministrator() {
|
||||||
|
return core.user?.credentials?.permissions?.administration;
|
||||||
|
});
|
||||||
|
|
||||||
|
core.register('onBroadcastsChanged', async function () {
|
||||||
await tfrpc.rpc.set('broadcasts', await ssb.getBroadcasts());
|
await tfrpc.rpc.set('broadcasts', await ssb.getBroadcasts());
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ function textNode(text) {
|
|||||||
function linkNode(text, link) {
|
function linkNode(text, link) {
|
||||||
const linkNode = new commonmark.Node('link', undefined);
|
const linkNode = new commonmark.Node('link', undefined);
|
||||||
if (link.startsWith('#')) {
|
if (link.startsWith('#')) {
|
||||||
linkNode.destination = `#q=${encodeURIComponent(link)}`;
|
linkNode.destination = `#${encodeURIComponent(link)}`;
|
||||||
} else {
|
} else {
|
||||||
linkNode.destination = link;
|
linkNode.destination = link;
|
||||||
}
|
}
|
||||||
|
|||||||
2
apps/ssb/commonmark.min.js
vendored
2
apps/ssb/commonmark.min.js
vendored
File diff suppressed because one or more lines are too long
@@ -1,4 +1,6 @@
|
|||||||
import * as tfrpc from '/static/tfrpc.js';
|
import * as tfrpc from '/static/tfrpc.js';
|
||||||
|
import {html, render} from './lit-all.min.js';
|
||||||
|
import {styles, generate_theme} from './tf-styles.js';
|
||||||
|
|
||||||
let g_emojis;
|
let g_emojis;
|
||||||
|
|
||||||
@@ -12,38 +14,20 @@ function get_emojis() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function get_recent(author) {
|
export async function picker(callback, anchor, author, recent) {
|
||||||
let recent = await tfrpc.rpc.query(
|
|
||||||
`
|
|
||||||
SELECT DISTINCT content ->> '$.vote.expression' AS value
|
|
||||||
FROM messages
|
|
||||||
WHERE author = ? AND
|
|
||||||
content ->> '$.type' = 'vote'
|
|
||||||
ORDER BY timestamp DESC LIMIT 10
|
|
||||||
`,
|
|
||||||
[author]
|
|
||||||
);
|
|
||||||
return recent.map((x) => x.value);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function picker(callback, anchor, author) {
|
|
||||||
let json = await get_emojis();
|
let json = await get_emojis();
|
||||||
let recent = await get_recent(author);
|
|
||||||
|
|
||||||
let div = document.createElement('div');
|
let div = document.createElement('div');
|
||||||
div.id = 'emoji_picker';
|
div.id = 'emoji_picker';
|
||||||
div.style.color = '#000';
|
div.style.color = '#000';
|
||||||
div.style.background = '#fff';
|
div.style.background = '#fff';
|
||||||
div.style.border = '1px solid #000';
|
div.style.border = '1px solid #000';
|
||||||
div.style.display = 'block';
|
div.style.display = 'flex';
|
||||||
div.style.position = 'absolute';
|
|
||||||
div.style.minWidth = 'min(16em, 90vw)';
|
|
||||||
div.style.width = 'min(16em, 90vw)';
|
|
||||||
div.style.maxWidth = 'min(16em, 90vw)';
|
|
||||||
div.style.maxHeight = '16em';
|
|
||||||
div.style.overflow = 'scroll';
|
div.style.overflow = 'scroll';
|
||||||
div.style.fontWeight = 'bold';
|
div.style.fontWeight = 'bold';
|
||||||
div.style.fontSize = 'xx-large';
|
div.style.fontSize = 'xx-large';
|
||||||
|
div.style.flex = '1 1';
|
||||||
|
div.style.flexDirection = 'column';
|
||||||
let input = document.createElement('input');
|
let input = document.createElement('input');
|
||||||
input.type = 'text';
|
input.type = 'text';
|
||||||
input.style.display = 'block';
|
input.style.display = 'block';
|
||||||
@@ -53,19 +37,12 @@ export async function picker(callback, anchor, author) {
|
|||||||
input.style.position = 'relative';
|
input.style.position = 'relative';
|
||||||
div.appendChild(input);
|
div.appendChild(input);
|
||||||
let list = document.createElement('div');
|
let list = document.createElement('div');
|
||||||
|
list.style.overflow = 'scroll';
|
||||||
div.appendChild(list);
|
div.appendChild(list);
|
||||||
div.addEventListener('mousedown', function (event) {
|
div.addEventListener('mousedown', function (event) {
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
});
|
});
|
||||||
|
|
||||||
function cleanup() {
|
|
||||||
console.log('emoji cleanup');
|
|
||||||
div.parentElement.removeChild(div);
|
|
||||||
window.removeEventListener('keydown', key_down);
|
|
||||||
console.log('removing click');
|
|
||||||
document.body.removeEventListener('mousedown', cleanup);
|
|
||||||
}
|
|
||||||
|
|
||||||
function key_down(event) {
|
function key_down(event) {
|
||||||
if (event.key == 'Escape') {
|
if (event.key == 'Escape') {
|
||||||
cleanup();
|
cleanup();
|
||||||
@@ -153,13 +130,45 @@ export async function picker(callback, anchor, author) {
|
|||||||
}
|
}
|
||||||
refresh();
|
refresh();
|
||||||
input.oninput = refresh;
|
input.oninput = refresh;
|
||||||
document.body.appendChild(div);
|
let parent = document.createElement('div');
|
||||||
div.style.position = 'fixed';
|
function cleanup() {
|
||||||
div.style.top = '50%';
|
parent.parentElement.removeChild(parent);
|
||||||
div.style.left = '50%';
|
window.removeEventListener('keydown', key_down);
|
||||||
div.style.transform = 'translate(-50%, -50%)';
|
document.body.removeEventListener('mousedown', cleanup);
|
||||||
|
}
|
||||||
|
let modal = html`
|
||||||
|
<style>
|
||||||
|
${styles}
|
||||||
|
</style>
|
||||||
|
<style>
|
||||||
|
${generate_theme()}
|
||||||
|
</style>
|
||||||
|
<div
|
||||||
|
class="w3-modal"
|
||||||
|
style="display: block; box-sizing: border-box; z-index: 10"
|
||||||
|
>
|
||||||
|
<div class="w3-modal-content w3-card-4">
|
||||||
|
<div
|
||||||
|
class="w3-content w3-theme-d1"
|
||||||
|
style="display: flex; flex-direction: column; max-height: 80vh"
|
||||||
|
>
|
||||||
|
<header class="w3-container" style="flex: 0 0">
|
||||||
|
<h1>Choose a Reaction</h1>
|
||||||
|
<span class="w3-button w3-display-topright" @click=${cleanup}
|
||||||
|
>×</span
|
||||||
|
>
|
||||||
|
</header>
|
||||||
|
${div}
|
||||||
|
<footer class="w3-container w3-padding" style="flex: 0 0">
|
||||||
|
<button class="w3-button" @click=${cleanup}>Close</button>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
document.body.appendChild(parent);
|
||||||
|
render(modal, parent);
|
||||||
input.focus();
|
input.focus();
|
||||||
console.log('adding click');
|
|
||||||
document.body.addEventListener('mousedown', cleanup);
|
document.body.addEventListener('mousedown', cleanup);
|
||||||
window.addEventListener('keydown', key_down);
|
window.addEventListener('keydown', key_down);
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
42
apps/ssb/lit-all.min.js
vendored
42
apps/ssb/lit-all.min.js
vendored
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -8,10 +8,18 @@ import * as tf_compose from './tf-compose.js';
|
|||||||
import * as tf_news from './tf-news.js';
|
import * as tf_news from './tf-news.js';
|
||||||
import * as tf_profile from './tf-profile.js';
|
import * as tf_profile from './tf-profile.js';
|
||||||
import * as tf_reactions_modal from './tf-reactions-modal.js';
|
import * as tf_reactions_modal from './tf-reactions-modal.js';
|
||||||
import * as tf_tab_mentions from './tf-tab-mentions.js';
|
|
||||||
import * as tf_tab_news from './tf-tab-news.js';
|
import * as tf_tab_news from './tf-tab-news.js';
|
||||||
import * as tf_tab_news_feed from './tf-tab-news-feed.js';
|
import * as tf_tab_news_feed from './tf-tab-news-feed.js';
|
||||||
import * as tf_tab_search from './tf-tab-search.js';
|
import * as tf_tab_search from './tf-tab-search.js';
|
||||||
import * as tf_tab_connections from './tf-tab-connections.js';
|
import * as tf_tab_connections from './tf-tab-connections.js';
|
||||||
import * as tf_tab_query from './tf-tab-query.js';
|
|
||||||
import * as tf_tag from './tf-tag.js';
|
import * as tf_tag from './tf-tag.js';
|
||||||
|
import * as tf_styles from './tf-styles.js';
|
||||||
|
|
||||||
|
window.addEventListener('load', function () {
|
||||||
|
let style = document.createElement('style');
|
||||||
|
style.innerText = tf_styles.styles;
|
||||||
|
Promise.resolve(tf_styles.generate_theme()).then(function (x) {
|
||||||
|
style.innerText += x;
|
||||||
|
});
|
||||||
|
document.body.appendChild(style);
|
||||||
|
});
|
||||||
|
|||||||
@@ -1,22 +1,33 @@
|
|||||||
import {LitElement, html, css, guard, until} from './lit-all.min.js';
|
import {LitElement, html, css, guard, until} from './lit-all.min.js';
|
||||||
import * as tfrpc from '/static/tfrpc.js';
|
import * as tfrpc from '/static/tfrpc.js';
|
||||||
import {styles} from './tf-styles.js';
|
import {styles, generate_theme} from './tf-styles.js';
|
||||||
|
|
||||||
class TfElement extends LitElement {
|
class TfElement extends LitElement {
|
||||||
static get properties() {
|
static get properties() {
|
||||||
return {
|
return {
|
||||||
whoami: {type: String},
|
whoami: {type: String},
|
||||||
hash: {type: String},
|
hash: {type: String},
|
||||||
unread: {type: Array},
|
|
||||||
tab: {type: String},
|
tab: {type: String},
|
||||||
broadcasts: {type: Array},
|
broadcasts: {type: Array},
|
||||||
connections: {type: Array},
|
connections: {type: Array},
|
||||||
loading: {type: Boolean},
|
loading: {type: Boolean},
|
||||||
|
loading_about: {type: Number},
|
||||||
loaded: {type: Boolean},
|
loaded: {type: Boolean},
|
||||||
following: {type: Array},
|
following: {type: Array},
|
||||||
users: {type: Object},
|
users: {type: Object},
|
||||||
ids: {type: Array},
|
ids: {type: Array},
|
||||||
tags: {type: Array},
|
channels: {type: Array},
|
||||||
|
channels_unread: {type: Object},
|
||||||
|
channels_latest: {type: Object},
|
||||||
|
guest: {type: Boolean},
|
||||||
|
url: {type: String},
|
||||||
|
private_closed: {type: Object},
|
||||||
|
private_messages: {type: Array},
|
||||||
|
grouped_private_messages: {type: Object},
|
||||||
|
recent_reactions: {type: Array},
|
||||||
|
is_administrator: {type: Boolean},
|
||||||
|
stay_connected: {type: Boolean},
|
||||||
|
progress: {type: Number},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -26,14 +37,20 @@ class TfElement extends LitElement {
|
|||||||
super();
|
super();
|
||||||
let self = this;
|
let self = this;
|
||||||
this.hash = '#';
|
this.hash = '#';
|
||||||
this.unread = [];
|
|
||||||
this.tab = 'news';
|
this.tab = 'news';
|
||||||
this.broadcasts = [];
|
this.broadcasts = [];
|
||||||
this.connections = [];
|
this.connections = [];
|
||||||
this.following = [];
|
this.following = [];
|
||||||
this.users = {};
|
this.users = {};
|
||||||
this.loaded = false;
|
this.loaded = false;
|
||||||
this.tags = [];
|
this.loading_about = 0;
|
||||||
|
this.channels = [];
|
||||||
|
this.channels_unread = {};
|
||||||
|
this.channels_latest = {};
|
||||||
|
this.loading_latest = 0;
|
||||||
|
this.loading_latest_scheduled = 0;
|
||||||
|
this.recent_reactions = [];
|
||||||
|
this.private_closed = {};
|
||||||
tfrpc.rpc.getBroadcasts().then((b) => {
|
tfrpc.rpc.getBroadcasts().then((b) => {
|
||||||
self.broadcasts = b || [];
|
self.broadcasts = b || [];
|
||||||
});
|
});
|
||||||
@@ -43,10 +60,22 @@ class TfElement extends LitElement {
|
|||||||
tfrpc.rpc.getHash().then((hash) => self.set_hash(hash));
|
tfrpc.rpc.getHash().then((hash) => self.set_hash(hash));
|
||||||
tfrpc.register(function hashChanged(hash) {
|
tfrpc.register(function hashChanged(hash) {
|
||||||
self.set_hash(hash);
|
self.set_hash(hash);
|
||||||
|
self.reset_progress();
|
||||||
});
|
});
|
||||||
tfrpc.register(async function notifyNewMessage(id) {
|
tfrpc.register(async function notifyNewMessage(id) {
|
||||||
await self.fetch_new_message(id);
|
await self.fetch_new_message(id);
|
||||||
});
|
});
|
||||||
|
tfrpc.register(async function notifyNewBlob(id) {
|
||||||
|
window.dispatchEvent(
|
||||||
|
new CustomEvent('blob-stored', {
|
||||||
|
bubbles: true,
|
||||||
|
composed: true,
|
||||||
|
detail: {
|
||||||
|
id: id,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
tfrpc.register(function set(name, value) {
|
tfrpc.register(function set(name, value) {
|
||||||
if (name === 'broadcasts') {
|
if (name === 'broadcasts') {
|
||||||
self.broadcasts = value;
|
self.broadcasts = value;
|
||||||
@@ -62,98 +91,217 @@ class TfElement extends LitElement {
|
|||||||
async initial_load() {
|
async initial_load() {
|
||||||
let whoami = await tfrpc.rpc.getActiveIdentity();
|
let whoami = await tfrpc.rpc.getActiveIdentity();
|
||||||
let ids = (await tfrpc.rpc.getIdentities()) || [];
|
let ids = (await tfrpc.rpc.getIdentities()) || [];
|
||||||
|
this.is_administrator = await tfrpc.rpc.isAdministrator();
|
||||||
|
this.stay_connected =
|
||||||
|
this.is_administrator &&
|
||||||
|
(await tfrpc.rpc.globalSettingsGet('stay_connected'));
|
||||||
|
this.url = await tfrpc.rpc.url();
|
||||||
this.whoami = whoami ?? (ids.length ? ids[0] : undefined);
|
this.whoami = whoami ?? (ids.length ? ids[0] : undefined);
|
||||||
|
this.guest = !this.whoami?.length;
|
||||||
this.ids = ids;
|
this.ids = ids;
|
||||||
|
let private_closed =
|
||||||
|
(await tfrpc.rpc.databaseGet('private_closed')) ?? '{}';
|
||||||
|
this.private_closed = JSON.parse(private_closed);
|
||||||
|
await this.load_channels();
|
||||||
|
}
|
||||||
|
|
||||||
|
async close_private_chat(event) {
|
||||||
|
let update = {};
|
||||||
|
update[event.detail.key] = true;
|
||||||
|
this.private_closed = Object.assign(update, this.private_closed);
|
||||||
|
await tfrpc.rpc.databaseSet(
|
||||||
|
'private_closed',
|
||||||
|
JSON.stringify(this.private_closed)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async load_channels() {
|
||||||
|
let channels = await tfrpc.rpc.query(
|
||||||
|
`
|
||||||
|
SELECT
|
||||||
|
content ->> 'channel' AS channel,
|
||||||
|
content ->> 'subscribed' AS subscribed
|
||||||
|
FROM
|
||||||
|
messages
|
||||||
|
WHERE
|
||||||
|
author = ? AND
|
||||||
|
content ->> 'type' = 'channel'
|
||||||
|
ORDER BY sequence
|
||||||
|
`,
|
||||||
|
[this.whoami]
|
||||||
|
);
|
||||||
|
let channel_map = {};
|
||||||
|
for (let row of channels) {
|
||||||
|
if (row.subscribed) {
|
||||||
|
channel_map[row.channel] = true;
|
||||||
|
} else {
|
||||||
|
delete channel_map[row.channel];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.channels = Object.keys(channel_map).sort();
|
||||||
|
}
|
||||||
|
|
||||||
|
connectedCallback() {
|
||||||
|
super.connectedCallback();
|
||||||
|
this._keydown = this.keydown.bind(this);
|
||||||
|
window.addEventListener('keydown', this._keydown);
|
||||||
|
}
|
||||||
|
|
||||||
|
disconnectedCallback() {
|
||||||
|
super.disconnectedCallback();
|
||||||
|
window.removeEventListener('keydown', this._keydown);
|
||||||
|
}
|
||||||
|
|
||||||
|
keydown(event) {
|
||||||
|
if (event.altKey && event.key == 'ArrowUp') {
|
||||||
|
this.next_channel(-1);
|
||||||
|
event.preventDefault();
|
||||||
|
} else if (event.altKey && event.key == 'ArrowDown') {
|
||||||
|
this.next_channel(1);
|
||||||
|
event.preventDefault();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
visible_private() {
|
||||||
|
if (!this.grouped_private_messages || !this.private_closed) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
let self = this;
|
||||||
|
return Object.fromEntries(
|
||||||
|
Object.entries(this.grouped_private_messages).filter(([key, value]) => {
|
||||||
|
let channel = '🔐' + [...new Set(JSON.parse(key))].sort().join(',');
|
||||||
|
let grouped_latest = Math.max(...value.map((x) => x.rowid));
|
||||||
|
return (
|
||||||
|
!self.private_closed[key] ||
|
||||||
|
self.channels_unread[channel] === undefined ||
|
||||||
|
grouped_latest > self.channels_unread[channel]
|
||||||
|
);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
next_channel(delta) {
|
||||||
|
let channel_names = [
|
||||||
|
'',
|
||||||
|
'@',
|
||||||
|
'👍',
|
||||||
|
...Object.keys(this.visible_private())
|
||||||
|
.sort()
|
||||||
|
.map((x) => '🔐' + JSON.parse(x).join(',')),
|
||||||
|
...this.channels.map((x) => '#' + x),
|
||||||
|
];
|
||||||
|
let index = channel_names.indexOf(this.hash.substring(1));
|
||||||
|
index = index != -1 ? index + delta : 0;
|
||||||
|
tfrpc.rpc.setHash(
|
||||||
|
'#' +
|
||||||
|
encodeURIComponent(
|
||||||
|
channel_names[(index + channel_names.length) % channel_names.length]
|
||||||
|
)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
set_hash(hash) {
|
set_hash(hash) {
|
||||||
this.hash = hash || '#';
|
this.hash = decodeURIComponent(hash || '#');
|
||||||
if (this.hash.startsWith('#q=')) {
|
if (this.hash.startsWith('#q=')) {
|
||||||
this.tab = 'search';
|
this.tab = 'search';
|
||||||
} else if (this.hash === '#connections') {
|
} else if (this.hash === '#connections') {
|
||||||
this.tab = 'connections';
|
this.tab = 'connections';
|
||||||
} else if (this.hash === '#mentions') {
|
|
||||||
this.tab = 'mentions';
|
|
||||||
} else if (this.hash.startsWith('#sql=')) {
|
|
||||||
this.tab = 'query';
|
|
||||||
} else {
|
} else {
|
||||||
this.tab = 'news';
|
this.tab = 'news';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fetch_about(ids, users) {
|
async fetch_about(following, users) {
|
||||||
const k_cache_version = 1;
|
this.loading_about++;
|
||||||
|
let ids = Object.keys(following).sort();
|
||||||
|
const k_cache_version = 3;
|
||||||
let cache = await tfrpc.rpc.databaseGet('about');
|
let cache = await tfrpc.rpc.databaseGet('about');
|
||||||
|
let original_cache = cache;
|
||||||
cache = cache ? JSON.parse(cache) : {};
|
cache = cache ? JSON.parse(cache) : {};
|
||||||
if (cache.version !== k_cache_version) {
|
if (cache.version !== k_cache_version) {
|
||||||
cache = {
|
cache = {
|
||||||
version: k_cache_version,
|
version: k_cache_version,
|
||||||
about: {},
|
about: {},
|
||||||
last_row_id: 0,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
let max_row_id = (
|
|
||||||
await tfrpc.rpc.query(
|
let ids_out_of_date = ids.filter(
|
||||||
`
|
(x) =>
|
||||||
SELECT MAX(rowid) AS max_row_id FROM messages
|
(users[x]?.seq && !cache.about[x]?.seq) ||
|
||||||
`,
|
(users[x]?.seq && users[x]?.seq > cache.about[x].seq)
|
||||||
[]
|
);
|
||||||
)
|
|
||||||
)[0].max_row_id;
|
|
||||||
for (let id of Object.keys(cache.about)) {
|
for (let id of Object.keys(cache.about)) {
|
||||||
if (ids.indexOf(id) == -1) {
|
if (ids.indexOf(id) == -1) {
|
||||||
delete cache.about[id];
|
delete cache.about[id];
|
||||||
|
} else {
|
||||||
|
users[id] = Object.assign(cache.about[id], users[id] || {});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let abouts = await tfrpc.rpc.query(
|
console.log(
|
||||||
|
'loading about for',
|
||||||
|
ids.length,
|
||||||
|
'accounts',
|
||||||
|
ids_out_of_date.length,
|
||||||
|
'out of date'
|
||||||
|
);
|
||||||
|
if (ids_out_of_date.length) {
|
||||||
|
try {
|
||||||
|
let rows = await tfrpc.rpc.query(
|
||||||
`
|
`
|
||||||
|
SELECT all_abouts.author, json(json_group_object(all_abouts.key, all_abouts.value)) AS about
|
||||||
|
FROM (
|
||||||
SELECT
|
SELECT
|
||||||
messages.id, messages.previous, messages.author, messages.sequence, messages.timestamp, messages.hash, json(messages.content) AS content, messages.signature
|
messages.author,
|
||||||
FROM
|
fields.key,
|
||||||
messages,
|
RANK() OVER (PARTITION BY messages.author, fields.key ORDER BY messages.sequence DESC) AS rank,
|
||||||
json_each(?1) AS following
|
fields.value
|
||||||
|
FROM messages JOIN json_each(messages.content) AS fields
|
||||||
WHERE
|
WHERE
|
||||||
messages.author = following.value AND
|
messages.content ->> '$.type' = 'about' AND
|
||||||
messages.rowid > ?3 AND
|
messages.content ->> '$.about' = messages.author AND
|
||||||
messages.rowid <= ?4 AND
|
NOT fields.key IN ('about', 'type')) all_abouts
|
||||||
json_extract(messages.content, '$.type') = 'about'
|
JOIN json_each(?) AS following ON all_abouts.author = following.value
|
||||||
UNION
|
WHERE rank = 1
|
||||||
SELECT
|
GROUP BY all_abouts.author
|
||||||
messages.id, messages.previous, messages.author, messages.sequence, messages.timestamp, messages.hash, json(messages.content) AS content, messages.signature
|
|
||||||
FROM
|
|
||||||
messages,
|
|
||||||
json_each(?2) AS following
|
|
||||||
WHERE
|
|
||||||
messages.author = following.value AND
|
|
||||||
messages.rowid <= ?4 AND
|
|
||||||
json_extract(messages.content, '$.type') = 'about'
|
|
||||||
ORDER BY messages.author, messages.sequence
|
|
||||||
`,
|
`,
|
||||||
[
|
[JSON.stringify(ids_out_of_date)]
|
||||||
JSON.stringify(ids.filter((id) => cache.about[id])),
|
|
||||||
JSON.stringify(ids.filter((id) => !cache.about[id])),
|
|
||||||
cache.last_row_id,
|
|
||||||
max_row_id,
|
|
||||||
]
|
|
||||||
);
|
);
|
||||||
for (let about of abouts) {
|
|
||||||
let content = JSON.parse(about.content);
|
|
||||||
if (content.about === about.author) {
|
|
||||||
delete content.type;
|
|
||||||
delete content.about;
|
|
||||||
cache.about[about.author] = Object.assign(
|
|
||||||
cache.about[about.author] || {},
|
|
||||||
content
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
cache.last_row_id = max_row_id;
|
|
||||||
await tfrpc.rpc.databaseSet('about', JSON.stringify(cache));
|
|
||||||
users = users || {};
|
users = users || {};
|
||||||
for (let id of Object.keys(cache.about)) {
|
for (let row of rows) {
|
||||||
users[id] = Object.assign(users[id] || {}, cache.about[id]);
|
users[row.author] = Object.assign(
|
||||||
|
users[row.author] || {},
|
||||||
|
JSON.parse(row.about)
|
||||||
|
);
|
||||||
|
cache.about[row.author] = Object.assign(
|
||||||
|
{seq: users[row.author].seq},
|
||||||
|
JSON.parse(row.about)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.log(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let id of ids_out_of_date) {
|
||||||
|
if (!cache.about[id]?.seq) {
|
||||||
|
cache.about[id] = Object.assign(cache.about[id] ?? {}, {
|
||||||
|
seq: users[id]?.seq ?? 0,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.loading_about--;
|
||||||
|
|
||||||
|
let new_cache = JSON.stringify(cache);
|
||||||
|
if (new_cache != original_cache) {
|
||||||
|
let start_time = new Date();
|
||||||
|
tfrpc.rpc.databaseSet('about', new_cache).then(function () {
|
||||||
|
console.log('saving about took', (new Date() - start_time) / 1000);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return Object.assign({}, users);
|
return Object.assign({}, users);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -167,11 +315,16 @@ class TfElement extends LitElement {
|
|||||||
`,
|
`,
|
||||||
[JSON.stringify(this.following), id]
|
[JSON.stringify(this.following), id]
|
||||||
);
|
);
|
||||||
if (messages && messages.length) {
|
for (let message of messages) {
|
||||||
this.unread = [...this.unread, ...messages];
|
if (
|
||||||
this.unread = this.unread.slice(this.unread.length - 1024);
|
message.author == this.whoami &&
|
||||||
|
JSON.parse(message.content)?.type == 'channel'
|
||||||
|
) {
|
||||||
|
this.load_channels();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
this.schedule_load_latest();
|
||||||
|
}
|
||||||
|
|
||||||
async _handle_whoami_changed(event) {
|
async _handle_whoami_changed(event) {
|
||||||
let old_id = this.whoami;
|
let old_id = this.whoami;
|
||||||
@@ -185,70 +338,344 @@ class TfElement extends LitElement {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async create_identity() {
|
async get_latest_private(following) {
|
||||||
if (confirm('Are you sure you want to create a new identity?')) {
|
const k_version = 1;
|
||||||
await tfrpc.rpc.createIdentity();
|
// { "version": 1, "range": [1234, 5678], messages: [ "%1.sha256", "%2.sha256", ... ], latest: rowid }
|
||||||
this.ids = (await tfrpc.rpc.getIdentities()) || [];
|
let cache = JSON.parse(
|
||||||
if (this.ids && !this.whoami) {
|
(await tfrpc.rpc.databaseGet(`private:${this.whoami}`)) ?? '{}'
|
||||||
this.whoami = this.ids[0];
|
);
|
||||||
|
if (cache.version !== k_version) {
|
||||||
|
cache = {
|
||||||
|
version: k_version,
|
||||||
|
messages: [],
|
||||||
|
range: [],
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
let latest = (
|
||||||
|
await tfrpc.rpc.query('SELECT MAX(rowid) AS latest FROM messages')
|
||||||
|
)[0].latest;
|
||||||
|
let ranges = [];
|
||||||
|
const k_chunk_size = 512;
|
||||||
|
if (cache.range.length) {
|
||||||
|
for (let i = cache.range[1]; i < latest; i += k_chunk_size) {
|
||||||
|
ranges.push([i, Math.min(i + k_chunk_size, latest), true]);
|
||||||
|
}
|
||||||
|
for (let i = cache.range[0]; i >= 0; i -= k_chunk_size) {
|
||||||
|
ranges.push([Math.max(i - k_chunk_size, 0), i, false]);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for (let i = 0; i < latest; i += k_chunk_size) {
|
||||||
|
ranges.push([i, Math.min(i + k_chunk_size, latest), true]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (let range of ranges) {
|
||||||
|
let messages = await tfrpc.rpc.query(
|
||||||
|
`
|
||||||
|
SELECT messages.rowid, messages.id, json(content) AS content
|
||||||
|
FROM messages
|
||||||
|
WHERE
|
||||||
|
messages.rowid > ?1 AND
|
||||||
|
messages.rowid <= ?2 AND
|
||||||
|
json(messages.content) LIKE '"%'
|
||||||
|
ORDER BY messages.rowid DESC
|
||||||
|
`,
|
||||||
|
[range[0], range[1]]
|
||||||
|
);
|
||||||
|
messages = (await this.decrypt(messages)).filter((x) => x.decrypted);
|
||||||
|
if (messages.length) {
|
||||||
|
cache.latest = Math.max(
|
||||||
|
cache.latest ?? 0,
|
||||||
|
...messages.map((x) => x.rowid)
|
||||||
|
);
|
||||||
|
if (range[2]) {
|
||||||
|
cache.messages = [...cache.messages, ...messages.map((x) => x.id)];
|
||||||
|
} else {
|
||||||
|
cache.messages = [...messages.map((x) => x.id), ...cache.messages];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cache.range[0] = Math.min(cache.range[0] ?? range[0], range[0]);
|
||||||
|
cache.range[1] = Math.max(cache.range[1] ?? range[1], range[1]);
|
||||||
|
await tfrpc.rpc.databaseSet(
|
||||||
|
`private:${this.whoami}`,
|
||||||
|
JSON.stringify(cache)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return [cache.latest, cache.messages];
|
||||||
|
}
|
||||||
|
|
||||||
|
async group_private_messages(messages) {
|
||||||
|
let groups = {};
|
||||||
|
let result = await this.decrypt(
|
||||||
|
await tfrpc.rpc.query(
|
||||||
|
`
|
||||||
|
SELECT messages.rowid, messages.id, author, timestamp, json(content) AS content
|
||||||
|
FROM messages
|
||||||
|
JOIN json_each(?) AS ids
|
||||||
|
WHERE messages.id = ids.value
|
||||||
|
ORDER BY timestamp DESC
|
||||||
|
`,
|
||||||
|
[JSON.stringify(messages)]
|
||||||
|
)
|
||||||
|
);
|
||||||
|
for (let message of result) {
|
||||||
|
let key = JSON.stringify(
|
||||||
|
[
|
||||||
|
...new Set(
|
||||||
|
message?.decrypted?.recps?.filter((x) => x != this.whoami)
|
||||||
|
),
|
||||||
|
].sort() ?? []
|
||||||
|
);
|
||||||
|
if (!groups[key]) {
|
||||||
|
groups[key] = [];
|
||||||
|
}
|
||||||
|
groups[key].push(message);
|
||||||
|
}
|
||||||
|
return groups;
|
||||||
|
}
|
||||||
|
|
||||||
|
async load_channels_latest(following) {
|
||||||
|
let start_time = new Date();
|
||||||
|
let latest_private = this.get_latest_private(following);
|
||||||
|
const k_args = [
|
||||||
|
JSON.stringify(this.channels),
|
||||||
|
JSON.stringify(following),
|
||||||
|
'"' + this.whoami.replace('"', '""') + '"',
|
||||||
|
this.whoami,
|
||||||
|
];
|
||||||
|
let channels = (
|
||||||
|
await Promise.all([
|
||||||
|
tfrpc.rpc.query(
|
||||||
|
`
|
||||||
|
SELECT channels.value AS channel, MAX(messages.rowid) AS rowid FROM messages
|
||||||
|
JOIN json_each(?1) AS channels ON messages.content ->> 'channel' = channels.value
|
||||||
|
JOIN json_each(?2) AS following ON messages.author = following.value
|
||||||
|
WHERE
|
||||||
|
messages.content ->> 'type' = 'post' AND
|
||||||
|
messages.content ->> 'root' IS NULL AND
|
||||||
|
messages.author != ?4
|
||||||
|
GROUP by channel
|
||||||
|
`,
|
||||||
|
k_args
|
||||||
|
),
|
||||||
|
tfrpc.rpc.query(
|
||||||
|
`
|
||||||
|
SELECT channels.value AS channel, MAX(messages.rowid) AS rowid FROM messages
|
||||||
|
JOIN messages_refs ON messages.id = messages_refs.message
|
||||||
|
JOIN json_each(?1) AS channels ON messages_refs.ref = '#' || channels.value
|
||||||
|
JOIN json_each(?2) AS following ON messages.author = following.value
|
||||||
|
WHERE
|
||||||
|
messages.content ->> 'type' = 'post' AND
|
||||||
|
messages.content ->> 'root' IS NULL AND
|
||||||
|
messages.author != ?4
|
||||||
|
GROUP by channel
|
||||||
|
`,
|
||||||
|
k_args
|
||||||
|
),
|
||||||
|
tfrpc.rpc.query(
|
||||||
|
`
|
||||||
|
SELECT '' AS channel, MAX(messages.rowid) AS rowid FROM messages
|
||||||
|
JOIN json_each(?2) AS following ON messages.author = following.value
|
||||||
|
WHERE
|
||||||
|
messages.content ->> 'type' = 'post' AND
|
||||||
|
messages.content ->> 'root' IS NULL AND
|
||||||
|
messages.author != ?4
|
||||||
|
`,
|
||||||
|
k_args
|
||||||
|
),
|
||||||
|
tfrpc.rpc.query(
|
||||||
|
`
|
||||||
|
SELECT '@' AS channel, MAX(messages.rowid) AS rowid FROM messages_fts(?3)
|
||||||
|
JOIN messages ON messages.rowid = messages_fts.rowid
|
||||||
|
JOIN json_each(?2) AS following ON messages.author = following.value
|
||||||
|
WHERE messages.author != ?4
|
||||||
|
`,
|
||||||
|
k_args
|
||||||
|
),
|
||||||
|
])
|
||||||
|
).flat();
|
||||||
|
let latest = {'🔐': undefined};
|
||||||
|
for (let row of channels) {
|
||||||
|
if (!latest[row.channel]) {
|
||||||
|
latest[row.channel] = row.rowid;
|
||||||
|
} else {
|
||||||
|
latest[row.channel] = Math.max(row.rowid, latest[row.channel]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.channels_latest = latest;
|
||||||
|
console.log('channels took', (new Date() - start_time) / 1000.0);
|
||||||
|
let self = this;
|
||||||
|
start_time = new Date();
|
||||||
|
latest_private.then(async function (latest) {
|
||||||
|
let grouped = await self.group_private_messages(latest[1]);
|
||||||
|
self.channels_latest = Object.assign({}, self.channels_latest, {
|
||||||
|
'🔐': latest[0],
|
||||||
|
});
|
||||||
|
self.private_messages = latest[1];
|
||||||
|
self.grouped_private_messages = grouped;
|
||||||
|
console.log('private took', (new Date() - start_time) / 1000.0);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
_schedule_load_latest_timer() {
|
||||||
|
--this.loading_latest_scheduled;
|
||||||
|
this.schedule_load_latest();
|
||||||
|
}
|
||||||
|
|
||||||
|
reset_progress() {
|
||||||
|
if (this.progress === undefined) {
|
||||||
|
this._progress_start = new Date();
|
||||||
|
requestAnimationFrame(this.update_progress.bind(this));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async load_recent_tags() {
|
update_progress() {
|
||||||
let start = new Date();
|
if (
|
||||||
this.tags = await tfrpc.rpc.query(
|
!this.loading_latest &&
|
||||||
|
!this.loading_latest_scheduled &&
|
||||||
|
!this.shadowRoot.getElementById('tf-tab-news')?.is_loading()
|
||||||
|
) {
|
||||||
|
this.progress = undefined;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.progress = (new Date() - this._progress_start).valueOf();
|
||||||
|
requestAnimationFrame(this.update_progress.bind(this));
|
||||||
|
}
|
||||||
|
|
||||||
|
schedule_load_latest() {
|
||||||
|
this.reset_progress();
|
||||||
|
if (!this.loading_latest) {
|
||||||
|
this.shadowRoot.getElementById('tf-tab-news')?.load_latest();
|
||||||
|
this.load();
|
||||||
|
} else if (!this.loading_latest_scheduled) {
|
||||||
|
this.loading_latest_scheduled++;
|
||||||
|
setTimeout(this._schedule_load_latest_timer.bind(this), 5000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fetch_user_info(users) {
|
||||||
|
let info = await tfrpc.rpc.query(
|
||||||
`
|
`
|
||||||
WITH
|
SELECT messages_stats.author, messages_stats.max_sequence, messages_stats.max_timestamp AS max_ts FROM messages_stats
|
||||||
recent AS (SELECT id, json(content) AS content FROM messages
|
JOIN json_each(?) AS following
|
||||||
WHERE messages.timestamp > ? AND json_extract(content, '$.type') = 'post'
|
ON messages_stats.author = following.value
|
||||||
ORDER BY timestamp DESC LIMIT 1024),
|
|
||||||
recent_channels AS (SELECT recent.id, '#' || json_extract(content, '$.channel') AS tag
|
|
||||||
FROM recent
|
|
||||||
WHERE json_extract(content, '$.channel') IS NOT NULL),
|
|
||||||
recent_mentions AS (SELECT recent.id, json_extract(mention.value, '$.link') AS tag
|
|
||||||
FROM recent, json_each(recent.content, '$.mentions') AS mention
|
|
||||||
WHERE json_valid(mention.value) AND tag LIKE '#%'),
|
|
||||||
combined AS (SELECT id, tag FROM recent_channels UNION ALL SELECT id, tag FROM recent_mentions),
|
|
||||||
by_message AS (SELECT DISTINCT id, tag FROM combined)
|
|
||||||
SELECT tag, COUNT(*) AS count FROM by_message GROUP BY tag ORDER BY count DESC LIMIT 10
|
|
||||||
`,
|
`,
|
||||||
[new Date() - 7 * 24 * 60 * 60 * 1000]
|
[JSON.stringify(Object.keys(users))]
|
||||||
);
|
);
|
||||||
console.log('tags took', (new Date() - start) / 1000.0, 'seconds');
|
for (let row of info) {
|
||||||
|
users[row.author] = Object.assign(users[row.author], {
|
||||||
|
seq: row.max_sequence,
|
||||||
|
ts: row.max_ts,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return users;
|
||||||
|
}
|
||||||
|
|
||||||
|
async load_recent_reactions() {
|
||||||
|
this.recent_reactions = (
|
||||||
|
await tfrpc.rpc.query(
|
||||||
|
`
|
||||||
|
SELECT DISTINCT content ->> '$.vote.expression' AS value
|
||||||
|
FROM messages
|
||||||
|
WHERE author = ? AND
|
||||||
|
content ->> '$.type' = 'vote'
|
||||||
|
ORDER BY timestamp DESC LIMIT 10
|
||||||
|
`,
|
||||||
|
[this.whoami]
|
||||||
|
)
|
||||||
|
).map((x) => x.value);
|
||||||
}
|
}
|
||||||
|
|
||||||
async load() {
|
async load() {
|
||||||
|
this.loading_latest = true;
|
||||||
|
this.reset_progress();
|
||||||
|
try {
|
||||||
|
let start_time = new Date();
|
||||||
let whoami = this.whoami;
|
let whoami = this.whoami;
|
||||||
let tags = this.load_recent_tags();
|
|
||||||
let following = await tfrpc.rpc.following([whoami], 2);
|
let following = await tfrpc.rpc.following([whoami], 2);
|
||||||
|
let old_users = this.users ?? {};
|
||||||
let users = {};
|
let users = {};
|
||||||
let by_count = [];
|
let by_count = [];
|
||||||
for (let [id, v] of Object.entries(following)) {
|
for (let [id, v] of Object.entries(following)) {
|
||||||
users[id] = {
|
users[id] = Object.assign(
|
||||||
|
{
|
||||||
following: v.of,
|
following: v.of,
|
||||||
blocking: v.ob,
|
blocking: v.ob,
|
||||||
followed: v.if,
|
followed: v.if,
|
||||||
blocked: v.ib,
|
blocked: v.ib,
|
||||||
};
|
follow_depth: following[id]?.d,
|
||||||
|
},
|
||||||
|
old_users[id]
|
||||||
|
);
|
||||||
by_count.push({count: v.of, id: id});
|
by_count.push({count: v.of, id: id});
|
||||||
}
|
}
|
||||||
console.log(by_count.sort((x, y) => y.count - x.count).slice(0, 20));
|
let reactions = this.load_recent_reactions();
|
||||||
let start_time = new Date();
|
this.load_channels_latest(Object.keys(following));
|
||||||
users = await this.fetch_about(Object.keys(following).sort(), users);
|
this.channels_unread = JSON.parse(
|
||||||
|
(await tfrpc.rpc.databaseGet('unread')) ?? '{}'
|
||||||
|
);
|
||||||
|
this.following = Object.keys(following);
|
||||||
|
let about_start_time = new Date();
|
||||||
|
start_time = new Date();
|
||||||
|
users = await this.fetch_user_info(users);
|
||||||
|
console.log(
|
||||||
|
'user info took',
|
||||||
|
(new Date() - start_time) / 1000.0,
|
||||||
|
'seconds'
|
||||||
|
);
|
||||||
|
this.users = users;
|
||||||
|
|
||||||
|
let self = this;
|
||||||
|
this.fetch_about(following, users).then(function (result) {
|
||||||
|
self.users = result;
|
||||||
console.log(
|
console.log(
|
||||||
'about took',
|
'about took',
|
||||||
(new Date() - start_time) / 1000.0,
|
(new Date() - about_start_time) / 1000.0,
|
||||||
'seconds for',
|
'seconds for',
|
||||||
Object.keys(users).length,
|
Object.keys(users).length,
|
||||||
'users'
|
'users'
|
||||||
);
|
);
|
||||||
this.following = Object.keys(following);
|
});
|
||||||
this.users = users;
|
console.log(
|
||||||
await tags;
|
`load finished ${whoami} => ${this.whoami} in ${(new Date() - start_time) / 1000}`
|
||||||
console.log(`load finished ${whoami} => ${this.whoami}`);
|
);
|
||||||
|
await reactions;
|
||||||
this.whoami = whoami;
|
this.whoami = whoami;
|
||||||
this.loaded = whoami;
|
this.loaded = whoami;
|
||||||
|
} finally {
|
||||||
|
this.loading_latest = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
channel_set_unread(event) {
|
||||||
|
this.channels_unread[event.detail.channel ?? ''] = event.detail.unread;
|
||||||
|
this.channels_unread = Object.assign({}, this.channels_unread);
|
||||||
|
tfrpc.rpc.databaseSet('unread', JSON.stringify(this.channels_unread));
|
||||||
|
}
|
||||||
|
|
||||||
|
async decrypt(messages) {
|
||||||
|
let whoami = this.whoami;
|
||||||
|
return Promise.all(
|
||||||
|
messages.map(async function (message) {
|
||||||
|
let content;
|
||||||
|
try {
|
||||||
|
content = JSON.parse(message?.content);
|
||||||
|
} catch {}
|
||||||
|
if (typeof content === 'string') {
|
||||||
|
let decrypted;
|
||||||
|
try {
|
||||||
|
decrypted = await tfrpc.rpc.try_decrypt(whoami, content);
|
||||||
|
} catch {}
|
||||||
|
if (decrypted) {
|
||||||
|
try {
|
||||||
|
message.decrypted = JSON.parse(decrypted);
|
||||||
|
} catch {
|
||||||
|
message.decrypted = decrypted;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return message;
|
||||||
|
})
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
render_tab() {
|
render_tab() {
|
||||||
@@ -262,9 +689,22 @@ class TfElement extends LitElement {
|
|||||||
whoami=${this.whoami}
|
whoami=${this.whoami}
|
||||||
.users=${this.users}
|
.users=${this.users}
|
||||||
hash=${this.hash}
|
hash=${this.hash}
|
||||||
.unread=${this.unread}
|
?loading=${this.loading || this.loading_about != 0}
|
||||||
@refresh=${() => (this.unread = [])}
|
.channels=${this.channels}
|
||||||
?loading=${this.loading}
|
.channels_latest=${this.channels_latest}
|
||||||
|
.channels_unread=${this.channels_unread}
|
||||||
|
@channelsetunread=${this.channel_set_unread}
|
||||||
|
@refresh=${this.refresh}
|
||||||
|
@toggle_stay_connected=${this.toggle_stay_connected}
|
||||||
|
@loadmessages=${this.reset_progress}
|
||||||
|
@closeprivatechat=${this.close_private_chat}
|
||||||
|
.connections=${this.connections}
|
||||||
|
.private_messages=${this.private_messages}
|
||||||
|
.visible_private_messages=${this.visible_private()}
|
||||||
|
.grouped_private_messages=${this.grouped_private_messages}
|
||||||
|
.recent_reactions=${this.recent_reactions}
|
||||||
|
?is_administrator=${this.is_administrator}
|
||||||
|
?stay_connected=${this.stay_connected}
|
||||||
></tf-tab-news>
|
></tf-tab-news>
|
||||||
`;
|
`;
|
||||||
} else if (this.tab === 'connections') {
|
} else if (this.tab === 'connections') {
|
||||||
@@ -275,14 +715,6 @@ class TfElement extends LitElement {
|
|||||||
.broadcasts=${this.broadcasts}
|
.broadcasts=${this.broadcasts}
|
||||||
></tf-tab-connections>
|
></tf-tab-connections>
|
||||||
`;
|
`;
|
||||||
} else if (this.tab === 'mentions') {
|
|
||||||
return html`
|
|
||||||
<tf-tab-mentions
|
|
||||||
.following=${this.following}
|
|
||||||
whoami=${this.whoami}
|
|
||||||
.users="${this.users}}"
|
|
||||||
></tf-tab-mentions>
|
|
||||||
`;
|
|
||||||
} else if (this.tab === 'search') {
|
} else if (this.tab === 'search') {
|
||||||
return html`
|
return html`
|
||||||
<tf-tab-search
|
<tf-tab-search
|
||||||
@@ -294,33 +726,46 @@ class TfElement extends LitElement {
|
|||||||
: null}
|
: null}
|
||||||
></tf-tab-search>
|
></tf-tab-search>
|
||||||
`;
|
`;
|
||||||
} else if (this.tab === 'query') {
|
|
||||||
return html`
|
|
||||||
<tf-tab-query
|
|
||||||
.following=${this.following}
|
|
||||||
whoami=${this.whoami}
|
|
||||||
.users=${this.users}
|
|
||||||
query=${this.hash?.startsWith('#sql=')
|
|
||||||
? decodeURIComponent(this.hash.substring(5))
|
|
||||||
: null}
|
|
||||||
></tf-tab-query>
|
|
||||||
`;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async set_tab(tab) {
|
async set_tab(tab) {
|
||||||
this.tab = tab;
|
this.tab = tab;
|
||||||
if (tab === 'news') {
|
if (tab === 'news') {
|
||||||
|
this.schedule_load_latest();
|
||||||
await tfrpc.rpc.setHash('#');
|
await tfrpc.rpc.setHash('#');
|
||||||
} else if (tab === 'connections') {
|
} else if (tab === 'connections') {
|
||||||
await tfrpc.rpc.setHash('#connections');
|
await tfrpc.rpc.setHash('#connections');
|
||||||
} else if (tab === 'mentions') {
|
|
||||||
await tfrpc.rpc.setHash('#mentions');
|
|
||||||
} else if (tab === 'query') {
|
|
||||||
await tfrpc.rpc.setHash('#sql=');
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
refresh() {
|
||||||
|
tfrpc.rpc.sync();
|
||||||
|
}
|
||||||
|
|
||||||
|
async toggle_stay_connected() {
|
||||||
|
let stay_connected = await tfrpc.rpc.globalSettingsGet('stay_connected');
|
||||||
|
let new_stay_connected = !this.stay_connected;
|
||||||
|
try {
|
||||||
|
if (new_stay_connected != stay_connected) {
|
||||||
|
await tfrpc.rpc.globalSettingsSet('stay_connected', new_stay_connected);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
this.stay_connected = await tfrpc.rpc.globalSettingsGet('stay_connected');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async pick_color() {
|
||||||
|
let input = document.createElement('input');
|
||||||
|
input.type = 'color';
|
||||||
|
input.value = (await tfrpc.rpc.localStorageGet('color')) ?? '#ff0000';
|
||||||
|
input.addEventListener('change', async function () {
|
||||||
|
await tfrpc.rpc.localStorageSet('color', input.value);
|
||||||
|
window.location.reload();
|
||||||
|
});
|
||||||
|
input.click();
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
let self = this;
|
let self = this;
|
||||||
|
|
||||||
@@ -334,47 +779,100 @@ class TfElement extends LitElement {
|
|||||||
const k_tabs = {
|
const k_tabs = {
|
||||||
'📰': 'news',
|
'📰': 'news',
|
||||||
'📡': 'connections',
|
'📡': 'connections',
|
||||||
'@': 'mentions',
|
|
||||||
'🔍': 'search',
|
'🔍': 'search',
|
||||||
'👩💻': 'query',
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let tabs = html`
|
let tabs = html`
|
||||||
<div class="w3-bar w3-theme-l1">
|
<div
|
||||||
|
class="w3-bar w3-theme-l1"
|
||||||
|
style="position: static; top: 0; z-index: 10"
|
||||||
|
>
|
||||||
|
${this.is_administrator
|
||||||
|
? html`
|
||||||
|
<button
|
||||||
|
class=${'w3-bar-item w3-button w3-circle w3-ripple' +
|
||||||
|
(this.connections?.some((x) => x.flags.one_shot)
|
||||||
|
? ' w3-spin'
|
||||||
|
: '')}
|
||||||
|
@click=${this.refresh}
|
||||||
|
>
|
||||||
|
↻
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="w3-bar-item w3-button w3-ripple"
|
||||||
|
@click=${this.toggle_stay_connected}
|
||||||
|
>
|
||||||
|
${this.stay_connected ? '🔗' : '⛓️💥'}
|
||||||
|
</button>
|
||||||
|
`
|
||||||
|
: undefined}
|
||||||
${Object.entries(k_tabs).map(
|
${Object.entries(k_tabs).map(
|
||||||
([k, v]) => html`
|
([k, v]) => html`
|
||||||
<button
|
<button
|
||||||
title=${v}
|
title=${v}
|
||||||
class="w3-bar-item w3-padding-large w3-hover-theme tab ${self.tab ==
|
class="w3-bar-item w3-padding w3-hover-theme tab ${self.tab == v
|
||||||
v
|
|
||||||
? 'w3-theme-l2'
|
? 'w3-theme-l2'
|
||||||
: 'w3-theme-l1'}"
|
: 'w3-theme-l1'}"
|
||||||
@click=${() => self.set_tab(v)}
|
@click=${() => self.set_tab(v)}
|
||||||
>
|
>
|
||||||
${k}
|
${k}
|
||||||
|
<span class=${self.tab == v ? '' : 'w3-hide-small'}
|
||||||
|
>${v.charAt(0).toUpperCase() + v.substring(1)}</span
|
||||||
|
>
|
||||||
</button>
|
</button>
|
||||||
`
|
`
|
||||||
)}
|
)}
|
||||||
|
<button
|
||||||
|
class="w3-bar-item w3-button w3-right"
|
||||||
|
@click=${this.pick_color}
|
||||||
|
>
|
||||||
|
🎨<span class="w3-hide-small">Color</span>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
let contents = !this.loaded
|
let contents = this.guest
|
||||||
? this.loading
|
? html`<div
|
||||||
? html`<div class="w3-panel w3-theme-l5 w3-card-4 w3-padding-large w3-round-xlarge">
|
class="w3-display-middle w3-panel w3-theme-l5 w3-card-4 w3-padding-large w3-round-xlarge w3-xlarge w3-container"
|
||||||
|
>
|
||||||
|
<p>⚠️🦀 Must be logged in to Tilde Friends to scuttle here. 🦀⚠️</p>
|
||||||
|
<footer class="w3-center">
|
||||||
|
<a
|
||||||
|
class="w3-button w3-theme-d1"
|
||||||
|
href=${`/login?return=${encodeURIComponent(this.url)}`}
|
||||||
|
>Login</a
|
||||||
|
>
|
||||||
|
</footer>
|
||||||
|
</div>`
|
||||||
|
: !this.loaded || this.loading
|
||||||
|
? html`<div
|
||||||
|
class="w3-display-middle w3-panel w3-theme-l5 w3-card-4 w3-padding-large w3-round-xlarge w3-xlarge"
|
||||||
|
>
|
||||||
|
<span class="w3-spin" style="display: inline-block">🦀</span>
|
||||||
Loading...
|
Loading...
|
||||||
</div>
|
</div>`
|
||||||
${this.render_tab()}`
|
|
||||||
: html`<div>Select or create an identity.</div>`
|
|
||||||
: this.render_tab();
|
: this.render_tab();
|
||||||
return html`
|
let progress =
|
||||||
|
this.progress !== undefined
|
||||||
|
? html`
|
||||||
|
<div style="position: absolute; width: 100%" id="progress">
|
||||||
<div
|
<div
|
||||||
style="width: 100vw; min-height: 100vh; height: 100%"
|
class="w3-theme-l3"
|
||||||
|
style=${`height: 4px; position: absolute; right: ${Math.cos(this.progress / 250) > 0 ? 'auto' : '0'}; width: ${50 * Math.sin(this.progress / 250) + 50}%`}
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
: undefined;
|
||||||
|
return html`
|
||||||
|
<style>
|
||||||
|
${generate_theme()}
|
||||||
|
</style>
|
||||||
|
<div
|
||||||
|
style="width: 100vw; min-height: 100vh; height: 100vh; display: flex; flex-direction: column"
|
||||||
class="w3-theme-dark"
|
class="w3-theme-dark"
|
||||||
>
|
>
|
||||||
${tabs}
|
${progress}
|
||||||
<div style="padding: 8px">
|
<div style="flex: 0 0">${tabs}</div>
|
||||||
${this.tags.map(
|
<div style="flex: 1 1; overflow: auto; contain: layout">
|
||||||
(x) => html`<tf-tag tag=${x.tag} count=${x.count}></tf-tag>`
|
|
||||||
)}
|
|
||||||
${contents}
|
${contents}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import {LitElement, html, unsafeHTML, live} from './lit-all.min.js';
|
import {LitElement, html, unsafeHTML, live} from './lit-all.min.js';
|
||||||
import * as tfutils from './tf-utils.js';
|
import * as tfutils from './tf-utils.js';
|
||||||
import * as tfrpc from '/static/tfrpc.js';
|
import * as tfrpc from '/static/tfrpc.js';
|
||||||
import {styles} from './tf-styles.js';
|
import {styles, generate_theme} from './tf-styles.js';
|
||||||
import Tribute from './tribute.esm.js';
|
import Tribute from './tribute.esm.js';
|
||||||
|
|
||||||
class TfComposeElement extends LitElement {
|
class TfComposeElement extends LitElement {
|
||||||
@@ -14,6 +14,9 @@ class TfComposeElement extends LitElement {
|
|||||||
apps: {type: Object},
|
apps: {type: Object},
|
||||||
drafts: {type: Object},
|
drafts: {type: Object},
|
||||||
author: {type: String},
|
author: {type: String},
|
||||||
|
channel: {type: String},
|
||||||
|
new_thread: {type: Boolean},
|
||||||
|
recipients: {type: Array},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -27,6 +30,7 @@ class TfComposeElement extends LitElement {
|
|||||||
this.apps = undefined;
|
this.apps = undefined;
|
||||||
this.drafts = {};
|
this.drafts = {};
|
||||||
this.author = undefined;
|
this.author = undefined;
|
||||||
|
this.new_thread = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
process_text(text) {
|
process_text(text) {
|
||||||
@@ -76,15 +80,9 @@ class TfComposeElement extends LitElement {
|
|||||||
let preview = this.renderRoot.getElementById('preview');
|
let preview = this.renderRoot.getElementById('preview');
|
||||||
preview.innerHTML = this.process_text(edit.innerText);
|
preview.innerHTML = this.process_text(edit.innerText);
|
||||||
let content_warning = this.renderRoot.getElementById('content_warning');
|
let content_warning = this.renderRoot.getElementById('content_warning');
|
||||||
let content_warning_preview = this.renderRoot.getElementById(
|
|
||||||
'content_warning_preview'
|
|
||||||
);
|
|
||||||
if (content_warning && content_warning_preview) {
|
|
||||||
content_warning_preview.innerText = content_warning.value;
|
|
||||||
}
|
|
||||||
let draft = this.get_draft();
|
let draft = this.get_draft();
|
||||||
draft.text = edit.innerText;
|
draft.text = edit.innerText;
|
||||||
draft.content_warning = content_warning?.innerText;
|
draft.content_warning = content_warning?.value;
|
||||||
setTimeout(() => this.notify(draft), 0);
|
setTimeout(() => this.notify(draft), 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -94,7 +92,9 @@ class TfComposeElement extends LitElement {
|
|||||||
bubbles: true,
|
bubbles: true,
|
||||||
composed: true,
|
composed: true,
|
||||||
detail: {
|
detail: {
|
||||||
id: this.branch,
|
id:
|
||||||
|
this.branch ??
|
||||||
|
(this.recipients ? this.recipients.join(',') : undefined),
|
||||||
draft: draft,
|
draft: draft,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
@@ -186,6 +186,13 @@ class TfComposeElement extends LitElement {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
event.preventDefault();
|
||||||
|
document.execCommand(
|
||||||
|
'insertText',
|
||||||
|
false,
|
||||||
|
event.clipboardData.getData('text/plain')
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async submit() {
|
async submit() {
|
||||||
@@ -195,11 +202,26 @@ class TfComposeElement extends LitElement {
|
|||||||
let message = {
|
let message = {
|
||||||
type: 'post',
|
type: 'post',
|
||||||
text: edit.innerText,
|
text: edit.innerText,
|
||||||
|
channel: this.channel,
|
||||||
};
|
};
|
||||||
if (this.root || this.branch) {
|
if (this.root || this.branch) {
|
||||||
message.root = this.root;
|
message.root = this.new_thread ? (this.branch ?? this.root) : this.root;
|
||||||
message.branch = this.branch;
|
message.branch = this.branch;
|
||||||
}
|
}
|
||||||
|
let reply = Object.fromEntries(
|
||||||
|
(
|
||||||
|
await tfrpc.rpc.query(
|
||||||
|
`
|
||||||
|
SELECT messages.id, messages.author FROM messages
|
||||||
|
JOIN json_each(?) AS refs ON messages.id = refs.value
|
||||||
|
`,
|
||||||
|
[JSON.stringify([this.root, this.branch])]
|
||||||
|
)
|
||||||
|
).map((row) => [row.id, row.author])
|
||||||
|
);
|
||||||
|
if (Object.keys(reply).length) {
|
||||||
|
message.reply = reply;
|
||||||
|
}
|
||||||
if (Object.values(draft.mentions || {}).length) {
|
if (Object.values(draft.mentions || {}).length) {
|
||||||
message.mentions = Object.values(draft.mentions);
|
message.mentions = Object.values(draft.mentions);
|
||||||
}
|
}
|
||||||
@@ -221,12 +243,8 @@ class TfComposeElement extends LitElement {
|
|||||||
console.log('encrypted as', message);
|
console.log('encrypted as', message);
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
await tfrpc.rpc.appendMessage(this.whoami, message).then(function () {
|
await tfrpc.rpc.appendMessage(this.whoami, message);
|
||||||
edit.innerText = '';
|
|
||||||
self.input();
|
|
||||||
self.notify(undefined);
|
self.notify(undefined);
|
||||||
self.requestUpdate();
|
|
||||||
});
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
alert(error.message);
|
alert(error.message);
|
||||||
}
|
}
|
||||||
@@ -240,10 +258,12 @@ class TfComposeElement extends LitElement {
|
|||||||
let self = this;
|
let self = this;
|
||||||
let input = document.createElement('input');
|
let input = document.createElement('input');
|
||||||
input.type = 'file';
|
input.type = 'file';
|
||||||
input.onchange = function (event) {
|
input.addEventListener('change', function (event) {
|
||||||
|
input.parentNode.removeChild(input);
|
||||||
let file = event.target.files[0];
|
let file = event.target.files[0];
|
||||||
self.add_file(file);
|
self.add_file(file);
|
||||||
};
|
});
|
||||||
|
document.body.appendChild(input);
|
||||||
input.click();
|
input.click();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -253,9 +273,9 @@ class TfComposeElement extends LitElement {
|
|||||||
try {
|
try {
|
||||||
let rows = await tfrpc.rpc.query(
|
let rows = await tfrpc.rpc.query(
|
||||||
`
|
`
|
||||||
SELECT json(messages.content) FROM messages_fts(?)
|
SELECT json(messages.content) AS content FROM messages_fts(?)
|
||||||
JOIN messages ON messages.rowid = messages_fts.rowid
|
JOIN messages ON messages.rowid = messages_fts.rowid
|
||||||
WHERE messages.content LIKE ?
|
WHERE json(messages.content) LIKE ?
|
||||||
ORDER BY timestamp DESC LIMIT 10
|
ORDER BY timestamp DESC LIMIT 10
|
||||||
`,
|
`,
|
||||||
['"' + text.replace('"', '""') + '"', `%%`]
|
['"' + text.replace('"', '""') + '"', `%%`]
|
||||||
@@ -274,7 +294,7 @@ class TfComposeElement extends LitElement {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
firstUpdated() {
|
get_values() {
|
||||||
let values = Object.entries(this.users).map((x) => ({
|
let values = Object.entries(this.users).map((x) => ({
|
||||||
key: x[1].name ?? x[0],
|
key: x[1].name ?? x[0],
|
||||||
value: x[0],
|
value: x[0],
|
||||||
@@ -290,24 +310,34 @@ class TfComposeElement extends LitElement {
|
|||||||
values
|
values
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
return values;
|
||||||
|
}
|
||||||
|
|
||||||
|
firstUpdated() {
|
||||||
let tribute = new Tribute({
|
let tribute = new Tribute({
|
||||||
|
iframe: this.shadowRoot,
|
||||||
collection: [
|
collection: [
|
||||||
{
|
{
|
||||||
values: values,
|
values: this.get_values(),
|
||||||
selectTemplate: function (item) {
|
selectTemplate: function (item) {
|
||||||
return item ? `[@${item.original.key}](${item.original.value})` : undefined;
|
return item
|
||||||
|
? `[@${item.original.key}](${item.original.value})`
|
||||||
|
: undefined;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
trigger: '&',
|
trigger: '&',
|
||||||
values: this.autocomplete,
|
values: this.autocomplete,
|
||||||
selectTemplate: function (item) {
|
selectTemplate: function (item) {
|
||||||
return item ? `` : undefined;
|
return item
|
||||||
|
? ``
|
||||||
|
: undefined;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
tribute.attach(this.renderRoot.getElementById('edit'));
|
tribute.attach(this.renderRoot.getElementById('edit'));
|
||||||
|
this._tribute = tribute;
|
||||||
}
|
}
|
||||||
|
|
||||||
updated() {
|
updated() {
|
||||||
@@ -318,9 +348,11 @@ class TfComposeElement extends LitElement {
|
|||||||
preview.innerHTML = this.process_text(edit.innerText);
|
preview.innerHTML = this.process_text(edit.innerText);
|
||||||
this.last_updated_text = edit.innerText;
|
this.last_updated_text = edit.innerText;
|
||||||
}
|
}
|
||||||
|
this._tribute.collection[0].values = this.get_values();
|
||||||
let encrypt = this.renderRoot.getElementById('encrypt_to');
|
let encrypt = this.renderRoot.getElementById('encrypt_to');
|
||||||
if (encrypt) {
|
if (encrypt) {
|
||||||
let tribute = new Tribute({
|
let tribute = new Tribute({
|
||||||
|
iframe: this.shadowRoot,
|
||||||
values: Object.entries(this.users).map((x) => ({
|
values: Object.entries(this.users).map((x) => ({
|
||||||
key: x[1].name,
|
key: x[1].name,
|
||||||
value: x[0],
|
value: x[0],
|
||||||
@@ -336,7 +368,7 @@ class TfComposeElement extends LitElement {
|
|||||||
remove_mention(id) {
|
remove_mention(id) {
|
||||||
let draft = this.get_draft();
|
let draft = this.get_draft();
|
||||||
delete draft.mentions[id];
|
delete draft.mentions[id];
|
||||||
setTimeout(() => this.notify(), 0);
|
setTimeout(() => this.notify(draft), 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
render_mention(mention) {
|
render_mention(mention) {
|
||||||
@@ -423,12 +455,15 @@ class TfComposeElement extends LitElement {
|
|||||||
self.apps = await tfrpc.rpc.apps();
|
self.apps = await tfrpc.rpc.apps();
|
||||||
}
|
}
|
||||||
if (!this.apps) {
|
if (!this.apps) {
|
||||||
return html`<button class="w3-button w3-theme-d1" @click=${attach_app}>
|
return html`<button
|
||||||
|
class="w3-button w3-bar-item w3-theme-d1"
|
||||||
|
@click=${attach_app}
|
||||||
|
>
|
||||||
Attach App
|
Attach App
|
||||||
</button>`;
|
</button>`;
|
||||||
} else {
|
} else {
|
||||||
return html`<button
|
return html`<button
|
||||||
class="w3-button w3-theme-d1"
|
class="w3-button w3-bar-item w3-theme-d1"
|
||||||
@click=${() => (this.apps = null)}
|
@click=${() => (this.apps = null)}
|
||||||
>
|
>
|
||||||
Discard App
|
Discard App
|
||||||
@@ -449,23 +484,38 @@ class TfComposeElement extends LitElement {
|
|||||||
if (draft.content_warning !== undefined) {
|
if (draft.content_warning !== undefined) {
|
||||||
return html`
|
return html`
|
||||||
<div class="w3-container w3-padding">
|
<div class="w3-container w3-padding">
|
||||||
<p>
|
<input type="text" class="w3-input w3-border w3-theme-d1" id="content_warning" placeholder="Enter a content warning here." @input=${self.input} value=${draft.content_warning}></input>
|
||||||
<input type="checkbox" class="w3-check w3-theme-d1" id="cw" @change=${() => self.set_content_warning(undefined)} checked="checked"></input>
|
|
||||||
<label for="cw">CW</label>
|
|
||||||
</p>
|
|
||||||
<input type="text" class="w3-input w3-border w3-theme-d1" id="content_warning" placeholder="Enter a content warning here." @input=${this.input} @change=${this.change} value=${draft.content_warning}></input>
|
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
} else {
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render_new_thread() {
|
||||||
|
let self = this;
|
||||||
|
if (
|
||||||
|
this.root !== undefined &&
|
||||||
|
this.branch !== undefined &&
|
||||||
|
this.root != this.branch
|
||||||
|
) {
|
||||||
return html`
|
return html`
|
||||||
<input type="checkbox" class="w3-check w3-theme-d1" id="cw" @change=${() => self.set_content_warning('')}></input>
|
<input type="checkbox" class="w3-check w3-theme-d1" id="new_thread" @change=${() => (self.new_thread = !self.new_thread)} ?checked=${self.new_thread}></input>
|
||||||
<label for="cw">CW</label>
|
<label for="new_thread">New Thread</label>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
get_draft() {
|
get_draft() {
|
||||||
return this.drafts[this.branch || ''] || {};
|
let key =
|
||||||
|
this.branch ||
|
||||||
|
(this.recipients ? this.recipients.join(',') : undefined) ||
|
||||||
|
'';
|
||||||
|
let draft = this.drafts[key] || {};
|
||||||
|
if (this.recipients && !draft.encrypt_to?.length) {
|
||||||
|
draft.encrypt_to = [
|
||||||
|
...new Set(this.recipients).union(new Set(draft.encrypt_to ?? [])),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
return draft;
|
||||||
}
|
}
|
||||||
|
|
||||||
update_encrypt(event) {
|
update_encrypt(event) {
|
||||||
@@ -487,7 +537,7 @@ class TfComposeElement extends LitElement {
|
|||||||
return html`
|
return html`
|
||||||
<div style="display: flex; flex-direction: row; width: 100%">
|
<div style="display: flex; flex-direction: row; width: 100%">
|
||||||
<label for="encrypt_to">🔐 To:</label>
|
<label for="encrypt_to">🔐 To:</label>
|
||||||
<input type="text" id="encrypt_to" style="display: flex; flex: 1 1" @input=${this.update_encrypt}></input>
|
<input type="text" id="encrypt_to" class="w3-input w3-theme-d1 w3-border" style="display: flex; flex: 1 1" @input=${this.update_encrypt}></input>
|
||||||
<button class="w3-button w3-theme-d1" @click=${() => this.set_encrypt(undefined)}>🚮</button>
|
<button class="w3-button w3-theme-d1" @click=${() => this.set_encrypt(undefined)}>🚮</button>
|
||||||
</div>
|
</div>
|
||||||
<ul>
|
<ul>
|
||||||
@@ -509,6 +559,31 @@ class TfComposeElement extends LitElement {
|
|||||||
this.requestUpdate();
|
this.requestUpdate();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
toggle_menu(event) {
|
||||||
|
event.srcElement.parentNode
|
||||||
|
.querySelector('.w3-dropdown-content')
|
||||||
|
.classList.toggle('w3-show');
|
||||||
|
}
|
||||||
|
|
||||||
|
connectedCallback() {
|
||||||
|
super.connectedCallback();
|
||||||
|
this._click_callback = this.document_click.bind(this);
|
||||||
|
document.body.addEventListener('mouseup', this._click_callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
disconnectedCallback() {
|
||||||
|
super.disconnectedCallback();
|
||||||
|
document.body.removeEventListener('mouseup', this._click_callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
document_click(event) {
|
||||||
|
let content = this.renderRoot.querySelector('.w3-dropdown-content');
|
||||||
|
let target = event.target;
|
||||||
|
if (content && !content.contains(target)) {
|
||||||
|
content.classList.remove('w3-show');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
let self = this;
|
let self = this;
|
||||||
let draft = self.get_draft();
|
let draft = self.get_draft();
|
||||||
@@ -522,50 +597,103 @@ class TfComposeElement extends LitElement {
|
|||||||
draft.encrypt_to !== undefined
|
draft.encrypt_to !== undefined
|
||||||
? undefined
|
? undefined
|
||||||
: html`<button
|
: html`<button
|
||||||
class="w3-button w3-theme-d1"
|
class="w3-button w3-bar-item w3-theme-d1"
|
||||||
@click=${() => this.set_encrypt([])}
|
@click=${() => this.set_encrypt([])}
|
||||||
>
|
>
|
||||||
🔐
|
🔐 Encrypt
|
||||||
</button>`;
|
</button>`;
|
||||||
let result = html`
|
let result = html`
|
||||||
|
<style>
|
||||||
|
${generate_theme()}
|
||||||
|
</style>
|
||||||
|
<style>
|
||||||
|
.w3-input:empty::before {
|
||||||
|
content: attr(placeholder);
|
||||||
|
}
|
||||||
|
.w3-input:empty:focus::before {
|
||||||
|
content: '';
|
||||||
|
}
|
||||||
|
</style>
|
||||||
<div
|
<div
|
||||||
class="w3-card-4 w3-theme-d4 w3-padding-small"
|
class="w3-card-4 w3-theme-d4 w3-padding w3-margin-top w3-margin-bottom"
|
||||||
style="box-sizing: border-box"
|
style="box-sizing: border-box"
|
||||||
>
|
>
|
||||||
|
<header class="w3-container">
|
||||||
|
${this.channel !== undefined
|
||||||
|
? html`<p>To #${this.channel}:</p>`
|
||||||
|
: undefined}
|
||||||
${this.render_encrypt()}
|
${this.render_encrypt()}
|
||||||
<div class="w3-container w3-padding-small">
|
</header>
|
||||||
|
<div class="w3-container" style="padding: 0 0 16px 0">
|
||||||
<div class="w3-half">
|
<div class="w3-half">
|
||||||
<span
|
<span
|
||||||
class="w3-input w3-theme-d1 w3-border"
|
class="w3-input w3-theme-d1 w3-border"
|
||||||
style="resize: vertical; width: 100%; overflow: hidden; white-space: pre-wrap"
|
style="resize: vertical; width: 100%; white-space: pre-wrap"
|
||||||
placeholder="Write a post here."
|
placeholder="Write a post here."
|
||||||
id="edit"
|
id="edit"
|
||||||
@input=${this.input}
|
@input=${this.input}
|
||||||
@paste=${this.paste}
|
@paste=${this.paste}
|
||||||
contenteditable
|
contenteditable="plaintext-only"
|
||||||
.innerText=${live(draft.text ?? '')}
|
.innerText=${live(draft.text ?? '')}
|
||||||
></span>
|
></span>
|
||||||
</div>
|
</div>
|
||||||
<div class="w3-half w3-padding">
|
<div class="w3-half w3-container">
|
||||||
${content_warning}
|
${content_warning}
|
||||||
<div id="preview"></div>
|
<p id="preview"></p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
${Object.values(draft.mentions || {}).map((x) =>
|
${Object.values(draft.mentions || {}).map((x) =>
|
||||||
self.render_mention(x)
|
self.render_mention(x)
|
||||||
)}
|
)}
|
||||||
|
<footer>
|
||||||
${this.render_attach_app()} ${this.render_content_warning()}
|
${this.render_attach_app()} ${this.render_content_warning()}
|
||||||
<button class="w3-button w3-theme-d1" id="submit" @click=${this.submit}>
|
${this.render_new_thread()}
|
||||||
|
<button
|
||||||
|
class="w3-button w3-theme-d1"
|
||||||
|
id="submit"
|
||||||
|
@click=${this.submit}
|
||||||
|
>
|
||||||
Submit
|
Submit
|
||||||
</button>
|
</button>
|
||||||
<button class="w3-button w3-theme-d1" @click=${this.attach}>
|
<div class="w3-dropdown-click">
|
||||||
|
<button class="w3-button w3-theme-d1" @click=${this.toggle_menu}>
|
||||||
|
⚙️
|
||||||
|
</button>
|
||||||
|
<div class="w3-dropdown-content w3-bar-block">
|
||||||
|
${this.get_draft().content_warning === undefined
|
||||||
|
? html`
|
||||||
|
<button
|
||||||
|
class="w3-button w3-bar-item w3-theme-d1"
|
||||||
|
@click=${() => self.set_content_warning('')}
|
||||||
|
>
|
||||||
|
Add Content Warning
|
||||||
|
</button>
|
||||||
|
`
|
||||||
|
: html`
|
||||||
|
<button
|
||||||
|
class="w3-button w3-bar-item w3-theme-d1"
|
||||||
|
@click=${() => self.set_content_warning(undefined)}
|
||||||
|
>
|
||||||
|
Remove Content Warning
|
||||||
|
</button>
|
||||||
|
`}
|
||||||
|
<button
|
||||||
|
class="w3-button w3-bar-item w3-theme-d1"
|
||||||
|
@click=${this.attach}
|
||||||
|
>
|
||||||
Attach
|
Attach
|
||||||
</button>
|
</button>
|
||||||
${this.render_attach_app_button()} ${encrypt}
|
${this.render_attach_app_button()} ${encrypt}
|
||||||
<button class="w3-button w3-theme-d1" @click=${this.discard}>
|
<button
|
||||||
|
class="w3-button w3-bar-item w3-theme-d1"
|
||||||
|
@click=${this.discard}
|
||||||
|
>
|
||||||
Discard
|
Discard
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
`;
|
`;
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
|||||||
import {LitElement, html, unsafeHTML, until} from './lit-all.min.js';
|
import {LitElement, html, unsafeHTML, repeat, until} from './lit-all.min.js';
|
||||||
import * as tfrpc from '/static/tfrpc.js';
|
import * as tfrpc from '/static/tfrpc.js';
|
||||||
import {styles} from './tf-styles.js';
|
import {styles, generate_theme} from './tf-styles.js';
|
||||||
|
|
||||||
class TfNewsElement extends LitElement {
|
class TfNewsElement extends LitElement {
|
||||||
static get properties() {
|
static get properties() {
|
||||||
@@ -11,6 +11,10 @@ class TfNewsElement extends LitElement {
|
|||||||
following: {type: Array},
|
following: {type: Array},
|
||||||
drafts: {type: Object},
|
drafts: {type: Object},
|
||||||
expanded: {type: Object},
|
expanded: {type: Object},
|
||||||
|
channel: {type: String},
|
||||||
|
channel_unread: {type: Number},
|
||||||
|
recent_reactions: {type: Array},
|
||||||
|
hash: {type: String},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -25,6 +29,8 @@ class TfNewsElement extends LitElement {
|
|||||||
this.following = [];
|
this.following = [];
|
||||||
this.drafts = {};
|
this.drafts = {};
|
||||||
this.expanded = {};
|
this.expanded = {};
|
||||||
|
this.channel_unread = -1;
|
||||||
|
this.recent_reactions = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
process_messages(messages) {
|
process_messages(messages) {
|
||||||
@@ -33,12 +39,13 @@ class TfNewsElement extends LitElement {
|
|||||||
|
|
||||||
console.log('processing', messages.length, 'messages');
|
console.log('processing', messages.length, 'messages');
|
||||||
|
|
||||||
function ensure_message(id) {
|
function ensure_message(id, rowid) {
|
||||||
let found = messages_by_id[id];
|
let found = messages_by_id[id];
|
||||||
if (found) {
|
if (found) {
|
||||||
return found;
|
return found;
|
||||||
} else {
|
} else {
|
||||||
let added = {
|
let added = {
|
||||||
|
rowid: rowid,
|
||||||
id: id,
|
id: id,
|
||||||
placeholder: true,
|
placeholder: true,
|
||||||
content: '"placeholder"',
|
content: '"placeholder"',
|
||||||
@@ -53,7 +60,7 @@ class TfNewsElement extends LitElement {
|
|||||||
|
|
||||||
function link_message(message) {
|
function link_message(message) {
|
||||||
if (message.content.type === 'vote') {
|
if (message.content.type === 'vote') {
|
||||||
let parent = ensure_message(message.content.vote.link);
|
let parent = ensure_message(message.content.vote.link, message.rowid);
|
||||||
if (!parent.votes) {
|
if (!parent.votes) {
|
||||||
parent.votes = [];
|
parent.votes = [];
|
||||||
}
|
}
|
||||||
@@ -62,14 +69,14 @@ class TfNewsElement extends LitElement {
|
|||||||
} else if (message.content.type == 'post') {
|
} else if (message.content.type == 'post') {
|
||||||
if (message.content.root) {
|
if (message.content.root) {
|
||||||
if (typeof message.content.root === 'string') {
|
if (typeof message.content.root === 'string') {
|
||||||
let m = ensure_message(message.content.root);
|
let m = ensure_message(message.content.root, message.rowid);
|
||||||
if (!m.child_messages) {
|
if (!m.child_messages) {
|
||||||
m.child_messages = [];
|
m.child_messages = [];
|
||||||
}
|
}
|
||||||
m.child_messages.push(message);
|
m.child_messages.push(message);
|
||||||
message.parent_message = message.content.root;
|
message.parent_message = message.content.root;
|
||||||
} else {
|
} else {
|
||||||
let m = ensure_message(message.content.root[0]);
|
let m = ensure_message(message.content.root[0], message.rowid);
|
||||||
if (!m.child_messages) {
|
if (!m.child_messages) {
|
||||||
m.child_messages = [];
|
m.child_messages = [];
|
||||||
}
|
}
|
||||||
@@ -153,43 +160,120 @@ class TfNewsElement extends LitElement {
|
|||||||
return recursive_sort(roots, true);
|
return recursive_sort(roots, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
group_following(messages) {
|
group_messages(messages) {
|
||||||
let result = [];
|
let result = [];
|
||||||
let group = [];
|
let group = [];
|
||||||
|
let type = undefined;
|
||||||
for (let message of messages) {
|
for (let message of messages) {
|
||||||
if (message?.content?.type === 'contact') {
|
if (
|
||||||
|
message?.content?.type === 'contact' ||
|
||||||
|
message?.content?.type === 'channel'
|
||||||
|
) {
|
||||||
|
if (type && message.content.type !== type) {
|
||||||
|
if (group.length == 1) {
|
||||||
|
result.push(group[0]);
|
||||||
|
group = [];
|
||||||
|
} else if (group.length > 1) {
|
||||||
|
result.push({
|
||||||
|
rowid: Math.max(...group.map((x) => x.rowid)),
|
||||||
|
type: `${type}_group`,
|
||||||
|
messages: group,
|
||||||
|
});
|
||||||
|
group = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
type = message.content.type;
|
||||||
group.push(message);
|
group.push(message);
|
||||||
} else {
|
} else {
|
||||||
if (group.length > 0) {
|
if (group.length == 1) {
|
||||||
|
result.push(group[0]);
|
||||||
|
group = [];
|
||||||
|
} else if (group.length > 1) {
|
||||||
result.push({
|
result.push({
|
||||||
type: 'contact_group',
|
rowid: Math.max(...group.map((x) => x.rowid)),
|
||||||
|
type: `${type}_group`,
|
||||||
messages: group,
|
messages: group,
|
||||||
});
|
});
|
||||||
group = [];
|
group = [];
|
||||||
}
|
}
|
||||||
result.push(message);
|
result.push(message);
|
||||||
|
type = undefined;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (group.length == 1) {
|
||||||
|
result.push(group[0]);
|
||||||
|
group = [];
|
||||||
|
} else if (group.length > 1) {
|
||||||
|
result.push({
|
||||||
|
rowid: Math.max(...group.map((x) => x.rowid)),
|
||||||
|
type: `${type}_group`,
|
||||||
|
messages: group,
|
||||||
|
});
|
||||||
|
}
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
unread_allowed() {
|
||||||
|
return !this.hash?.startsWith('#%') && !this.hash?.startsWith('#@');
|
||||||
|
}
|
||||||
|
|
||||||
load_and_render(messages) {
|
load_and_render(messages) {
|
||||||
let messages_by_id = this.process_messages(messages);
|
let messages_by_id = this.process_messages(messages);
|
||||||
let final_messages = this.group_following(
|
let final_messages = this.group_messages(
|
||||||
this.finalize_messages(messages_by_id)
|
this.finalize_messages(messages_by_id)
|
||||||
);
|
);
|
||||||
|
let unread_rowid = -1;
|
||||||
|
if (this.unread_allowed()) {
|
||||||
|
for (let message of final_messages) {
|
||||||
|
if (message.rowid >= this.channel_unread) {
|
||||||
|
unread_rowid = message.rowid;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
return html`
|
return html`
|
||||||
<div style="display: flex; flex-direction: column">
|
<style>
|
||||||
${final_messages.map(
|
${generate_theme()}
|
||||||
(x) =>
|
</style>
|
||||||
html`<tf-message
|
<div>
|
||||||
|
${repeat(
|
||||||
|
final_messages,
|
||||||
|
(x) => x.id,
|
||||||
|
(x) => html`
|
||||||
|
<tf-message
|
||||||
.message=${x}
|
.message=${x}
|
||||||
whoami=${this.whoami}
|
whoami=${this.whoami}
|
||||||
.users=${this.users}
|
.users=${this.users}
|
||||||
.drafts=${this.drafts}
|
.drafts=${this.drafts}
|
||||||
.expanded=${this.expanded}
|
.expanded=${this.expanded}
|
||||||
collapsed="true"
|
collapsed="true"
|
||||||
></tf-message>`
|
channel=${this.channel}
|
||||||
|
channel_unread=${this.channel_unread}
|
||||||
|
.recent_reactions=${this.recent_reactions}
|
||||||
|
></tf-message>
|
||||||
|
${x.rowid == unread_rowid
|
||||||
|
? html`<div style="display: flex; flex-direction: row">
|
||||||
|
<div
|
||||||
|
style="border-bottom: 1px solid #f00; flex: 1; align-self: center; height: 1px"
|
||||||
|
></div>
|
||||||
|
<button
|
||||||
|
style="color: #f00; padding: 8px"
|
||||||
|
class="w3-button"
|
||||||
|
@click=${() =>
|
||||||
|
this.dispatchEvent(
|
||||||
|
new Event('mark_all_read', {
|
||||||
|
bubbles: true,
|
||||||
|
composed: true,
|
||||||
|
})
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
unread
|
||||||
|
</button>
|
||||||
|
<div
|
||||||
|
style="border-bottom: 1px solid #f00; flex: 1; align-self: center; height: 1px"
|
||||||
|
></div>
|
||||||
|
</div>`
|
||||||
|
: undefined}
|
||||||
|
`
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import {LitElement, html, unsafeHTML} from './lit-all.min.js';
|
import {LitElement, html, until, unsafeHTML} from './lit-all.min.js';
|
||||||
import * as tfrpc from '/static/tfrpc.js';
|
import * as tfrpc from '/static/tfrpc.js';
|
||||||
import * as tfutils from './tf-utils.js';
|
import * as tfutils from './tf-utils.js';
|
||||||
import {styles} from './tf-styles.js';
|
import {styles, generate_theme} from './tf-styles.js';
|
||||||
|
|
||||||
class TfProfileElement extends LitElement {
|
class TfProfileElement extends LitElement {
|
||||||
static get properties() {
|
static get properties() {
|
||||||
@@ -11,9 +11,10 @@ class TfProfileElement extends LitElement {
|
|||||||
id: {type: String},
|
id: {type: String},
|
||||||
users: {type: Object},
|
users: {type: Object},
|
||||||
size: {type: Number},
|
size: {type: Number},
|
||||||
server_follows_me: {type: Boolean},
|
sequence: {type: Number},
|
||||||
following: {type: Boolean},
|
following: {type: Boolean},
|
||||||
blocking: {type: Boolean},
|
blocking: {type: Boolean},
|
||||||
|
show_followed: {type: Boolean},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -27,7 +28,7 @@ class TfProfileElement extends LitElement {
|
|||||||
this.id = null;
|
this.id = null;
|
||||||
this.users = {};
|
this.users = {};
|
||||||
this.size = 0;
|
this.size = 0;
|
||||||
this.server_follows_me = undefined;
|
this.sequence = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
async load() {
|
async load() {
|
||||||
@@ -36,16 +37,22 @@ class TfProfileElement extends LitElement {
|
|||||||
this.following = undefined;
|
this.following = undefined;
|
||||||
this.blocking = undefined;
|
this.blocking = undefined;
|
||||||
|
|
||||||
|
let latest = (
|
||||||
|
await tfrpc.rpc.query('SELECT MAX(rowid) AS latest FROM messages')
|
||||||
|
)[0].latest;
|
||||||
|
|
||||||
let result = await tfrpc.rpc.query(
|
let result = await tfrpc.rpc.query(
|
||||||
`
|
`
|
||||||
SELECT json_extract(content, '$.following') AS following
|
SELECT json_extract(content, '$.following') AS following
|
||||||
FROM messages WHERE author = ? AND
|
FROM messages WHERE author = ? AND
|
||||||
json_extract(content, '$.type') = 'contact' AND
|
json_extract(content, '$.type') = 'contact' AND
|
||||||
json_extract(content, '$.contact') = ? AND
|
json_extract(content, '$.contact') = ? AND
|
||||||
following IS NOT NULL
|
following IS NOT NULL AND
|
||||||
|
messages.rowid <= ?
|
||||||
ORDER BY sequence DESC LIMIT 1
|
ORDER BY sequence DESC LIMIT 1
|
||||||
`,
|
`,
|
||||||
[this.whoami, this.id]
|
[this.whoami, this.id, latest],
|
||||||
|
{cacheable: true}
|
||||||
);
|
);
|
||||||
this.following = result?.[0]?.following ?? false;
|
this.following = result?.[0]?.following ?? false;
|
||||||
result = await tfrpc.rpc.query(
|
result = await tfrpc.rpc.query(
|
||||||
@@ -54,36 +61,19 @@ class TfProfileElement extends LitElement {
|
|||||||
FROM messages WHERE author = ? AND
|
FROM messages WHERE author = ? AND
|
||||||
json_extract(content, '$.type') = 'contact' AND
|
json_extract(content, '$.type') = 'contact' AND
|
||||||
json_extract(content, '$.contact') = ? AND
|
json_extract(content, '$.contact') = ? AND
|
||||||
blocking IS NOT NULL
|
blocking IS NOT NULL AND
|
||||||
|
messages.rowid <= ?
|
||||||
ORDER BY sequence DESC LIMIT 1
|
ORDER BY sequence DESC LIMIT 1
|
||||||
`,
|
`,
|
||||||
[this.whoami, this.id]
|
[this.whoami, this.id, latest],
|
||||||
|
{cacheable: true}
|
||||||
);
|
);
|
||||||
this.blocking = result?.[0]?.blocking ?? false;
|
this.blocking = result?.[0]?.blocking ?? false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async initial_load() {
|
|
||||||
this.server_follows_me = undefined;
|
|
||||||
let server_id = await tfrpc.rpc.getServerIdentity();
|
|
||||||
let followed = await tfrpc.rpc.query(
|
|
||||||
`
|
|
||||||
SELECT json_extract(content, '$.following') AS following
|
|
||||||
FROM messages
|
|
||||||
WHERE author = ? AND
|
|
||||||
json_extract(content, '$.type') = 'contact' AND
|
|
||||||
json_extract(content, '$.contact') = ? ORDER BY sequence DESC LIMIT 1
|
|
||||||
`,
|
|
||||||
[server_id, this.whoami]
|
|
||||||
);
|
|
||||||
let is_followed = false;
|
|
||||||
for (let row of followed) {
|
|
||||||
is_followed = row.following != 0;
|
|
||||||
}
|
|
||||||
this.server_follows_me = is_followed;
|
|
||||||
}
|
|
||||||
|
|
||||||
modify(change) {
|
modify(change) {
|
||||||
|
let self = this;
|
||||||
tfrpc.rpc
|
tfrpc.rpc
|
||||||
.appendMessage(
|
.appendMessage(
|
||||||
this.whoami,
|
this.whoami,
|
||||||
@@ -95,6 +85,10 @@ class TfProfileElement extends LitElement {
|
|||||||
change
|
change
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
.then(function () {
|
||||||
|
self._follow_whoami = undefined;
|
||||||
|
self.load();
|
||||||
|
})
|
||||||
.catch(function (error) {
|
.catch(function (error) {
|
||||||
alert(error?.message);
|
alert(error?.message);
|
||||||
});
|
});
|
||||||
@@ -156,7 +150,8 @@ class TfProfileElement extends LitElement {
|
|||||||
let self = this;
|
let self = this;
|
||||||
let input = document.createElement('input');
|
let input = document.createElement('input');
|
||||||
input.type = 'file';
|
input.type = 'file';
|
||||||
input.onchange = function (event) {
|
input.addEventListener('change', function (event) {
|
||||||
|
input.parentNode.removeChild(input);
|
||||||
let file = event.target.files[0];
|
let file = event.target.files[0];
|
||||||
file
|
file
|
||||||
.arrayBuffer()
|
.arrayBuffer()
|
||||||
@@ -171,74 +166,116 @@ class TfProfileElement extends LitElement {
|
|||||||
.catch(function (e) {
|
.catch(function (e) {
|
||||||
alert(e.message);
|
alert(e.message);
|
||||||
});
|
});
|
||||||
};
|
});
|
||||||
|
document.body.appendChild(input);
|
||||||
input.click();
|
input.click();
|
||||||
}
|
}
|
||||||
|
|
||||||
async server_follow_me(follow) {
|
copy_id() {
|
||||||
try {
|
navigator.clipboard.writeText(this.id);
|
||||||
await tfrpc.rpc.setServerFollowingMe(this.whoami, follow);
|
|
||||||
} catch (e) {
|
|
||||||
console.log(e);
|
|
||||||
}
|
}
|
||||||
try {
|
|
||||||
await this.initial_load();
|
show_image(link) {
|
||||||
} catch (e) {
|
let div = document.createElement('div');
|
||||||
console.log(e);
|
div.style.left = 0;
|
||||||
|
div.style.top = 0;
|
||||||
|
div.style.width = '100%';
|
||||||
|
div.style.height = '100%';
|
||||||
|
div.style.position = 'fixed';
|
||||||
|
div.style.background = '#000';
|
||||||
|
div.style.zIndex = 100;
|
||||||
|
div.style.display = 'grid';
|
||||||
|
let img = document.createElement('img');
|
||||||
|
img.src = link;
|
||||||
|
img.style.maxWidth = '100vw';
|
||||||
|
img.style.maxHeight = '100vh';
|
||||||
|
img.style.display = 'block';
|
||||||
|
img.style.margin = 'auto';
|
||||||
|
img.style.objectFit = 'contain';
|
||||||
|
img.style.width = '100vw';
|
||||||
|
div.appendChild(img);
|
||||||
|
function image_close(event) {
|
||||||
|
document.body.removeChild(div);
|
||||||
|
window.removeEventListener('keydown', image_close);
|
||||||
|
}
|
||||||
|
div.onclick = image_close;
|
||||||
|
window.addEventListener('keydown', image_close);
|
||||||
|
document.body.appendChild(div);
|
||||||
|
}
|
||||||
|
|
||||||
|
body_click(event) {
|
||||||
|
if (event.srcElement.tagName == 'IMG') {
|
||||||
|
this.show_image(event.srcElement.src);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
toggle_account_list(event) {
|
||||||
if (
|
let content = event.srcElement.nextElementSibling;
|
||||||
this.id == this.whoami &&
|
this.show_followed = !this.show_followed;
|
||||||
this.editing &&
|
|
||||||
this.server_follows_me === undefined
|
|
||||||
) {
|
|
||||||
this.initial_load();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async load_follows() {
|
||||||
|
let accounts = await tfrpc.rpc.following([this.id], 1);
|
||||||
|
return html`
|
||||||
|
<div class="w3-container">
|
||||||
|
<button
|
||||||
|
class="w3-button w3-block w3-theme-d1 followed_accounts"
|
||||||
|
@click=${this.toggle_account_list}
|
||||||
|
>
|
||||||
|
${this.show_followed ? 'Hide' : 'Show'} Followed Accounts
|
||||||
|
(${Object.keys(accounts).length})
|
||||||
|
</button>
|
||||||
|
<div class=${'w3-card' + (this.show_followed ? '' : ' w3-hide')}>
|
||||||
|
<ul class="w3-ul w3-theme-d4 w3-border-theme">
|
||||||
|
${Object.keys(accounts).map(
|
||||||
|
(x) => html`
|
||||||
|
<li class="w3-border-theme">
|
||||||
|
<tf-user id=${x} .users=${this.users}></tf-user>
|
||||||
|
</li>
|
||||||
|
`
|
||||||
|
)}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
this.load();
|
this.load();
|
||||||
let self = this;
|
let self = this;
|
||||||
let profile = this.users[this.id] || {};
|
let profile = this.users[this.id] || {};
|
||||||
tfrpc.rpc
|
tfrpc.rpc
|
||||||
.query(
|
.query(
|
||||||
`SELECT SUM(LENGTH(content)) AS size FROM messages WHERE author = ?`,
|
`SELECT size AS size, max_sequence AS sequence FROM messages_stats WHERE author = ?`,
|
||||||
[this.id]
|
[this.id]
|
||||||
)
|
)
|
||||||
.then(function (result) {
|
.then(function (result) {
|
||||||
self.size = result[0].size;
|
self.size = result[0].size;
|
||||||
|
self.sequence = result[0].sequence;
|
||||||
});
|
});
|
||||||
let edit;
|
let edit;
|
||||||
let follow;
|
let follow;
|
||||||
let block;
|
let block;
|
||||||
if (this.id === this.whoami) {
|
if (this.id === this.whoami) {
|
||||||
if (this.editing) {
|
if (this.editing) {
|
||||||
let server_follow;
|
|
||||||
if (this.server_follows_me === true) {
|
|
||||||
server_follow = html`<button
|
|
||||||
class="w3-button w3-theme-d1"
|
|
||||||
@click=${() => this.server_follow_me(false)}
|
|
||||||
>
|
|
||||||
Server, Stop Following Me
|
|
||||||
</button>`;
|
|
||||||
} else if (this.server_follows_me === false) {
|
|
||||||
server_follow = html`<button
|
|
||||||
class="w3-button w3-theme-d1"
|
|
||||||
@click=${() => this.server_follow_me(true)}
|
|
||||||
>
|
|
||||||
Server, Follow Me
|
|
||||||
</button>`;
|
|
||||||
}
|
|
||||||
edit = html`
|
edit = html`
|
||||||
<button class="w3-button w3-theme-d1" @click=${this.save_edits}>
|
<button
|
||||||
|
id="save_profile"
|
||||||
|
class="w3-button w3-theme-d1"
|
||||||
|
@click=${this.save_edits}
|
||||||
|
>
|
||||||
Save Profile
|
Save Profile
|
||||||
</button>
|
</button>
|
||||||
<button class="w3-button w3-theme-d1" @click=${this.discard_edits}>
|
<button class="w3-button w3-theme-d1" @click=${this.discard_edits}>
|
||||||
Discard
|
Discard
|
||||||
</button>
|
</button>
|
||||||
${server_follow}
|
|
||||||
`;
|
`;
|
||||||
} else {
|
} else {
|
||||||
edit = html`<button class="w3-button w3-theme-d1" @click=${this.edit}>
|
edit = html`<button
|
||||||
|
id="edit_profile"
|
||||||
|
class="w3-button w3-theme-d1"
|
||||||
|
@click=${this.edit}
|
||||||
|
>
|
||||||
Edit Profile
|
Edit Profile
|
||||||
</button>`;
|
</button>`;
|
||||||
}
|
}
|
||||||
@@ -264,13 +301,12 @@ class TfProfileElement extends LitElement {
|
|||||||
let edit_profile = this.editing
|
let edit_profile = this.editing
|
||||||
? html`
|
? html`
|
||||||
<div style="flex: 1 0 50%; display: flex; flex-direction: column; gap: 8px">
|
<div style="flex: 1 0 50%; display: flex; flex-direction: column; gap: 8px">
|
||||||
<div class="w3-container">
|
|
||||||
<div>
|
<div>
|
||||||
<label for="name">Name:</label>
|
<label for="name">Name:</label>
|
||||||
<input class="w3-input w3-theme-d1" type="text" id="name" value=${this.editing.name} @input=${(event) => (this.editing = Object.assign({}, this.editing, {name: event.srcElement.value}))}></input>
|
<input class="w3-input w3-theme-d1" type="text" id="name" value=${this.editing.name} @input=${(event) => (this.editing = Object.assign({}, this.editing, {name: event.srcElement.value}))} placeholder="Choose a name"></input>
|
||||||
</div>
|
</div>
|
||||||
<div><label for="description">Description:</label></div>
|
<div><label for="description">Description:</label></div>
|
||||||
<textarea class="w3-input w3-theme-d1" style="resize: vertical" rows="8" id="description" @input=${(event) => (this.editing = Object.assign({}, this.editing, {description: event.srcElement.value}))}>${this.editing.description}</textarea>
|
<textarea class="w3-input w3-theme-d1" style="resize: vertical" rows="8" id="description" @input=${(event) => (this.editing = Object.assign({}, this.editing, {description: event.srcElement.value}))} placeholder="Tell people a little bit about yourself here, if you like.">${this.editing.description}</textarea>
|
||||||
<div>
|
<div>
|
||||||
<label for="public_web_hosting">Public Web Hosting:</label>
|
<label for="public_web_hosting">Public Web Hosting:</label>
|
||||||
<input class="w3-check w3-theme-d1" type="checkbox" id="public_web_hosting" ?checked=${this.editing.publicWebHosting} @input=${(event) => (self.editing = Object.assign({}, self.editing, {publicWebHosting: event.srcElement.checked}))}></input>
|
<input class="w3-check w3-theme-d1" type="checkbox" id="public_web_hosting" ?checked=${this.editing.publicWebHosting} @input=${(event) => (self.editing = Object.assign({}, self.editing, {publicWebHosting: event.srcElement.checked}))}></input>
|
||||||
@@ -278,19 +314,38 @@ class TfProfileElement extends LitElement {
|
|||||||
<div>
|
<div>
|
||||||
<button class="w3-button w3-theme-d1" @click=${this.attach_image}>Attach Image</button>
|
<button class="w3-button w3-theme-d1" @click=${this.attach_image}>Attach Image</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</div>`
|
</div>`
|
||||||
: null;
|
: null;
|
||||||
let image =
|
let image = profile.image;
|
||||||
typeof profile.image == 'string' ? profile.image : profile.image?.link;
|
if (typeof image == 'string' && !image.startsWith('&')) {
|
||||||
|
try {
|
||||||
|
image = JSON.parse(image)?.link;
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
image = this.editing?.image ?? image;
|
image = this.editing?.image ?? image;
|
||||||
let description = this.editing?.description ?? profile.description;
|
let description = this.editing?.description ?? profile.description;
|
||||||
return html`<div style="border: 2px solid black; background-color: rgba(255, 255, 255, 0.2); padding: 16px">
|
return html`
|
||||||
<tf-user id=${this.id} .users=${this.users}></tf-user> (${tfutils.human_readable_size(this.size)})
|
<style>${generate_theme()}</style>
|
||||||
|
<div class="w3-card-4 w3-container w3-theme-d3" style="box-sizing: border-box">
|
||||||
|
<header class="w3-container">
|
||||||
|
<p><tf-user id=${this.id} .users=${this.users}></tf-user> (${tfutils.human_readable_size(this.size)} in ${this.sequence} messages)</p>
|
||||||
|
</header>
|
||||||
|
<div class="w3-container" @click=${this.body_click}>
|
||||||
|
<div class="w3-margin-bottom" style="display: flex; flex-direction: row">
|
||||||
|
<input type="text" class="w3-input w3-border w3-theme-d1" style="display: flex 1 1" readonly value=${this.id}></input>
|
||||||
|
<button class="w3-button w3-theme-d1 w3-ripple" style="flex: 0 0 auto" @click=${this.copy_id}>Copy</button>
|
||||||
|
</div>
|
||||||
<div style="display: flex; flex-direction: row; gap: 1em">
|
<div style="display: flex; flex-direction: row; gap: 1em">
|
||||||
${edit_profile}
|
${edit_profile}
|
||||||
<div style="flex: 1 0 50%">
|
<div style="flex: 1 0 50%; contain: layout; overflow: auto; word-wrap: normal; word-break: normal">
|
||||||
<div><img src=${'/' + image + '/view'} style="width: 256px; height: auto"></img></div>
|
${
|
||||||
|
image
|
||||||
|
? html`<div><img src=${'/' + image + '/view'} style="width: 256px; height: auto"></img></div>`
|
||||||
|
: html`<div>
|
||||||
|
<div class="w3-jumbo">😎</div>
|
||||||
|
<div><i>Profile image not set.</i></div>
|
||||||
|
</div>`
|
||||||
|
}
|
||||||
<div>${unsafeHTML(tfutils.markdown(description))}</div>
|
<div>${unsafeHTML(tfutils.markdown(description))}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -300,11 +355,18 @@ class TfProfileElement extends LitElement {
|
|||||||
Blocking ${profile.blocking} identities.
|
Blocking ${profile.blocking} identities.
|
||||||
Blocked by ${profile.blocked} identities.
|
Blocked by ${profile.blocked} identities.
|
||||||
</div>
|
</div>
|
||||||
<div>
|
</div>
|
||||||
|
${until(this.load_follows(), html`<p>Loading accounts followed...</p>`)}
|
||||||
|
<footer class="w3-container">
|
||||||
|
<p>
|
||||||
|
<a class="w3-button w3-theme-d1" href=${'#🔐' + (this.id != this.whoami ? this.id : '')}>
|
||||||
|
Open Private Chat
|
||||||
|
</a>
|
||||||
${edit}
|
${edit}
|
||||||
${follow}
|
${follow}
|
||||||
${block}
|
${block}
|
||||||
</div>
|
</p>
|
||||||
|
</footer>
|
||||||
</div>`;
|
</div>`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import {LitElement, html, unsafeHTML} from './lit-all.min.js';
|
import {LitElement, html, unsafeHTML} from './lit-all.min.js';
|
||||||
import {styles} from './tf-styles.js';
|
import {styles, generate_theme} from './tf-styles.js';
|
||||||
|
|
||||||
class TfReactionsModalElement extends LitElement {
|
class TfReactionsModalElement extends LitElement {
|
||||||
static get properties() {
|
static get properties() {
|
||||||
@@ -24,31 +24,45 @@ class TfReactionsModalElement extends LitElement {
|
|||||||
render() {
|
render() {
|
||||||
let self = this;
|
let self = this;
|
||||||
return this.votes?.length
|
return this.votes?.length
|
||||||
? html` <div
|
? html` <style>
|
||||||
|
${generate_theme()}
|
||||||
|
</style>
|
||||||
|
<div
|
||||||
class="w3-modal w3-animate-opacity"
|
class="w3-modal w3-animate-opacity"
|
||||||
style="display: block; box-sizing: border-box"
|
style="display: block; box-sizing: border-box; z-index: 10"
|
||||||
|
@click=${this.clear}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="w3-modal-content w3-card-4 w3-theme-d1"
|
||||||
|
onclick="event.stopPropagation()"
|
||||||
>
|
>
|
||||||
<div class="w3-modal-content w3-card-4 w3-theme-d1">
|
|
||||||
<div class="w3-container w3-padding">
|
<div class="w3-container w3-padding">
|
||||||
<header class="w3-container">
|
<header class="w3-container">
|
||||||
<h2>Reactions</h2>
|
<h2>Reactions</h2>
|
||||||
<span class="w3-button w3-display-topright" @click=${this.clear}
|
<span
|
||||||
|
class="w3-button w3-display-topright"
|
||||||
|
@click=${this.clear}
|
||||||
>×</span
|
>×</span
|
||||||
>
|
>
|
||||||
</header>
|
</header>
|
||||||
<ul class="w3-theme-dark w3-container w3-ul">
|
<ul class="w3-theme-dark w3-container w3-ul">
|
||||||
${this.votes.map(
|
${this.votes
|
||||||
|
.sort((x, y) => y.timestamp - x.timestamp)
|
||||||
|
.map(
|
||||||
(x) => html`
|
(x) => html`
|
||||||
<li class="w3-bar">
|
<li
|
||||||
<span class="w3-bar-item"
|
style="display: flex; flex-direction: row; gap: 4px"
|
||||||
|
>
|
||||||
|
<span style="flex-basis: 3em"
|
||||||
>${x?.content?.vote?.expression}</span
|
>${x?.content?.vote?.expression}</span
|
||||||
>
|
>
|
||||||
<tf-user
|
<tf-user
|
||||||
class="w3-bar-item"
|
style="flex: 1 1; overflow: hidden"
|
||||||
id=${x.author}
|
id=${x.author}
|
||||||
.users=${this.users}
|
.users=${this.users}
|
||||||
></tf-user>
|
></tf-user>
|
||||||
<span class="w3-bar-item w3-right"
|
<span
|
||||||
|
style="flex-shrink: 1; white-space: nowrap; overflow: hidden; text-overflow: ellipsis"
|
||||||
>${new Date(x?.timestamp).toLocaleString()}</span
|
>${new Date(x?.timestamp).toLocaleString()}</span
|
||||||
>
|
>
|
||||||
</li>
|
</li>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import {css} from './lit-all.min.js';
|
import {css, unsafeCSS, until} from './lit-all.min.js';
|
||||||
|
import * as tfrpc from '/static/tfrpc.js';
|
||||||
|
|
||||||
const tf = css`
|
const tf = css`
|
||||||
img {
|
img {
|
||||||
@@ -43,12 +44,14 @@ const tf = css`
|
|||||||
border-left: 4px solid #fff;
|
border-left: 4px solid #fff;
|
||||||
padding: 8px;
|
padding: 8px;
|
||||||
padding-left: 12px;
|
padding-left: 12px;
|
||||||
|
margin-left: 0;
|
||||||
|
margin-right: 0;
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
// prettier-ignore
|
// prettier-ignore
|
||||||
const w3 = css`
|
const w3 = css`
|
||||||
/* W3.CSS 4.15 December 2020 by Jan Egil and Borge Refsnes */
|
/* W3.CSS 5.01 March 14 2025 by Jan Egil and Borge Refsnes */
|
||||||
html{box-sizing:border-box}*,*:before,*:after{box-sizing:inherit}
|
html{box-sizing:border-box}*,*:before,*:after{box-sizing:inherit}
|
||||||
/* Extract from normalize.css by Nicolas Gallagher and Jonathan Neal git.io/normalize */
|
/* Extract from normalize.css by Nicolas Gallagher and Jonathan Neal git.io/normalize */
|
||||||
html{-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%}body{margin:0}
|
html{-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%}body{margin:0}
|
||||||
@@ -158,6 +161,10 @@ hr{border:0;border-top:1px solid #eee;margin:20px 0}
|
|||||||
.w3-round-small{border-radius:2px}.w3-round,.w3-round-medium{border-radius:4px}.w3-round-large{border-radius:8px}.w3-round-xlarge{border-radius:16px}.w3-round-xxlarge{border-radius:32px}
|
.w3-round-small{border-radius:2px}.w3-round,.w3-round-medium{border-radius:4px}.w3-round-large{border-radius:8px}.w3-round-xlarge{border-radius:16px}.w3-round-xxlarge{border-radius:32px}
|
||||||
.w3-row-padding,.w3-row-padding>.w3-half,.w3-row-padding>.w3-third,.w3-row-padding>.w3-twothird,.w3-row-padding>.w3-threequarter,.w3-row-padding>.w3-quarter,.w3-row-padding>.w3-col{padding:0 8px}
|
.w3-row-padding,.w3-row-padding>.w3-half,.w3-row-padding>.w3-third,.w3-row-padding>.w3-twothird,.w3-row-padding>.w3-threequarter,.w3-row-padding>.w3-quarter,.w3-row-padding>.w3-col{padding:0 8px}
|
||||||
.w3-container,.w3-panel{padding:0.01em 16px}.w3-panel{margin-top:16px;margin-bottom:16px}
|
.w3-container,.w3-panel{padding:0.01em 16px}.w3-panel{margin-top:16px;margin-bottom:16px}
|
||||||
|
|
||||||
|
.w3-grid{display:grid}.w3-grid-padding{display:grid;gap:16px}.w3-flex{display:flex}
|
||||||
|
.w3-text-center{text-align:center}.w3-text-bold,.w3-bold{font-weight:bold}.w3-text-italic,.w3-italic{font-style:italic}
|
||||||
|
|
||||||
.w3-code,.w3-codespan{font-family:Consolas,"courier new";font-size:16px}
|
.w3-code,.w3-codespan{font-family:Consolas,"courier new";font-size:16px}
|
||||||
.w3-code{width:auto;background-color:#fff;padding:8px 12px;border-left:4px solid #4CAF50;word-wrap:break-word}
|
.w3-code{width:auto;background-color:#fff;padding:8px 12px;border-left:4px solid #4CAF50;word-wrap:break-word}
|
||||||
.w3-codespan{color:crimson;background-color:#f1f1f1;padding-left:4px;padding-right:4px;font-size:110%}
|
.w3-codespan{color:crimson;background-color:#f1f1f1;padding-left:4px;padding-right:4px;font-size:110%}
|
||||||
@@ -199,9 +206,9 @@ hr{border:0;border-top:1px solid #eee;margin:20px 0}
|
|||||||
.w3-transparent,.w3-hover-none:hover{background-color:transparent!important}
|
.w3-transparent,.w3-hover-none:hover{background-color:transparent!important}
|
||||||
.w3-hover-none:hover{box-shadow:none!important}
|
.w3-hover-none:hover{box-shadow:none!important}
|
||||||
/* Colors */
|
/* Colors */
|
||||||
.w3-amber,.w3-hover-amber:hover{color:#000!important;background-color:#ffc107!important}
|
.w3-amber,.w3-hover-amber:hover,.w3-warning{color:#000!important;background-color:#ffc107!important}
|
||||||
.w3-aqua,.w3-hover-aqua:hover{color:#000!important;background-color:#00ffff!important}
|
.w3-aqua,.w3-hover-aqua:hover{color:#000!important;background-color:#00ffff!important}
|
||||||
.w3-blue,.w3-hover-blue:hover{color:#fff!important;background-color:#2196F3!important}
|
.w3-blue,.w3-hover-blue:hover,.w3-info,.w3-primary{color:#fff!important;background-color:#2196F3!important}
|
||||||
.w3-light-blue,.w3-hover-light-blue:hover{color:#000!important;background-color:#87CEEB!important}
|
.w3-light-blue,.w3-hover-light-blue:hover{color:#000!important;background-color:#87CEEB!important}
|
||||||
.w3-brown,.w3-hover-brown:hover{color:#fff!important;background-color:#795548!important}
|
.w3-brown,.w3-hover-brown:hover{color:#fff!important;background-color:#795548!important}
|
||||||
.w3-cyan,.w3-hover-cyan:hover{color:#000!important;background-color:#00bcd4!important}
|
.w3-cyan,.w3-hover-cyan:hover{color:#000!important;background-color:#00bcd4!important}
|
||||||
@@ -216,15 +223,24 @@ hr{border:0;border-top:1px solid #eee;margin:20px 0}
|
|||||||
.w3-pink,.w3-hover-pink:hover{color:#fff!important;background-color:#e91e63!important}
|
.w3-pink,.w3-hover-pink:hover{color:#fff!important;background-color:#e91e63!important}
|
||||||
.w3-purple,.w3-hover-purple:hover{color:#fff!important;background-color:#9c27b0!important}
|
.w3-purple,.w3-hover-purple:hover{color:#fff!important;background-color:#9c27b0!important}
|
||||||
.w3-deep-purple,.w3-hover-deep-purple:hover{color:#fff!important;background-color:#673ab7!important}
|
.w3-deep-purple,.w3-hover-deep-purple:hover{color:#fff!important;background-color:#673ab7!important}
|
||||||
.w3-red,.w3-hover-red:hover{color:#fff!important;background-color:#f44336!important}
|
.w3-red,.w3-hover-red:hover,.w3-danger{color:#fff!important;background-color:#f44336!important}
|
||||||
.w3-sand,.w3-hover-sand:hover{color:#000!important;background-color:#fdf5e6!important}
|
.w3-sand,.w3-hover-sand:hover{color:#000!important;background-color:#fdf5e6!important}
|
||||||
.w3-teal,.w3-hover-teal:hover{color:#fff!important;background-color:#009688!important}
|
.w3-teal,.w3-hover-teal:hover{color:#fff!important;background-color:#009688!important}
|
||||||
.w3-yellow,.w3-hover-yellow:hover{color:#000!important;background-color:#ffeb3b!important}
|
.w3-yellow,.w3-hover-yellow:hover,.w3-note{color:#000!important;background-color:#ffeb3b!important}
|
||||||
.w3-white,.w3-hover-white:hover{color:#000!important;background-color:#fff!important}
|
.w3-white,.w3-hover-white:hover{color:#000!important;background-color:#fff!important}
|
||||||
.w3-black,.w3-hover-black:hover{color:#fff!important;background-color:#000!important}
|
.w3-black,.w3-hover-black:hover{color:#fff!important;background-color:#000!important}
|
||||||
.w3-grey,.w3-hover-grey:hover,.w3-gray,.w3-hover-gray:hover{color:#000!important;background-color:#9e9e9e!important}
|
|
||||||
|
.w3-grey,.w3-hover-grey:hover,.w3-gray,.w3-hover-gray:hover,.w3-secondary{color:#000!important;background-color:#9e9e9e!important}
|
||||||
.w3-light-grey,.w3-hover-light-grey:hover,.w3-light-gray,.w3-hover-light-gray:hover{color:#000!important;background-color:#f1f1f1!important}
|
.w3-light-grey,.w3-hover-light-grey:hover,.w3-light-gray,.w3-hover-light-gray:hover{color:#000!important;background-color:#f1f1f1!important}
|
||||||
.w3-dark-grey,.w3-hover-dark-grey:hover,.w3-dark-gray,.w3-hover-dark-gray:hover{color:#fff!important;background-color:#616161!important}
|
.w3-dark-grey,.w3-hover-dark-grey:hover,.w3-dark-gray,.w3-hover-dark-gray:hover{color:#fff!important;background-color:#616161!important}
|
||||||
|
|
||||||
|
.w3-asphalt,.w3-hover-asphalt:hover{color:#fff!important;background-color:#343a40!important}.w3-crimson,.w3-hover-crimson:hover{color:#fff!important;background-color:#a20025!important}
|
||||||
|
.w3-cobalt,w3-hover-cobalt:hover{color:#fff!important;background-color:#0050ef!important}
|
||||||
|
.w3-emerald,.w3-hover-emerald:hover,.w3-success{color:#fff!important;background-color:#008a00!important}
|
||||||
|
.w3-olive,.w3-hover-olive:hover{color:#fff!important;background-color:#6d8764!important}
|
||||||
|
.w3-paper,.w3-hover-paper:hover{color:#000!important;background-color:#f8f9fa!important}.w3-sienna,.w3-hover-sienna:hover{color:#fff!important;background-color:#a0522d!important}
|
||||||
|
.w3-taupe,.w3-hover-taupe:hover{color:#fff!important;background-color:#87794e!important}
|
||||||
|
|
||||||
.w3-pale-red,.w3-hover-pale-red:hover{color:#000!important;background-color:#ffdddd!important}
|
.w3-pale-red,.w3-hover-pale-red:hover{color:#000!important;background-color:#ffdddd!important}
|
||||||
.w3-pale-green,.w3-hover-pale-green:hover{color:#000!important;background-color:#ddffdd!important}
|
.w3-pale-green,.w3-hover-pale-green:hover{color:#000!important;background-color:#ddffdd!important}
|
||||||
.w3-pale-yellow,.w3-hover-pale-yellow:hover{color:#000!important;background-color:#ffffcc!important}
|
.w3-pale-yellow,.w3-hover-pale-yellow:hover{color:#000!important;background-color:#ffffcc!important}
|
||||||
@@ -285,30 +301,181 @@ hr{border:0;border-top:1px solid #eee;margin:20px 0}
|
|||||||
.w3-border-pale-yellow,.w3-hover-border-pale-yellow:hover{border-color:#ffffcc!important}.w3-border-pale-blue,.w3-hover-border-pale-blue:hover{border-color:#e7ffff!important}
|
.w3-border-pale-yellow,.w3-hover-border-pale-yellow:hover{border-color:#ffffcc!important}.w3-border-pale-blue,.w3-hover-border-pale-blue:hover{border-color:#e7ffff!important}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
// prettier-ignore
|
function rgb_to_hsl(r, g, b) {
|
||||||
const w3_2016_riverside = css`
|
let min,
|
||||||
.w3-theme-l5 {color:#000 !important; background-color:#f4f6f9 !important}
|
max,
|
||||||
.w3-theme-l4 {color:#000 !important; background-color:#d9e1ec !important}
|
i,
|
||||||
.w3-theme-l3 {color:#000 !important; background-color:#b4c3d8 !important}
|
l,
|
||||||
.w3-theme-l2 {color:#fff !important; background-color:#8ea6c5 !important}
|
s,
|
||||||
.w3-theme-l1 {color:#fff !important; background-color:#6888b1 !important}
|
maxcolor,
|
||||||
.w3-theme-d1 {color:#fff !important; background-color:#456185 !important}
|
h,
|
||||||
.w3-theme-d2 {color:#fff !important; background-color:#3d5676 !important}
|
rgb = [];
|
||||||
.w3-theme-d3 {color:#fff !important; background-color:#354b68 !important}
|
rgb[0] = r / 255;
|
||||||
.w3-theme-d4 {color:#fff !important; background-color:#2e4059 !important}
|
rgb[1] = g / 255;
|
||||||
.w3-theme-d5 {color:#fff !important; background-color:#26364a !important}
|
rgb[2] = b / 255;
|
||||||
|
min = rgb[0];
|
||||||
|
max = rgb[0];
|
||||||
|
maxcolor = 0;
|
||||||
|
for (i = 0; i < rgb.length - 1; i++) {
|
||||||
|
if (rgb[i + 1] <= min) {
|
||||||
|
min = rgb[i + 1];
|
||||||
|
}
|
||||||
|
if (rgb[i + 1] >= max) {
|
||||||
|
max = rgb[i + 1];
|
||||||
|
maxcolor = i + 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (maxcolor == 0) {
|
||||||
|
h = (rgb[1] - rgb[2]) / (max - min);
|
||||||
|
}
|
||||||
|
if (maxcolor == 1) {
|
||||||
|
h = 2 + (rgb[2] - rgb[0]) / (max - min);
|
||||||
|
}
|
||||||
|
if (maxcolor == 2) {
|
||||||
|
h = 4 + (rgb[0] - rgb[1]) / (max - min);
|
||||||
|
}
|
||||||
|
if (isNaN(h)) {
|
||||||
|
h = 0;
|
||||||
|
}
|
||||||
|
h = h * 60;
|
||||||
|
if (h < 0) {
|
||||||
|
h = h + 360;
|
||||||
|
}
|
||||||
|
l = (min + max) / 2;
|
||||||
|
if (min == max) {
|
||||||
|
s = 0;
|
||||||
|
} else {
|
||||||
|
if (l < 0.5) {
|
||||||
|
s = (max - min) / (max + min);
|
||||||
|
} else {
|
||||||
|
s = (max - min) / (2 - max - min);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
s = s;
|
||||||
|
return [h, s, l];
|
||||||
|
}
|
||||||
|
|
||||||
.w3-theme-light {color:#000 !important; background-color:#f4f6f9 !important}
|
function hex_to_rgb(hex) {
|
||||||
.w3-theme-dark {color:#fff !important; background-color:#26364a !important}
|
if (hex.charAt(0) == '#') {
|
||||||
.w3-theme-action {color:#fff !important; background-color:#26364a !important}
|
hex = hex.substring(1);
|
||||||
|
}
|
||||||
|
return [
|
||||||
|
parseInt(hex.substring(0, 2), 16),
|
||||||
|
parseInt(hex.substring(2, 4), 16),
|
||||||
|
parseInt(hex.substring(4, 6), 16),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
.w3-theme {color:#fff !important; background-color:#4c6a92 !important}
|
function hsl_to_rgb(hue, sat, light) {
|
||||||
.w3-text-theme {color:#4c6a92 !important}
|
let t2;
|
||||||
.w3-border-theme {border-color:#4c6a92 !important}
|
hue /= 60;
|
||||||
|
if (light <= 0.5) {
|
||||||
|
t2 = light * (sat + 1);
|
||||||
|
} else {
|
||||||
|
t2 = light + sat - light * sat;
|
||||||
|
}
|
||||||
|
let t1 = light * 2 - t2;
|
||||||
|
return [
|
||||||
|
hue_to_rgb(t1, t2, hue + 2) * 255,
|
||||||
|
hue_to_rgb(t1, t2, hue) * 255,
|
||||||
|
hue_to_rgb(t1, t2, hue - 2) * 255,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
function hue_to_rgb(t1, t2, hue) {
|
||||||
|
if (hue < 0) {
|
||||||
|
hue += 6;
|
||||||
|
}
|
||||||
|
if (hue >= 6) {
|
||||||
|
hue -= 6;
|
||||||
|
}
|
||||||
|
if (hue < 1) {
|
||||||
|
return (t2 - t1) * hue + t1;
|
||||||
|
} else if (hue < 3) {
|
||||||
|
return t2;
|
||||||
|
} else if (hue < 4) {
|
||||||
|
return (t2 - t1) * (4 - hue) + t1;
|
||||||
|
} else {
|
||||||
|
return t1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.w3-hover-theme:hover {color:#fff !important; background-color:#4c6a92 !important}
|
function rgb_to_hex(rgb) {
|
||||||
.w3-hover-text-theme:hover {color:#4c6a92 !important}
|
const hex_pair = (x) => Math.floor(x).toString(16).padStart(2, '0');
|
||||||
.w3-hover-border-theme:hover {border-color:#4c6a92 !important}
|
return `#${hex_pair(rgb[0])}${hex_pair(rgb[1])}${hex_pair(rgb[2])}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function is_dark(hex, value) {
|
||||||
|
let [r, g, b] = hex_to_rgb(hex);
|
||||||
|
return (r * 299 + g * 587 + b * 114) / 1000 < value;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function generate(color) {
|
||||||
|
let [r, g, b] = hex_to_rgb(color);
|
||||||
|
let [h, s, l] = rgb_to_hsl(r, g, b);
|
||||||
|
|
||||||
|
let theme1 = {
|
||||||
|
l5: rgb_to_hex(hsl_to_rgb(h, s, l + ((1.0 - l) / 5) * 4.7)),
|
||||||
|
l4: rgb_to_hex(hsl_to_rgb(h, s, l + ((1.0 - l) / 5) * 4)),
|
||||||
|
l3: rgb_to_hex(hsl_to_rgb(h, s, l + ((1.0 - l) / 5) * 3)),
|
||||||
|
l2: rgb_to_hex(hsl_to_rgb(h, s, l + ((1.0 - l) / 5) * 2)),
|
||||||
|
l1: rgb_to_hex(hsl_to_rgb(h, s, l + ((1.0 - l) / 5) * 1)),
|
||||||
|
d0: rgb_to_hex(hsl_to_rgb(h, s, l)),
|
||||||
|
d1: rgb_to_hex(hsl_to_rgb(h, s, l - (l / 5) * 0.5)),
|
||||||
|
d2: rgb_to_hex(hsl_to_rgb(h, s, l - (l / 5) * 1)),
|
||||||
|
d3: rgb_to_hex(hsl_to_rgb(h, s, l - (l / 5) * 1.5)),
|
||||||
|
d4: rgb_to_hex(hsl_to_rgb(h, s, l - (l / 5) * 2)),
|
||||||
|
d5: rgb_to_hex(hsl_to_rgb(h, s, l - (l / 5) * 2.5)),
|
||||||
|
};
|
||||||
|
for (let [k, v] of Object.entries(theme1)) {
|
||||||
|
theme1['t' + k] = is_dark(v, 165) ? '#fff' : '#000';
|
||||||
|
}
|
||||||
|
|
||||||
|
let result = `
|
||||||
|
.w3-theme-l5 {color: ${theme1.tl5} !important; background-color: ${theme1.l5} !important}
|
||||||
|
.w3-theme-l4 {color: ${theme1.tl4} !important; background-color: ${theme1.l4} !important}
|
||||||
|
.w3-theme-l3 {color: ${theme1.tl3} !important; background-color: ${theme1.l3} !important}
|
||||||
|
.w3-theme-l2 {color: ${theme1.tl2} !important; background-color: ${theme1.l2} !important}
|
||||||
|
.w3-theme-l1 {color: ${theme1.tl1} !important; background-color: ${theme1.l1} !important}
|
||||||
|
.w3-theme-d1 {color: ${theme1.td1} !important; background-color: ${theme1.d1} !important}
|
||||||
|
.w3-theme-d2 {color: ${theme1.td2} !important; background-color: ${theme1.d2} !important}
|
||||||
|
.w3-theme-d3 {color: ${theme1.td3} !important; background-color: ${theme1.d3} !important}
|
||||||
|
.w3-theme-d4 {color: ${theme1.td4} !important; background-color: ${theme1.d4} !important}
|
||||||
|
.w3-theme-d5 {color: ${theme1.td5} !important; background-color: ${theme1.d5} !important}
|
||||||
|
.w3-theme-light {color: ${theme1.tl5} !important; background-color: ${theme1.l5} !important}
|
||||||
|
.w3-theme-dark {color: ${theme1.td5} !important; background-color: ${theme1.d5} !important}
|
||||||
|
.w3-theme-action {color: ${theme1.td5} !important; background-color: ${theme1.d5} !important}
|
||||||
|
.w3-theme {color: ${theme1.td0} !important; background-color: ${theme1.d0} !important}
|
||||||
|
.w3-text-theme {color: ${theme1.d0} !important}
|
||||||
|
.w3-border-theme {border-color: ${theme1.d0} !important}
|
||||||
|
.w3-hover-theme:hover {color: ${theme1.td0} !important; background-color: ${theme1.d0} !important}
|
||||||
|
.w3-hover-text-theme:hover {color: ${theme1.d0} !important}
|
||||||
|
.w3-hover-border-theme:hover {border-color: ${theme1.d0} !important}
|
||||||
`;
|
`;
|
||||||
|
return unsafeCSS(result);
|
||||||
|
}
|
||||||
|
|
||||||
export let styles = [tf, w3, w3_2016_riverside];
|
let g_theme;
|
||||||
|
export function generate_theme() {
|
||||||
|
return g_theme
|
||||||
|
? g_theme
|
||||||
|
: until(
|
||||||
|
tfrpc.rpc.localStorageGet('color').then(function (value) {
|
||||||
|
g_theme = generate(value ?? '#034f84');
|
||||||
|
return g_theme;
|
||||||
|
}),
|
||||||
|
generated_now()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function generated_now() {
|
||||||
|
let now = new Date();
|
||||||
|
return generate(
|
||||||
|
rgb_to_hex([
|
||||||
|
(now.getDay() * 128) / 6,
|
||||||
|
(now.getHours() * 128) / 23,
|
||||||
|
(now.getSeconds() * 128) / 59,
|
||||||
|
])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export let styles = [tf, w3];
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import {LitElement, html} from './lit-all.min.js';
|
import {LitElement, html} from './lit-all.min.js';
|
||||||
import * as tfrpc from '/static/tfrpc.js';
|
import * as tfrpc from '/static/tfrpc.js';
|
||||||
import {styles} from './tf-styles.js';
|
import {styles, generate_theme} from './tf-styles.js';
|
||||||
|
|
||||||
class TfTabConnectionsElement extends LitElement {
|
class TfTabConnectionsElement extends LitElement {
|
||||||
static get properties() {
|
static get properties() {
|
||||||
@@ -12,11 +12,21 @@ class TfTabConnectionsElement extends LitElement {
|
|||||||
stored_connections: {type: Array},
|
stored_connections: {type: Array},
|
||||||
users: {type: Object},
|
users: {type: Object},
|
||||||
server_identity: {type: String},
|
server_identity: {type: String},
|
||||||
|
connect_attempt: {type: Object},
|
||||||
|
connect_message: {type: String},
|
||||||
|
connect_success: {type: Boolean},
|
||||||
|
peer_exchange: {type: Boolean},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
static styles = styles;
|
static styles = styles;
|
||||||
|
|
||||||
|
static k_broadcast_emojis = {
|
||||||
|
discovery: '🏓',
|
||||||
|
room: '🚪',
|
||||||
|
peer_exchange: '🕸',
|
||||||
|
};
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
let self = this;
|
let self = this;
|
||||||
@@ -38,6 +48,20 @@ class TfTabConnectionsElement extends LitElement {
|
|||||||
tfrpc.rpc.getServerIdentity().then(function (identity) {
|
tfrpc.rpc.getServerIdentity().then(function (identity) {
|
||||||
self.server_identity = identity;
|
self.server_identity = identity;
|
||||||
});
|
});
|
||||||
|
this.check_peer_exchange();
|
||||||
|
}
|
||||||
|
|
||||||
|
async check_peer_exchange() {
|
||||||
|
if (await tfrpc.rpc.isAdministrator()) {
|
||||||
|
this.peer_exchange = await tfrpc.rpc.globalSettingsGet('peer_exchange');
|
||||||
|
} else {
|
||||||
|
this.peer_exchange = undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async enable_peer_exchange() {
|
||||||
|
await tfrpc.rpc.globalSettingsSet('peer_exchange', true);
|
||||||
|
await this.check_peer_exchange();
|
||||||
}
|
}
|
||||||
|
|
||||||
render_connection_summary(connection) {
|
render_connection_summary(connection) {
|
||||||
@@ -82,19 +106,53 @@ class TfTabConnectionsElement extends LitElement {
|
|||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
render_broadcast(connection) {
|
render_message(connection) {
|
||||||
|
return html`<div
|
||||||
|
?hidden=${this.connect_message === undefined ||
|
||||||
|
this.connect_attempt != connection}
|
||||||
|
style="cursor: pointer"
|
||||||
|
class=${'w3-panel ' + (this.connect_success ? 'w3-green' : 'w3-red')}
|
||||||
|
@click=${() => (this.connect_attempt = undefined)}
|
||||||
|
>
|
||||||
|
<p>${this.connect_message}</p>
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
render_progress(name, value, max) {
|
||||||
|
if (max && value != max) {
|
||||||
return html`
|
return html`
|
||||||
<li class="w3-bar" style="overflow: hidden; overflow-wrap: nowrap">
|
<div class="w3-theme-d1 w3-small">
|
||||||
|
<div
|
||||||
|
class="w3-container w3-theme-l1"
|
||||||
|
style="width: ${Math.floor(
|
||||||
|
(100.0 * value) / max
|
||||||
|
)}%; text-wrap: nowrap"
|
||||||
|
>
|
||||||
|
${name} ${value} / ${max} (${Math.round((100.0 * value) / max)}%)
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render_broadcast(connection) {
|
||||||
|
let self = this;
|
||||||
|
return html`
|
||||||
|
<li>
|
||||||
|
<div class="w3-bar" style="overflow: hidden; overflow-wrap: nowrap">
|
||||||
<button
|
<button
|
||||||
class="w3-bar-item w3-button w3-theme-d1"
|
class="w3-bar-item w3-button w3-theme-d1"
|
||||||
@click=${() => tfrpc.rpc.connect(connection)}
|
@click=${() => self.connect(connection)}
|
||||||
>
|
>
|
||||||
Connect
|
Connect
|
||||||
</button>
|
</button>
|
||||||
<div class="w3-bar-item">
|
<div class="w3-bar-item">
|
||||||
|
${TfTabConnectionsElement.k_broadcast_emojis[connection.origin]}
|
||||||
<tf-user id=${connection.pubkey} .users=${this.users}></tf-user>
|
<tf-user id=${connection.pubkey} .users=${this.users}></tf-user>
|
||||||
${this.render_connection_summary(connection)}
|
${this.render_connection_summary(connection)}
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
${this.render_message(connection)}
|
||||||
</li>
|
</li>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
@@ -105,63 +163,170 @@ class TfTabConnectionsElement extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render_connection(connection) {
|
render_connection(connection) {
|
||||||
|
let requests = Object.values(
|
||||||
|
connection.requests.reduce(function (accumulator, value) {
|
||||||
|
let key = `${value.name}:${Math.sign(value.request_number)}`;
|
||||||
|
if (!accumulator[key]) {
|
||||||
|
accumulator[key] = Object.assign({count: 0}, value);
|
||||||
|
}
|
||||||
|
accumulator[key].count++;
|
||||||
|
return accumulator;
|
||||||
|
}, {})
|
||||||
|
);
|
||||||
return html`
|
return html`
|
||||||
|
${connection.connected
|
||||||
|
? html`
|
||||||
<button
|
<button
|
||||||
class="w3-button w3-theme-d1"
|
class="w3-button w3-theme-d1"
|
||||||
@click=${() => tfrpc.rpc.closeConnection(connection.id)}
|
@click=${() => tfrpc.rpc.closeConnection(connection.id)}
|
||||||
>
|
>
|
||||||
Close
|
Close
|
||||||
</button>
|
</button>
|
||||||
|
`
|
||||||
|
: undefined}
|
||||||
|
${connection.flags.one_shot ? '🔃' : undefined}
|
||||||
<tf-user id=${connection.id} .users=${this.users}></tf-user>
|
<tf-user id=${connection.id} .users=${this.users}></tf-user>
|
||||||
|
${this.render_progress(
|
||||||
|
'recv',
|
||||||
|
connection.progress.in.total - connection.progress.in.current,
|
||||||
|
connection.progress.in.total
|
||||||
|
)}
|
||||||
|
${this.render_progress(
|
||||||
|
'send',
|
||||||
|
connection.progress.out.total - connection.progress.out.current,
|
||||||
|
connection.progress.out.total
|
||||||
|
)}
|
||||||
${connection.tunnel !== undefined
|
${connection.tunnel !== undefined
|
||||||
? '🚇'
|
? '🚇'
|
||||||
: html`(${connection.host}:${connection.port})`}
|
: html`(${connection.host}:${connection.port})`}
|
||||||
<div>${connection.requests.map(x => html`
|
<div>
|
||||||
<span class="w3-tag w3-small">${x.request_number > 0 ? '🟩' : '🟥'} ${x.name}</span>
|
${requests.map(
|
||||||
`)}</div>
|
(x) => html`
|
||||||
|
<span
|
||||||
|
class=${'w3-tag w3-small ' +
|
||||||
|
(x.active ? 'w3-theme-l3' : 'w3-theme-d3')}
|
||||||
|
>${x.request_number > 0 ? '🟩' : '🟥'} ${x.name}
|
||||||
|
<span
|
||||||
|
class="w3-badge w3-white"
|
||||||
|
style=${x.count > 1 ? undefined : 'display: none'}
|
||||||
|
>${x.count}</span
|
||||||
|
></span
|
||||||
|
>
|
||||||
|
`
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
<ul>
|
<ul>
|
||||||
${this.connections
|
${this.connections
|
||||||
.filter((x) => x.tunnel === this.connections.indexOf(connection))
|
.filter((x) => x.tunnel === this.connections.indexOf(connection))
|
||||||
.map((x) => html`<li>${this.render_connection(x)}</li>`)}
|
.map((x) => html`<li>${this.render_connection(x)}</li>`)}
|
||||||
${this.render_room_peers(connection.id)}
|
${this.render_room_peers(connection.id)}
|
||||||
</ul>
|
</ul>
|
||||||
|
<div ?hidden=${!connection.destroy_reason} class="w3-panel w3-red">
|
||||||
|
<p>${connection.destroy_reason}</p>
|
||||||
|
</div>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
connect(address) {
|
||||||
|
let self = this;
|
||||||
|
self.connect_attempt = address;
|
||||||
|
self.connect_message = undefined;
|
||||||
|
self.connect_success = false;
|
||||||
|
tfrpc.rpc
|
||||||
|
.connect(address)
|
||||||
|
.then(function () {
|
||||||
|
if (self.connect_attempt == address) {
|
||||||
|
self.connect_message = 'Connected.';
|
||||||
|
self.connect_success = true;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(function (error) {
|
||||||
|
if (self.connect_attempt == address) {
|
||||||
|
self.connect_message = 'Error: ' + error;
|
||||||
|
self.connect_success = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
toggle_accordian(id) {
|
||||||
|
let element = this.renderRoot.getElementById(id);
|
||||||
|
element.classList.toggle('w3-hide');
|
||||||
|
}
|
||||||
|
|
||||||
|
valid_connections() {
|
||||||
|
return this.connections.filter((x) => x.tunnel === undefined);
|
||||||
|
}
|
||||||
|
|
||||||
|
valid_broadcasts() {
|
||||||
|
return this.broadcasts
|
||||||
|
.filter((x) => x.address)
|
||||||
|
.filter((x) => this.connections.map((c) => c.id).indexOf(x.pubkey) == -1);
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
let self = this;
|
let self = this;
|
||||||
return html`
|
return html`
|
||||||
|
<style>
|
||||||
|
${generate_theme()}
|
||||||
|
</style>
|
||||||
<div class="w3-container" style="box-sizing: border-box">
|
<div class="w3-container" style="box-sizing: border-box">
|
||||||
|
<div
|
||||||
|
class=${'w3-panel w3-padding w3-theme-l3' +
|
||||||
|
(this.peer_exchange !== false ? ' w3-hide' : '')}
|
||||||
|
>
|
||||||
|
<p>
|
||||||
|
Looking for connections? Enabling this option will include publicly
|
||||||
|
advertised rooms and pubs among the list of discovered connections
|
||||||
|
to help you replicate.
|
||||||
|
</p>
|
||||||
|
<button
|
||||||
|
class="w3-button w3-theme-d1"
|
||||||
|
@click=${this.enable_peer_exchange}
|
||||||
|
>
|
||||||
|
🔍🌐 Use publicly advertised peers
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
<h2>New Connection</h2>
|
<h2>New Connection</h2>
|
||||||
<textarea class="w3-input w3-theme-d1" id="code"></textarea>
|
<textarea class="w3-input w3-theme-d1" id="code"></textarea>
|
||||||
|
${this.render_message(this.renderRoot.getElementById('code')?.value)}
|
||||||
<button
|
<button
|
||||||
class="w3-button w3-theme-d1"
|
class="w3-button w3-theme-d1"
|
||||||
@click=${() =>
|
@click=${() =>
|
||||||
tfrpc.rpc.connect(self.renderRoot.getElementById('code').value)}
|
self.connect(self.renderRoot.getElementById('code')?.value)}
|
||||||
>
|
>
|
||||||
Connect
|
Connect
|
||||||
</button>
|
</button>
|
||||||
<h2>Broadcasts</h2>
|
<h2
|
||||||
<ul class="w3-ul w3-border">
|
class="w3-button w3-block w3-theme-d1"
|
||||||
${this.broadcasts
|
@click=${() => self.toggle_accordian('connections')}
|
||||||
.filter((x) => x.address)
|
>
|
||||||
.map((x) => self.render_broadcast(x))}
|
Connections (${this.valid_connections().length})
|
||||||
</ul>
|
</h2>
|
||||||
<h2>Connections</h2>
|
<ul class="w3-ul w3-border" id="connections">
|
||||||
<ul class="w3-ul w3-border">
|
${this.valid_connections().map(
|
||||||
${this.connections
|
(x) => html` <li class="w3-bar">${this.render_connection(x)}</li> `
|
||||||
.filter((x) => x.tunnel === undefined)
|
|
||||||
.map(
|
|
||||||
(x) => html`
|
|
||||||
<li class="w3-bar">${this.render_connection(x)}</li>
|
|
||||||
`
|
|
||||||
)}
|
)}
|
||||||
</ul>
|
</ul>
|
||||||
<h2>Stored Connections</h2>
|
<h2
|
||||||
<ul class="w3-ul w3-border">
|
class="w3-button w3-block w3-theme-d1"
|
||||||
|
@click=${() => self.toggle_accordian('broadcasts')}
|
||||||
|
>
|
||||||
|
Discovery (${this.valid_broadcasts().length})
|
||||||
|
</h2>
|
||||||
|
<ul class="w3-ul w3-border w3-hide" id="broadcasts">
|
||||||
|
${this.valid_broadcasts().map((x) => self.render_broadcast(x))}
|
||||||
|
</ul>
|
||||||
|
<h2
|
||||||
|
class="w3-button w3-block w3-theme-d1"
|
||||||
|
@click=${() => self.toggle_accordian('stored_connections')}
|
||||||
|
>
|
||||||
|
Stored Connections (${this.stored_connections.length})
|
||||||
|
</h2>
|
||||||
|
<ul class="w3-ul w3-border w3-hide" id="stored_connections">
|
||||||
${this.stored_connections.map(
|
${this.stored_connections.map(
|
||||||
(x) => html`
|
(x) => html`
|
||||||
<li class="w3-bar">
|
<li>
|
||||||
|
<div class="w3-bar">
|
||||||
<button
|
<button
|
||||||
class="w3-bar-item w3-button w3-theme-d1"
|
class="w3-bar-item w3-button w3-theme-d1"
|
||||||
@click=${() => self.forget_stored_connection(x)}
|
@click=${() => self.forget_stored_connection(x)}
|
||||||
@@ -170,33 +335,53 @@ class TfTabConnectionsElement extends LitElement {
|
|||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
class="w3-bar-item w3-button w3-theme-d1"
|
class="w3-bar-item w3-button w3-theme-d1"
|
||||||
@click=${() => tfrpc.rpc.connect(x)}
|
@click=${() => this.connect(x)}
|
||||||
>
|
>
|
||||||
Connect
|
Connect
|
||||||
</button>
|
</button>
|
||||||
<div class="w3-bar-item">
|
<div class="w3-bar-item">
|
||||||
<tf-user id=${x.pubkey} .users=${self.users}></tf-user>
|
<tf-user id=${x.pubkey} .users=${self.users}></tf-user>
|
||||||
<div><small>${x.address}:${x.port}</small></div>
|
<div><small>${x.address}:${x.port}</small></div>
|
||||||
|
<div>
|
||||||
|
<small
|
||||||
|
>Last connection:
|
||||||
|
${new Date(x.last_success * 1000)}</small
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
${this.render_message(x)}
|
||||||
</li>
|
</li>
|
||||||
`
|
`
|
||||||
)}
|
)}
|
||||||
</ul>
|
</ul>
|
||||||
<h2>Local Accounts</h2>
|
<h2
|
||||||
<ul class="w3-ul w3-border">
|
class="w3-button w3-block w3-theme-d1"
|
||||||
|
@click=${() => self.toggle_accordian('local_accounts')}
|
||||||
|
>
|
||||||
|
Local Accounts (${this.identities.length})
|
||||||
|
</h2>
|
||||||
|
<div class="w3-container w3-hide" id="local_accounts">
|
||||||
${this.identities.map(
|
${this.identities.map(
|
||||||
(x) =>
|
(x) =>
|
||||||
html`<li class="w3-bar">
|
html`<div
|
||||||
${x == this.server_identity ?
|
class="w3-tag w3-round w3-theme-l3"
|
||||||
html`<span class="w3-tag w3-medium w3-round w3-theme-l1">🖥 local server</span>` :
|
style="padding: 4px; margin: 2px; max-width: 100%; text-wrap: nowrap; overflow: hidden"
|
||||||
undefined}
|
>
|
||||||
${this.my_identities.indexOf(x) != -1 ?
|
${x == this.server_identity
|
||||||
html`<span class="w3-tag w3-medium w3-round w3-theme-d1">😎 you</span>` :
|
? html`<div class="w3-tag w3-medium w3-round w3-theme-l1">
|
||||||
undefined}
|
🖥 local server
|
||||||
|
</div>`
|
||||||
|
: undefined}
|
||||||
|
${this.my_identities.indexOf(x) != -1
|
||||||
|
? html`<div class="w3-tag w3-medium w3-round w3-theme-d1">
|
||||||
|
😎 you
|
||||||
|
</div>`
|
||||||
|
: undefined}
|
||||||
<tf-user id=${x} .users=${this.users}></tf-user>
|
<tf-user id=${x} .users=${this.users}></tf-user>
|
||||||
</li>`
|
</div>`
|
||||||
)}
|
)}
|
||||||
</ul>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,78 +0,0 @@
|
|||||||
import {LitElement, html, unsafeHTML} from './lit-all.min.js';
|
|
||||||
import * as tfrpc from '/static/tfrpc.js';
|
|
||||||
import {styles} from './tf-styles.js';
|
|
||||||
|
|
||||||
class TfTabMentionsElement extends LitElement {
|
|
||||||
static get properties() {
|
|
||||||
return {
|
|
||||||
whoami: {type: String},
|
|
||||||
users: {type: Object},
|
|
||||||
following: {type: Array},
|
|
||||||
expanded: {type: Object},
|
|
||||||
messages: {type: Array},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
static styles = styles;
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
super();
|
|
||||||
let self = this;
|
|
||||||
this.whoami = null;
|
|
||||||
this.users = {};
|
|
||||||
this.following = [];
|
|
||||||
this.expanded = {};
|
|
||||||
this.messages = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
async load() {
|
|
||||||
console.log('Loading...', this.whoami);
|
|
||||||
let results = await tfrpc.rpc.query(
|
|
||||||
`
|
|
||||||
SELECT messages.id, messages.previous, messages.author, messages.sequence, messages.timestamp, messages.hash, json(messages.content) AS content, messages.signature
|
|
||||||
FROM messages_fts(?)
|
|
||||||
JOIN messages ON messages.rowid = messages_fts.rowid
|
|
||||||
JOIN json_each(?) AS following ON messages.author = following.value
|
|
||||||
WHERE messages.author != ?
|
|
||||||
ORDER BY timestamp DESC limit 20
|
|
||||||
`,
|
|
||||||
[
|
|
||||||
'"' + this.whoami.replace('"', '""') + '"',
|
|
||||||
JSON.stringify(this.following),
|
|
||||||
this.whoami,
|
|
||||||
]
|
|
||||||
);
|
|
||||||
console.log('Done.');
|
|
||||||
this.messages = results;
|
|
||||||
}
|
|
||||||
|
|
||||||
on_expand(event) {
|
|
||||||
if (event.detail.expanded) {
|
|
||||||
let expand = {};
|
|
||||||
expand[event.detail.id] = true;
|
|
||||||
this.expanded = Object.assign({}, this.expanded, expand);
|
|
||||||
} else {
|
|
||||||
delete this.expanded[event.detail.id];
|
|
||||||
this.expanded = Object.assign({}, this.expanded);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
let self = this;
|
|
||||||
if (!this.loading) {
|
|
||||||
this.loading = true;
|
|
||||||
this.load();
|
|
||||||
}
|
|
||||||
return html`
|
|
||||||
<tf-news
|
|
||||||
id="news"
|
|
||||||
whoami=${this.whoami}
|
|
||||||
.messages=${this.messages}
|
|
||||||
.users=${this.users}
|
|
||||||
.expanded=${this.expanded}
|
|
||||||
@tf-expand=${this.on_expand}
|
|
||||||
></tf-news>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
customElements.define('tf-tab-mentions', TfTabMentionsElement);
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import {LitElement, html, unsafeHTML, until} from './lit-all.min.js';
|
import {LitElement, cache, html, unsafeHTML, until} from './lit-all.min.js';
|
||||||
import * as tfrpc from '/static/tfrpc.js';
|
import * as tfrpc from '/static/tfrpc.js';
|
||||||
import {styles} from './tf-styles.js';
|
import {styles, generate_theme} from './tf-styles.js';
|
||||||
|
|
||||||
class TfTabNewsFeedElement extends LitElement {
|
class TfTabNewsFeedElement extends LitElement {
|
||||||
static get properties() {
|
static get properties() {
|
||||||
@@ -12,6 +12,14 @@ class TfTabNewsFeedElement extends LitElement {
|
|||||||
messages: {type: Array},
|
messages: {type: Array},
|
||||||
drafts: {type: Object},
|
drafts: {type: Object},
|
||||||
expanded: {type: Object},
|
expanded: {type: Object},
|
||||||
|
channels_unread: {type: Object},
|
||||||
|
channels_latest: {type: Object},
|
||||||
|
loading: {type: Number},
|
||||||
|
time_range: {type: Array},
|
||||||
|
time_loading: {type: Array},
|
||||||
|
private_messages: {type: Array},
|
||||||
|
grouped_private_messages: {type: Object},
|
||||||
|
recent_reactions: {type: Array},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -26,112 +34,296 @@ class TfTabNewsFeedElement extends LitElement {
|
|||||||
this.following = [];
|
this.following = [];
|
||||||
this.drafts = {};
|
this.drafts = {};
|
||||||
this.expanded = {};
|
this.expanded = {};
|
||||||
this.start_time = new Date().valueOf() - 24 * 60 * 60 * 1000;
|
this.channels_unread = {};
|
||||||
|
this.channels_latest = {};
|
||||||
|
this.start_time = new Date().valueOf();
|
||||||
|
this.time_range = [0, 0];
|
||||||
|
this.time_loading = undefined;
|
||||||
|
this.recent_reactions = [];
|
||||||
|
this.loading = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
async fetch_messages() {
|
channel() {
|
||||||
if (this.hash.startsWith('#@')) {
|
return this.hash.startsWith('##')
|
||||||
let r = await tfrpc.rpc.query(
|
? this.hash.substring(2)
|
||||||
|
: this.hash.substring(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
async _fetch_related_messages(messages) {
|
||||||
|
let refs = await tfrpc.rpc.query(
|
||||||
`
|
`
|
||||||
WITH mine AS (SELECT id, previous, author, sequence, timestamp, hash, json(content) AS content, signature
|
WITH
|
||||||
|
news AS (
|
||||||
|
SELECT value AS id FROM json_each(?)
|
||||||
|
)
|
||||||
|
SELECT refs_out.ref AS ref FROM messages_refs refs_out JOIN news ON refs_out.message = news.id
|
||||||
|
UNION
|
||||||
|
SELECT refs_in.message AS ref FROM messages_refs refs_in JOIN news ON refs_in.ref = news.id
|
||||||
|
`,
|
||||||
|
[JSON.stringify(messages.map((x) => x.id))]
|
||||||
|
);
|
||||||
|
let related_messages = await tfrpc.rpc.query(
|
||||||
|
`
|
||||||
|
SELECT FALSE AS is_primary, messages.rowid, messages.id, messages.previous, messages.author, messages.sequence, messages.timestamp, messages.hash, json(messages.content) AS content, messages.signature
|
||||||
FROM messages
|
FROM messages
|
||||||
WHERE messages.author = ?
|
JOIN json_each(?2) refs ON messages.id = refs.value
|
||||||
ORDER BY sequence DESC
|
JOIN json_each(?1) AS following ON messages.author = following.value
|
||||||
LIMIT 20)
|
`,
|
||||||
SELECT messages.id, messages.previous, messages.author, messages.sequence, messages.timestamp, messages.hash, json(messages.content) AS content, messages.signature
|
[JSON.stringify(this.following), JSON.stringify(refs.map((x) => x.ref))]
|
||||||
FROM mine
|
);
|
||||||
JOIN messages_refs ON mine.id = messages_refs.ref
|
let combined = [].concat(messages, related_messages);
|
||||||
|
let refs2 = await tfrpc.rpc.query(
|
||||||
|
`
|
||||||
|
WITH
|
||||||
|
news AS (
|
||||||
|
SELECT value AS id FROM json_each(?)
|
||||||
|
)
|
||||||
|
SELECT refs_out.ref AS ref FROM messages_refs refs_out JOIN news ON refs_out.message = news.id
|
||||||
|
UNION
|
||||||
|
SELECT refs_in.message AS ref FROM messages_refs refs_in JOIN news ON refs_in.ref = news.id
|
||||||
|
`,
|
||||||
|
[JSON.stringify(combined.map((x) => x.id))]
|
||||||
|
);
|
||||||
|
let t0 = new Date();
|
||||||
|
let result = [].concat(
|
||||||
|
combined,
|
||||||
|
await tfrpc.rpc.query(
|
||||||
|
`
|
||||||
|
SELECT FALSE AS is_primary, messages.rowid, messages.id, messages.previous, messages.author, messages.sequence, messages.timestamp, messages.hash, json(messages.content) AS content, messages.signature
|
||||||
|
FROM json_each(?2) refs
|
||||||
|
JOIN messages ON messages.id = refs.value
|
||||||
|
JOIN json_each(?1) following ON messages.author = following.value
|
||||||
|
WHERE messages.content ->> 'type' != 'post'
|
||||||
|
`,
|
||||||
|
[
|
||||||
|
JSON.stringify(this.following),
|
||||||
|
JSON.stringify(refs2.map((x) => x.ref)),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
);
|
||||||
|
let t1 = new Date();
|
||||||
|
console.log((t1 - t0) / 1000);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
async fetch_messages(start_time, end_time) {
|
||||||
|
this.dispatchEvent(
|
||||||
|
new CustomEvent('loadmessages', {
|
||||||
|
bubbles: true,
|
||||||
|
composed: true,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
this.time_loading = [start_time, end_time];
|
||||||
|
let result;
|
||||||
|
const k_max_results = 64;
|
||||||
|
if (this.hash == '#@') {
|
||||||
|
result = await tfrpc.rpc.query(
|
||||||
|
`
|
||||||
|
WITH mentions AS (SELECT messages.rowid, messages.id, messages.previous, messages.author, messages.sequence, messages.timestamp, messages.hash, json(messages.content) AS content, messages.signature
|
||||||
|
FROM messages_fts(?1)
|
||||||
|
JOIN messages ON messages.rowid = messages_fts.rowid
|
||||||
|
JOIN json_each(?2) AS following ON messages.author = following.value
|
||||||
|
WHERE
|
||||||
|
messages.author != ?1 AND
|
||||||
|
(?3 IS NULL OR messages.timestamp >= ?3) AND messages.timestamp < ?4
|
||||||
|
ORDER BY timestamp DESC limit ?5)
|
||||||
|
SELECT FALSE AS is_primary, messages.rowid, messages.id, messages.previous, messages.author, messages.sequence, messages.timestamp, messages.hash, json(messages.content) AS content, messages.signature
|
||||||
|
FROM mentions
|
||||||
|
JOIN messages_refs ON mentions.id = messages_refs.ref
|
||||||
JOIN messages ON messages_refs.message = messages.id
|
JOIN messages ON messages_refs.message = messages.id
|
||||||
UNION
|
UNION
|
||||||
SELECT * FROM mine
|
SELECT TRUE AS is_primary, * FROM mentions
|
||||||
`,
|
`,
|
||||||
[this.hash.substring(1)]
|
[
|
||||||
|
'"' + this.whoami.replace('"', '""') + '"',
|
||||||
|
JSON.stringify(this.following),
|
||||||
|
start_time,
|
||||||
|
end_time,
|
||||||
|
k_max_results,
|
||||||
|
]
|
||||||
);
|
);
|
||||||
return r;
|
} else if (this.hash.startsWith('#@')) {
|
||||||
} else if (this.hash.startsWith('#%')) {
|
result = await tfrpc.rpc.query(
|
||||||
return await tfrpc.rpc.query(
|
|
||||||
`
|
`
|
||||||
SELECT id, previous, author, sequence, timestamp, hash, json(content) AS content, signature
|
WITH
|
||||||
|
selected AS (SELECT rowid, id, previous, author, sequence, timestamp, hash, json(content) AS content, signature
|
||||||
FROM messages
|
FROM messages
|
||||||
WHERE id = ?1
|
WHERE messages.author = ?1 AND (?2 IS NULL OR messages.timestamp >= 2) AND messages.timestamp < ?3
|
||||||
|
ORDER BY sequence DESC LIMIT ?4
|
||||||
|
)
|
||||||
|
SELECT FALSE AS is_primary, messages.rowid, messages.id, messages.previous, messages.author, messages.sequence, messages.timestamp, messages.hash, json(messages.content) AS content, messages.signature
|
||||||
|
FROM selected
|
||||||
|
JOIN messages_refs ON selected.id = messages_refs.ref
|
||||||
|
JOIN messages ON messages_refs.message = messages.id
|
||||||
UNION
|
UNION
|
||||||
SELECT id, previous, author, sequence, timestamp, hash, json(content) AS content, signature
|
SELECT TRUE AS is_primary, * FROM selected
|
||||||
|
`,
|
||||||
|
[this.hash.substring(1), start_time, end_time, k_max_results]
|
||||||
|
);
|
||||||
|
} else if (this.hash.startsWith('#%')) {
|
||||||
|
result = await tfrpc.rpc.query(
|
||||||
|
`
|
||||||
|
SELECT TRUE AS is_primary, id, previous, author, sequence, timestamp, hash, json(content) AS content, signature
|
||||||
|
FROM messages
|
||||||
|
WHERE messages.id = ?1
|
||||||
|
UNION
|
||||||
|
SELECT FALSE AS is_primary, id, previous, author, sequence, timestamp, hash, json(content) AS content, signature
|
||||||
FROM messages JOIN messages_refs
|
FROM messages JOIN messages_refs
|
||||||
ON messages.id = messages_refs.message
|
ON messages.id = messages_refs.message
|
||||||
WHERE messages_refs.ref = ?1
|
WHERE messages_refs.ref = ?1
|
||||||
`,
|
`,
|
||||||
[this.hash.substring(1)]
|
[this.hash.substring(1)]
|
||||||
);
|
);
|
||||||
} else {
|
} else if (this.hash.startsWith('##')) {
|
||||||
let promises = [];
|
let initial_messages = await tfrpc.rpc.query(
|
||||||
const k_following_limit = 256;
|
|
||||||
for (let i = 0; i < this.following.length; i += k_following_limit) {
|
|
||||||
promises.push(
|
|
||||||
tfrpc.rpc.query(
|
|
||||||
`
|
`
|
||||||
WITH news AS (SELECT messages.id, messages.previous, messages.author, messages.sequence, messages.timestamp, messages.hash, json(messages.content) AS content, messages.signature
|
WITH
|
||||||
|
all_news AS (
|
||||||
|
SELECT messages.rowid, messages.id, messages.previous, messages.author, messages.sequence, messages.timestamp, messages.hash, json(messages.content) AS content, messages.signature
|
||||||
FROM messages
|
FROM messages
|
||||||
JOIN json_each(?) AS following ON messages.author = following.value
|
JOIN json_each(?) AS following ON messages.author = following.value
|
||||||
WHERE messages.timestamp > ? AND messages.timestamp < ?
|
WHERE messages.content ->> 'channel' = ?4 AND messages.content ->> 'type' != 'vote'
|
||||||
ORDER BY messages.timestamp DESC)
|
|
||||||
SELECT messages.id, messages.previous, messages.author, messages.sequence, messages.timestamp, messages.hash, json(messages.content) AS content, messages.signature
|
|
||||||
FROM news
|
|
||||||
JOIN messages_refs ON news.id = messages_refs.ref
|
|
||||||
JOIN messages ON messages_refs.message = messages.id
|
|
||||||
UNION
|
UNION
|
||||||
SELECT messages.id, messages.previous, messages.author, messages.sequence, messages.timestamp, messages.hash, json(messages.content) AS content, messages.signature
|
SELECT messages.rowid, messages.id, messages.previous, messages.author, messages.sequence, messages.timestamp, messages.hash, json(messages.content) AS content, messages.signature
|
||||||
FROM news
|
FROM messages_refs
|
||||||
JOIN messages_refs ON news.id = messages_refs.message
|
JOIN messages ON messages.id = messages_refs.message
|
||||||
JOIN messages ON messages_refs.ref = messages.id
|
JOIN json_each(?1) AS following ON messages.author = following.value
|
||||||
UNION
|
WHERE messages_refs.ref = '#' || ?4 AND messages.content ->> 'type' != 'vote'
|
||||||
SELECT news.* FROM news
|
)
|
||||||
|
SELECT TRUE AS is_primary, all_news.* FROM all_news
|
||||||
|
WHERE (?2 IS NULL OR all_news.timestamp >= ?2) AND all_news.timestamp < ?3
|
||||||
|
ORDER BY all_news.timestamp DESC LIMIT ?5
|
||||||
`,
|
`,
|
||||||
[
|
[
|
||||||
JSON.stringify(this.following.slice(i, i + k_following_limit)),
|
JSON.stringify(this.following),
|
||||||
this.start_time,
|
start_time,
|
||||||
/*
|
end_time,
|
||||||
** Don't show messages more than a day into the future to prevent
|
this.hash.substring(2),
|
||||||
** messages with far-future timestamps from staying at the top forever.
|
k_max_results,
|
||||||
*/
|
|
||||||
new Date().valueOf() + 24 * 60 * 60 * 1000,
|
|
||||||
]
|
]
|
||||||
)
|
|
||||||
);
|
);
|
||||||
|
result = await this._fetch_related_messages(initial_messages);
|
||||||
|
} else if (this.hash.startsWith('#🔐')) {
|
||||||
|
let ids =
|
||||||
|
this.hash == '#🔐' ? [] : this.hash.substring('#🔐'.length).split(',');
|
||||||
|
result = await tfrpc.rpc.query(
|
||||||
|
`
|
||||||
|
SELECT TRUE AS is_primary, messages.rowid, messages.id, previous, author, sequence, timestamp, hash, json(content) AS content, signature
|
||||||
|
FROM messages
|
||||||
|
JOIN json_each(?1) AS private_messages ON messages.id = private_messages.value
|
||||||
|
WHERE
|
||||||
|
(?2 IS NULL OR (messages.timestamp >= ?2)) AND messages.timestamp < ?3 AND
|
||||||
|
json(messages.content) LIKE '"%'
|
||||||
|
ORDER BY messages.rowid DESC LIMIT ?4
|
||||||
|
`,
|
||||||
|
[
|
||||||
|
JSON.stringify(
|
||||||
|
this.grouped_private_messages?.[JSON.stringify(ids)]?.map(
|
||||||
|
(x) => x.id
|
||||||
|
) ?? []
|
||||||
|
),
|
||||||
|
start_time,
|
||||||
|
end_time,
|
||||||
|
k_max_results,
|
||||||
|
]
|
||||||
|
);
|
||||||
|
result = (await this.decrypt(result)).filter((x) => x.decrypted);
|
||||||
|
} else if (this.hash == '#👍') {
|
||||||
|
result = await tfrpc.rpc.query(
|
||||||
|
`
|
||||||
|
WITH votes AS (SELECT messages.rowid, messages.id, messages.previous, messages.author, messages.sequence, messages.timestamp, messages.hash, json(messages.content) AS content, messages.signature
|
||||||
|
FROM messages
|
||||||
|
JOIN json_each(?1) AS following ON messages.author = following.value
|
||||||
|
WHERE
|
||||||
|
messages.content ->> 'type' = 'vote' AND
|
||||||
|
(?2 IS NULL OR messages.timestamp >= ?2) AND messages.timestamp < ?3
|
||||||
|
ORDER BY timestamp DESC limit ?4)
|
||||||
|
SELECT FALSE AS is_primary, messages.rowid, messages.id, messages.previous, messages.author, messages.sequence, messages.timestamp, messages.hash, json(messages.content) AS content, messages.signature
|
||||||
|
FROM votes
|
||||||
|
JOIN messages ON messages.id = votes.content ->> '$.vote.link'
|
||||||
|
UNION
|
||||||
|
SELECT TRUE AS is_primary, * FROM votes
|
||||||
|
`,
|
||||||
|
[JSON.stringify(this.following), start_time, end_time, k_max_results]
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
let initial_messages = await tfrpc.rpc.query(
|
||||||
|
`
|
||||||
|
WITH
|
||||||
|
channels AS (SELECT '#' || value AS value FROM json_each(?5))
|
||||||
|
SELECT TRUE AS is_primary, messages.rowid, messages.id, messages.previous, messages.author, messages.sequence, messages.timestamp, messages.hash, json(messages.content) AS content, messages.signature
|
||||||
|
FROM messages
|
||||||
|
JOIN json_each(?1) AS following ON messages.author = following.value
|
||||||
|
WHERE messages.timestamp < ?3 AND (?2 IS NULL OR messages.timestamp >= ?2) AND
|
||||||
|
messages.content ->> 'type' != 'vote' AND
|
||||||
|
(messages.content ->> 'root' IS NULL OR (
|
||||||
|
NOT EXISTS (SELECT * FROM messages root JOIN channels ON ('#' || (root.content ->> 'channel')) = channels.value WHERE root.id = messages.content ->> 'root') AND
|
||||||
|
NOT EXISTS (SELECT * FROM messages root JOIN messages_refs ON root.id = messages.content ->> 'root' JOIN channels ON messages_refs.message = root.id AND messages_refs.ref = channels.value)
|
||||||
|
)) AND
|
||||||
|
(messages.content ->> 'channel' IS NULL OR ('#' || (messages.content ->> 'channel')) NOT IN (SELECT * FROM channels)) AND
|
||||||
|
NOT EXISTS (SELECT * FROM messages_refs JOIN channels ON messages_refs.message = messages.id AND messages_refs.ref = channels.value)
|
||||||
|
ORDER BY timestamp DESC LIMIT ?4
|
||||||
|
`,
|
||||||
|
[
|
||||||
|
JSON.stringify(this.following),
|
||||||
|
start_time,
|
||||||
|
end_time,
|
||||||
|
k_max_results,
|
||||||
|
JSON.stringify(Object.keys(this.channels_latest)),
|
||||||
|
]
|
||||||
|
);
|
||||||
|
result = await this._fetch_related_messages(initial_messages);
|
||||||
}
|
}
|
||||||
return [].concat(...(await Promise.all(promises)));
|
this.time_loading = undefined;
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
update_time_range_from_messages(messages) {
|
||||||
|
let only_primary = messages.filter((x) => x.is_primary);
|
||||||
|
this.time_range = [
|
||||||
|
only_primary.reduce(
|
||||||
|
(accumulator, current) => Math.min(accumulator, current.timestamp),
|
||||||
|
this.time_range[0]
|
||||||
|
),
|
||||||
|
only_primary.reduce(
|
||||||
|
(accumulator, current) => Math.max(accumulator, current.timestamp),
|
||||||
|
this.time_range[1]
|
||||||
|
),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
unread_allowed() {
|
||||||
|
return (
|
||||||
|
this.hash == '#@' ||
|
||||||
|
(!this.hash.startsWith('#%') && !this.hash.startsWith('#@'))
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async load_more() {
|
async load_more() {
|
||||||
let last_start_time = this.start_time;
|
this.loading++;
|
||||||
this.start_time = last_start_time - 24 * 60 * 60 * 1000;
|
this.loading_canceled = false;
|
||||||
let more = await tfrpc.rpc.query(
|
try {
|
||||||
`
|
let more = [];
|
||||||
WITH news AS (SELECT messages.id, messages.previous, messages.author, messages.sequence, messages.timestamp, messages.hash, json(messages.content) AS content, messages.signature
|
let last_start_time = this.time_range[0];
|
||||||
FROM messages
|
try {
|
||||||
JOIN json_each(?) AS following ON messages.author = following.value
|
more = await this.fetch_messages(null, last_start_time);
|
||||||
WHERE messages.timestamp > ?
|
} catch (e) {
|
||||||
AND messages.timestamp <= ?
|
console.log(e);
|
||||||
ORDER BY messages.timestamp DESC)
|
}
|
||||||
SELECT messages.id, messages.previous, messages.author, messages.sequence, messages.timestamp, messages.hash, json(messages.content) AS content, messages.signature
|
this.update_time_range_from_messages(
|
||||||
FROM news
|
more.filter((x) => x.timestamp < last_start_time)
|
||||||
JOIN messages_refs ON news.id = messages_refs.ref
|
|
||||||
JOIN messages ON messages_refs.message = messages.id
|
|
||||||
UNION
|
|
||||||
SELECT messages.id, messages.previous, messages.author, messages.sequence, messages.timestamp, messages.hash, json(messages.content) AS content, messages.signature
|
|
||||||
FROM news
|
|
||||||
JOIN messages_refs ON news.id = messages_refs.message
|
|
||||||
JOIN messages ON messages_refs.ref = messages.id
|
|
||||||
UNION
|
|
||||||
SELECT news.* FROM news
|
|
||||||
`,
|
|
||||||
[JSON.stringify(this.following), this.start_time, last_start_time]
|
|
||||||
);
|
);
|
||||||
this.messages = await this.decrypt([...more, ...this.messages]);
|
this.messages = await this.decrypt([...more, ...this.messages]);
|
||||||
|
} finally {
|
||||||
|
this.loading--;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cancel_load() {
|
||||||
|
this.loading_canceled = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
async decrypt(messages) {
|
async decrypt(messages) {
|
||||||
console.log('decrypt');
|
|
||||||
let result = [];
|
let result = [];
|
||||||
for (let message of messages) {
|
for (let message of messages) {
|
||||||
let content;
|
let content;
|
||||||
@@ -156,44 +348,203 @@ class TfTabNewsFeedElement extends LitElement {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
async add_messages(messages) {
|
merge_messages(old_messages, new_messages) {
|
||||||
this.messages = await this.decrypt([...messages, ...this.messages]);
|
let old_by_id = Object.fromEntries(old_messages.map((x) => [x.id, x]));
|
||||||
|
return new_messages.map((x) => (old_by_id[x.id] ? old_by_id[x.id] : x));
|
||||||
|
}
|
||||||
|
|
||||||
|
async load_latest() {
|
||||||
|
this.loading++;
|
||||||
|
let now = new Date().valueOf();
|
||||||
|
let end_time = now + 24 * 60 * 60 * 1000;
|
||||||
|
let messages = [];
|
||||||
|
try {
|
||||||
|
messages = await this.fetch_messages(this.time_range[0], end_time);
|
||||||
|
messages = await this.decrypt(messages);
|
||||||
|
this.update_time_range_from_messages(
|
||||||
|
messages.filter(
|
||||||
|
(x) => x.timestamp >= this.time_range[0] && x.timestamp < end_time
|
||||||
|
)
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
this.loading--;
|
||||||
|
}
|
||||||
|
this.messages = this.merge_messages(
|
||||||
|
this.messages,
|
||||||
|
Object.values(
|
||||||
|
Object.fromEntries(
|
||||||
|
[...this.messages, ...messages]
|
||||||
|
.sort((x, y) => x.timestamp - y.timestamp)
|
||||||
|
.slice(-1024)
|
||||||
|
.map((x) => [x.id, x])
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
console.log('done loading latest messages.');
|
||||||
|
}
|
||||||
|
|
||||||
|
async load_messages() {
|
||||||
|
let start_time = new Date();
|
||||||
|
let self = this;
|
||||||
|
this.loading++;
|
||||||
|
let messages = [];
|
||||||
|
let original_hash = this.hash;
|
||||||
|
try {
|
||||||
|
if (this._messages_hash !== this.hash) {
|
||||||
|
this.messages = [];
|
||||||
|
this._messages_hash = this.hash;
|
||||||
|
}
|
||||||
|
this._messages_following = JSON.stringify(this.following);
|
||||||
|
this._private_messages = JSON.stringify([
|
||||||
|
this.private_messages,
|
||||||
|
this.grouped_private_messages,
|
||||||
|
]);
|
||||||
|
this._channels_latest = JSON.stringify(
|
||||||
|
Object.keys(this.channels_latest ?? {})
|
||||||
|
);
|
||||||
|
let now = new Date().valueOf();
|
||||||
|
let start_time = now - 24 * 60 * 60 * 1000;
|
||||||
|
this.start_time = start_time;
|
||||||
|
this.time_range = [now + 24 * 60 * 60 * 1000, now + 24 * 60 * 60 * 1000];
|
||||||
|
messages = await this.fetch_messages(null, this.time_range[1]);
|
||||||
|
this.update_time_range_from_messages(
|
||||||
|
messages.filter((x) => x.timestamp < this.time_range[1])
|
||||||
|
);
|
||||||
|
messages = await this.decrypt(messages);
|
||||||
|
} finally {
|
||||||
|
this.loading--;
|
||||||
|
}
|
||||||
|
if (this.hash == original_hash) {
|
||||||
|
this.messages = this.merge_messages(this.messages, messages);
|
||||||
|
}
|
||||||
|
this.time_loading = undefined;
|
||||||
|
console.log(
|
||||||
|
`loading ${messages.length} messages done for ${self.whoami} in ${(new Date() - start_time) / 1000}s`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
mark_all_read() {
|
||||||
|
let newest = this.messages.reduce(
|
||||||
|
(accumulator, current) => Math.max(accumulator, current.rowid),
|
||||||
|
this.channels_latest[this.channel()] ?? -1
|
||||||
|
);
|
||||||
|
if (newest >= 0) {
|
||||||
|
this.dispatchEvent(
|
||||||
|
new CustomEvent('channelsetunread', {
|
||||||
|
bubbles: true,
|
||||||
|
composed: true,
|
||||||
|
detail: {
|
||||||
|
channel: this.channel(),
|
||||||
|
unread: newest + 1,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
close_private_chat() {
|
||||||
|
this.mark_all_read();
|
||||||
|
this.dispatchEvent(
|
||||||
|
new CustomEvent('closeprivatechat', {
|
||||||
|
bubbles: true,
|
||||||
|
composed: true,
|
||||||
|
detail: {
|
||||||
|
key: JSON.stringify(
|
||||||
|
this.hash == '#🔐'
|
||||||
|
? []
|
||||||
|
: this.hash.substring('#🔐'.length).split(',')
|
||||||
|
),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
|
tfrpc.rpc.setHash('#');
|
||||||
|
}
|
||||||
|
|
||||||
|
render_close_chat_button() {
|
||||||
|
if (this.hash.startsWith('#🔐')) {
|
||||||
|
return html`
|
||||||
|
<button class="w3-button w3-theme-d1" @click=${this.close_private_chat}>
|
||||||
|
Close Chat
|
||||||
|
</button>
|
||||||
|
`;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
if (
|
if (
|
||||||
!this.messages ||
|
!this.messages ||
|
||||||
this._messages_hash !== this.hash ||
|
this._messages_hash !== this.hash ||
|
||||||
this._messages_following !== this.following
|
this._messages_following !== JSON.stringify(this.following) ||
|
||||||
|
this._private_messages !==
|
||||||
|
JSON.stringify([
|
||||||
|
this.private_messages,
|
||||||
|
this.grouped_private_messages,
|
||||||
|
]) ||
|
||||||
|
this._channels_latest !==
|
||||||
|
JSON.stringify(Object.keys(this.channels_latest))
|
||||||
) {
|
) {
|
||||||
console.log(
|
console.log(
|
||||||
`loading messages for ${this.whoami} (following ${this.following.length})`
|
`loading messages for ${this.whoami} (messages=${!this.messages},${this._messages_hash != this.hash} following=${this._messages_following !== JSON.stringify(this.following)}, channels=${this._channels_latest !== JSON.stringify(Object.keys(this.channels_latest))}, private=${this._private_messages !== JSON.stringify([this.private_messages, this.grouped_private_messages])},${this.private_messages?.length},${Object.keys(this.grouped_private_messages ?? {}).length})`
|
||||||
);
|
);
|
||||||
let self = this;
|
this.load_messages();
|
||||||
this.messages = [];
|
|
||||||
this._messages_hash = this.hash;
|
|
||||||
this._messages_following = this.following;
|
|
||||||
this.fetch_messages()
|
|
||||||
.then(this.decrypt.bind(this))
|
|
||||||
.then(function (messages) {
|
|
||||||
self.messages = messages;
|
|
||||||
console.log(`loading mesages done for ${self.whoami}`);
|
|
||||||
})
|
|
||||||
.catch(function (error) {
|
|
||||||
alert(JSON.stringify(error, null, 2));
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
let more;
|
let more;
|
||||||
if (!this.hash.startsWith('#@') && !this.hash.startsWith('#%')) {
|
if (!this.hash.startsWith('#%')) {
|
||||||
more = html`
|
more = html`
|
||||||
<p>
|
<p>
|
||||||
<button class="w3-button w3-theme-d1" @click=${this.load_more}>
|
${this.unread_allowed()
|
||||||
|
? html`
|
||||||
|
<button
|
||||||
|
class="w3-button w3-theme-d1"
|
||||||
|
@click=${this.mark_all_read}
|
||||||
|
>
|
||||||
|
Mark All Read
|
||||||
|
</button>
|
||||||
|
`
|
||||||
|
: undefined}
|
||||||
|
<button
|
||||||
|
?disabled=${this.loading}
|
||||||
|
class="w3-button w3-theme-d1"
|
||||||
|
@click=${this.load_more}
|
||||||
|
>
|
||||||
Load More
|
Load More
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
class=${'w3-button w3-theme-d1' + (this.loading ? '' : ' w3-hide')}
|
||||||
|
@click=${this.cancel_load}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
<span
|
||||||
|
>Showing
|
||||||
|
${new Date(
|
||||||
|
this.time_loading
|
||||||
|
? Math.min(this.time_loading[0], this.time_range[0])
|
||||||
|
: this.time_range[0]
|
||||||
|
).toLocaleDateString()}
|
||||||
|
-
|
||||||
|
${new Date(
|
||||||
|
this.time_loading
|
||||||
|
? Math.max(this.time_loading[1], this.time_range[1])
|
||||||
|
: this.time_range[1]
|
||||||
|
).toLocaleDateString()}.</span
|
||||||
|
>
|
||||||
</p>
|
</p>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
return html`
|
return cache(html`
|
||||||
|
<style>
|
||||||
|
${generate_theme()}
|
||||||
|
</style>
|
||||||
|
${this.unread_allowed()
|
||||||
|
? html`<button
|
||||||
|
class="w3-button w3-theme-d1"
|
||||||
|
@click=${this.mark_all_read}
|
||||||
|
>
|
||||||
|
Mark All Read
|
||||||
|
</button>`
|
||||||
|
: undefined}
|
||||||
|
${this.render_close_chat_button()}
|
||||||
<tf-news
|
<tf-news
|
||||||
id="news"
|
id="news"
|
||||||
whoami=${this.whoami}
|
whoami=${this.whoami}
|
||||||
@@ -202,9 +553,14 @@ class TfTabNewsFeedElement extends LitElement {
|
|||||||
.following=${this.following}
|
.following=${this.following}
|
||||||
.drafts=${this.drafts}
|
.drafts=${this.drafts}
|
||||||
.expanded=${this.expanded}
|
.expanded=${this.expanded}
|
||||||
|
hash=${this.hash}
|
||||||
|
channel=${this.channel()}
|
||||||
|
channel_unread=${this.channels_unread?.[this.channel()]}
|
||||||
|
.recent_reactions=${this.recent_reactions}
|
||||||
|
@mark_all_read=${this.mark_all_read}
|
||||||
></tf-news>
|
></tf-news>
|
||||||
${more}
|
${more}
|
||||||
`;
|
`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,13 @@
|
|||||||
import {LitElement, html, unsafeHTML, until} from './lit-all.min.js';
|
import {
|
||||||
|
LitElement,
|
||||||
|
cache,
|
||||||
|
keyed,
|
||||||
|
html,
|
||||||
|
unsafeHTML,
|
||||||
|
until,
|
||||||
|
} from './lit-all.min.js';
|
||||||
import * as tfrpc from '/static/tfrpc.js';
|
import * as tfrpc from '/static/tfrpc.js';
|
||||||
import {styles} from './tf-styles.js';
|
import {styles, generate_theme} from './tf-styles.js';
|
||||||
|
|
||||||
class TfTabNewsElement extends LitElement {
|
class TfTabNewsElement extends LitElement {
|
||||||
static get properties() {
|
static get properties() {
|
||||||
@@ -8,11 +15,21 @@ class TfTabNewsElement extends LitElement {
|
|||||||
whoami: {type: String},
|
whoami: {type: String},
|
||||||
users: {type: Object},
|
users: {type: Object},
|
||||||
hash: {type: String},
|
hash: {type: String},
|
||||||
unread: {type: Array},
|
|
||||||
following: {type: Array},
|
following: {type: Array},
|
||||||
drafts: {type: Object},
|
drafts: {type: Object},
|
||||||
expanded: {type: Object},
|
expanded: {type: Object},
|
||||||
loading: {type: Boolean},
|
loading: {type: Boolean},
|
||||||
|
channels: {type: Array},
|
||||||
|
channels_unread: {type: Object},
|
||||||
|
channels_latest: {type: Object},
|
||||||
|
connections: {type: Array},
|
||||||
|
private_messages: {type: Array},
|
||||||
|
grouped_private_messages: {type: Object},
|
||||||
|
visible_private_messages: {type: Object},
|
||||||
|
recent_reactions: {type: Array},
|
||||||
|
peer_exchange: {type: Boolean},
|
||||||
|
is_administrator: {type: Boolean},
|
||||||
|
stay_connected: {type: Boolean},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -24,14 +41,19 @@ class TfTabNewsElement extends LitElement {
|
|||||||
this.whoami = null;
|
this.whoami = null;
|
||||||
this.users = {};
|
this.users = {};
|
||||||
this.hash = '#';
|
this.hash = '#';
|
||||||
this.unread = [];
|
|
||||||
this.following = [];
|
this.following = [];
|
||||||
this.cache = {};
|
this.cache = {};
|
||||||
this.drafts = {};
|
this.drafts = {};
|
||||||
this.expanded = {};
|
this.expanded = {};
|
||||||
|
this.channels_unread = {};
|
||||||
|
this.channels_latest = {};
|
||||||
|
this.channels = [];
|
||||||
|
this.connections = [];
|
||||||
|
this.recent_reactions = [];
|
||||||
tfrpc.rpc.localStorageGet('drafts').then(function (d) {
|
tfrpc.rpc.localStorageGet('drafts').then(function (d) {
|
||||||
self.drafts = JSON.parse(d || '{}');
|
self.drafts = JSON.parse(d || '{}');
|
||||||
});
|
});
|
||||||
|
this.check_peer_exchange();
|
||||||
}
|
}
|
||||||
|
|
||||||
connectedCallback() {
|
connectedCallback() {
|
||||||
@@ -44,37 +66,19 @@ class TfTabNewsElement extends LitElement {
|
|||||||
document.body.removeEventListener('keypress', this.on_keypress.bind(this));
|
document.body.removeEventListener('keypress', this.on_keypress.bind(this));
|
||||||
}
|
}
|
||||||
|
|
||||||
show_more() {
|
async check_peer_exchange() {
|
||||||
let unread = this.unread;
|
if (await tfrpc.rpc.isAdministrator()) {
|
||||||
let news = this.shadowRoot?.getElementById('news');
|
this.peer_exchange = await tfrpc.rpc.globalSettingsGet('peer_exchange');
|
||||||
if (news) {
|
} else {
|
||||||
console.log('injecting messages', news.messages);
|
this.peer_exchange = undefined;
|
||||||
news.add_messages(
|
|
||||||
Object.values(Object.fromEntries(this.unread.map((x) => [x.id, x])))
|
|
||||||
);
|
|
||||||
this.dispatchEvent(new CustomEvent('refresh'));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
new_messages_text() {
|
load_latest() {
|
||||||
if (!this.unread?.length) {
|
let news = this.shadowRoot?.getElementById('news');
|
||||||
return 'No new messages.';
|
if (news) {
|
||||||
|
news.load_latest();
|
||||||
}
|
}
|
||||||
let counts = {};
|
|
||||||
for (let message of this.unread) {
|
|
||||||
let type = 'private';
|
|
||||||
try {
|
|
||||||
type = JSON.parse(message.content).type || type;
|
|
||||||
} catch {}
|
|
||||||
counts[type] = (counts[type] || 0) + 1;
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
'↻ Show New: ' +
|
|
||||||
Object.keys(counts)
|
|
||||||
.sort()
|
|
||||||
.map((x) => counts[x].toString() + ' ' + x + 's')
|
|
||||||
.join(', ')
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
draft(event) {
|
draft(event) {
|
||||||
@@ -106,34 +110,330 @@ class TfTabNewsElement extends LitElement {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
unread_status(channel) {
|
||||||
|
if (channel === undefined) {
|
||||||
|
if (
|
||||||
|
Object.keys(this.channels_unread).some((x) => this.unread_status(x))
|
||||||
|
) {
|
||||||
|
return '✉️ ';
|
||||||
|
}
|
||||||
|
} else if (channel?.startsWith('🔐')) {
|
||||||
|
let key = JSON.stringify(channel.substring('🔐'.length).split(','));
|
||||||
|
if (this.grouped_private_messages?.[key]) {
|
||||||
|
let grouped_latest = Math.max(
|
||||||
|
...this.grouped_private_messages?.[key]?.map((x) => x.rowid)
|
||||||
|
);
|
||||||
|
if (
|
||||||
|
this.channels_unread[channel] === undefined ||
|
||||||
|
grouped_latest > this.channels_unread[channel]
|
||||||
|
) {
|
||||||
|
return '✉️ ';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (
|
||||||
|
this.channels_latest[channel] &&
|
||||||
|
this.channels_latest[channel] > 0 &&
|
||||||
|
(this.channels_unread[channel] === undefined ||
|
||||||
|
this.channels_unread[channel] <= this.channels_latest[channel])
|
||||||
|
) {
|
||||||
|
return '✉️ ';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
show_sidebar() {
|
||||||
|
this.renderRoot.getElementById('sidebar').style.display = 'block';
|
||||||
|
this.renderRoot.getElementById('sidebar_overlay').style.display = 'block';
|
||||||
|
}
|
||||||
|
|
||||||
|
hide_sidebar() {
|
||||||
|
this.renderRoot.getElementById('sidebar').style.display = 'none';
|
||||||
|
this.renderRoot.getElementById('sidebar_overlay').style.display = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
async channel_toggle_subscribed() {
|
||||||
|
let channel = this.hash.substring(2);
|
||||||
|
let subscribed = this.channels.indexOf(channel) != -1;
|
||||||
|
subscribed = !subscribed;
|
||||||
|
|
||||||
|
await tfrpc.rpc.appendMessage(this.whoami, {
|
||||||
|
type: 'channel',
|
||||||
|
channel: channel,
|
||||||
|
subscribed: subscribed,
|
||||||
|
});
|
||||||
|
if (subscribed) {
|
||||||
|
this.channels = [].concat([channel], this.channels).sort();
|
||||||
|
} else {
|
||||||
|
this.channels = this.channels.filter((x) => x != channel);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
channel() {
|
||||||
|
return this.hash.startsWith('##') ? this.hash.substring(2) : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
compare_follows(a, b) {
|
||||||
|
return b[1].ts > a[1].ts ? 1 : b[1].ts < a[1].ts ? -1 : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
suggested_follows() {
|
||||||
|
/*
|
||||||
|
** Filter out people who have used future timestamps so that they aren't
|
||||||
|
** pinned at the top.
|
||||||
|
*/
|
||||||
|
let self = this;
|
||||||
|
let now = new Date().valueOf();
|
||||||
|
return Object.entries(this.users)
|
||||||
|
.filter((x) => x[1].ts < now)
|
||||||
|
.filter((x) => x[1].follow_depth > 1)
|
||||||
|
.sort(self.compare_follows)
|
||||||
|
.slice(0, 8)
|
||||||
|
.map((x) => x[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
async enable_peer_exchange() {
|
||||||
|
await tfrpc.rpc.globalSettingsSet('peer_exchange', true);
|
||||||
|
await this.check_peer_exchange();
|
||||||
|
}
|
||||||
|
|
||||||
|
is_loading() {
|
||||||
|
return this.shadowRoot?.getElementById('news')?.loading;
|
||||||
|
}
|
||||||
|
|
||||||
|
render_sidebar() {
|
||||||
|
return html`
|
||||||
|
<div
|
||||||
|
class="w3-sidebar w3-bar-block w3-theme-d1 w3-collapse w3-animate-left"
|
||||||
|
style="width: 2in; left: 0; z-index: 5; box-sizing: border-box; top: 0"
|
||||||
|
id="sidebar"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="w3-right w3-button w3-hide-large"
|
||||||
|
@click=${this.hide_sidebar}
|
||||||
|
>
|
||||||
|
×
|
||||||
|
</div>
|
||||||
|
${this.hash.startsWith('##') &&
|
||||||
|
this.channels.indexOf(this.hash.substring(2)) == -1
|
||||||
|
? html`
|
||||||
|
<div class="w3-bar-item w3-theme-d2">Viewing</div>
|
||||||
|
<a
|
||||||
|
href="#"
|
||||||
|
class="w3-bar-item w3-button"
|
||||||
|
style="font-weight: bold"
|
||||||
|
>${this.hash.substring(1)}</a
|
||||||
|
>
|
||||||
|
`
|
||||||
|
: undefined}
|
||||||
|
<h4 class="w3-bar-item w3-theme-d2">Channels</h4>
|
||||||
|
<a
|
||||||
|
href="#"
|
||||||
|
class="w3-bar-item w3-button"
|
||||||
|
style=${this.hash == '#' ? 'font-weight: bold' : undefined}
|
||||||
|
>${this.unread_status('')}general</a
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
href="#@"
|
||||||
|
class="w3-bar-item w3-button"
|
||||||
|
style=${this.hash == '#@' ? 'font-weight: bold' : undefined}
|
||||||
|
>${this.unread_status('@')}@mentions</a
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
href="#👍"
|
||||||
|
class="w3-bar-item w3-button"
|
||||||
|
style=${this.hash == '#👍' ? 'font-weight: bold' : undefined}
|
||||||
|
>${this.unread_status('👍')}👍votes</a
|
||||||
|
>
|
||||||
|
${Object.keys(this?.visible_private_messages ?? [])
|
||||||
|
?.sort()
|
||||||
|
?.map(
|
||||||
|
(key) => html`
|
||||||
|
<a
|
||||||
|
href=${'#🔐' + JSON.parse(key).join(',')}
|
||||||
|
class="w3-bar-item w3-button"
|
||||||
|
style=${this.hash == '#🔐' + JSON.parse(key).join(',')
|
||||||
|
? 'font-weight: bold'
|
||||||
|
: undefined}
|
||||||
|
>${this.unread_status('🔐' + JSON.parse(key).join(','))}
|
||||||
|
${(key != '[]' ? JSON.parse(key) : [this.whoami]).map(
|
||||||
|
(id) => html`
|
||||||
|
<tf-user
|
||||||
|
id=${id}
|
||||||
|
nolink="true"
|
||||||
|
.users=${this.users}
|
||||||
|
></tf-user>
|
||||||
|
`
|
||||||
|
)}</a
|
||||||
|
>
|
||||||
|
`
|
||||||
|
)}
|
||||||
|
${Object.keys(this.drafts)
|
||||||
|
.sort()
|
||||||
|
.map(
|
||||||
|
(x) => html`
|
||||||
|
<a
|
||||||
|
href=${'#' + encodeURIComponent(x)}
|
||||||
|
class="w3-bar-item w3-button"
|
||||||
|
style="text-wrap: nowrap; text-overflow: ellipsis"
|
||||||
|
>📝 ${this.drafts[x]?.text ?? x}</a
|
||||||
|
>
|
||||||
|
`
|
||||||
|
)}
|
||||||
|
${this.channels.map(
|
||||||
|
(x) => html`
|
||||||
|
<a
|
||||||
|
href=${'#' + encodeURIComponent('#' + x)}
|
||||||
|
class="w3-bar-item w3-button"
|
||||||
|
style=${this.hash == '##' + x ? 'font-weight: bold' : undefined}
|
||||||
|
>${this.unread_status(x)}#${x}</a
|
||||||
|
>
|
||||||
|
`
|
||||||
|
)}
|
||||||
|
|
||||||
|
<a class="w3-bar-item w3-theme-d2 w3-button" href="#connections">
|
||||||
|
<h4 style="margin: 0">Connections</h4>
|
||||||
|
</a>
|
||||||
|
${this.connections?.filter((x) => x.id)?.length == 0
|
||||||
|
? html`
|
||||||
|
<button
|
||||||
|
class=${'w3-bar-item w3-button' +
|
||||||
|
(this.connections?.some((x) => x.flags.one_shot)
|
||||||
|
? ' w3-spin'
|
||||||
|
: '')}
|
||||||
|
@click=${() =>
|
||||||
|
this.dispatchEvent(
|
||||||
|
new Event('refresh', {bubbles: true, composed: true})
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
↻ Sync now
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="w3-bar-item w3-button w3-ripple"
|
||||||
|
@click=${() =>
|
||||||
|
this.dispatchEvent(
|
||||||
|
new Event('toggle_stay_connected', {
|
||||||
|
bubbles: true,
|
||||||
|
composed: true,
|
||||||
|
})
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<span style="display: inline-block; width: 1.8em"
|
||||||
|
>${this.stay_connected ? '🔗' : '⛓️💥'}</span
|
||||||
|
>
|
||||||
|
${this.stay_connected ? 'Online mode' : 'Passive mode'}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class=${'w3-bar-item w3-button w3-border w3-leftbar w3-rightbar' +
|
||||||
|
(this.peer_exchange !== false ? ' w3-hide' : '')}
|
||||||
|
@click=${this.enable_peer_exchange}
|
||||||
|
>
|
||||||
|
🔍🌐 Use publicly advertised peers
|
||||||
|
</button>
|
||||||
|
`
|
||||||
|
: undefined}
|
||||||
|
${this.connections
|
||||||
|
.filter((x) => x.id)
|
||||||
|
.map(
|
||||||
|
(x) => html`
|
||||||
|
<tf-user
|
||||||
|
class="w3-bar-item"
|
||||||
|
style=${x.destroy_reason
|
||||||
|
? 'border-left: 4px solid red; border-right: 4px solid red'
|
||||||
|
: x.connected
|
||||||
|
? x.flags?.one_shot
|
||||||
|
? 'border-left: 4px solid blue; border-right: 4px solid blue'
|
||||||
|
: 'border-left: 4px solid green; border-right: 4px solid green'
|
||||||
|
: ''}
|
||||||
|
id=${x.id}
|
||||||
|
fallback_name=${x.host}
|
||||||
|
.users=${this.users}
|
||||||
|
></tf-user>
|
||||||
|
`
|
||||||
|
)}
|
||||||
|
<h4 class="w3-bar-item w3-theme-d2">Suggested Follows</h4>
|
||||||
|
${this.suggested_follows().map(
|
||||||
|
(x) => html`
|
||||||
|
<tf-user
|
||||||
|
class="w3-bar-item"
|
||||||
|
style="max-width: 100%"
|
||||||
|
id=${x}
|
||||||
|
.users=${this.users}
|
||||||
|
></tf-user>
|
||||||
|
`
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="w3-overlay"
|
||||||
|
id="sidebar_overlay"
|
||||||
|
@click=${this.hide_sidebar}
|
||||||
|
></div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
let profile = this.hash.startsWith('#@')
|
let profile =
|
||||||
? html`<tf-profile
|
this.hash.startsWith('#@') && this.hash != '#@'
|
||||||
|
? keyed(
|
||||||
|
this.hash.substring(1),
|
||||||
|
html`<tf-profile
|
||||||
|
class="tf-profile"
|
||||||
id=${this.hash.substring(1)}
|
id=${this.hash.substring(1)}
|
||||||
whoami=${this.whoami}
|
whoami=${this.whoami}
|
||||||
.users=${this.users}
|
.users=${this.users}
|
||||||
></tf-profile>`
|
></tf-profile>`
|
||||||
|
)
|
||||||
: undefined;
|
: undefined;
|
||||||
let edit_profile;
|
let edit_profile;
|
||||||
if (!this.loading &&
|
if (
|
||||||
|
!this.loading &&
|
||||||
this.users[this.whoami]?.name === undefined &&
|
this.users[this.whoami]?.name === undefined &&
|
||||||
this.hash.substring(1) != this.whoami) {
|
this.hash.substring(1) != this.whoami
|
||||||
edit_profile = html`
|
) {
|
||||||
<div class="w3-panel w3-padding w3-round w3-card-4 w3-theme-l3">
|
edit_profile = html` <div
|
||||||
ℹ️ Follow your identity link ☝️ above to edit your profile and set your name.
|
class="w3-panel w3-padding w3-round w3-card-4 w3-theme-l3"
|
||||||
|
>
|
||||||
|
ℹ️ Follow your identity link ☝️ above to edit your profile and set your
|
||||||
|
name.
|
||||||
</div>`;
|
</div>`;
|
||||||
}
|
}
|
||||||
return html`
|
return cache(html`
|
||||||
<p class="w3-bar">
|
<style>
|
||||||
<button
|
${generate_theme()}
|
||||||
class="w3-bar-item w3-button w3-theme-d1"
|
</style>
|
||||||
@click=${this.show_more}
|
${this.render_sidebar()}
|
||||||
|
<div
|
||||||
|
style="margin-left: 2in; padding: 0px; top: 0; height: 100vh; max-height: 100%; overflow: auto; contain: layout"
|
||||||
|
id="main"
|
||||||
|
class="w3-main"
|
||||||
>
|
>
|
||||||
${this.new_messages_text()}
|
<div style="padding: 8px">
|
||||||
|
<p>
|
||||||
|
${this.hash.startsWith('##')
|
||||||
|
? html`
|
||||||
|
<button
|
||||||
|
class="w3-button w3-theme-d1"
|
||||||
|
@click=${this.channel_toggle_subscribed}
|
||||||
|
>
|
||||||
|
${this.channels.indexOf(this.hash.substring(2)) != -1
|
||||||
|
? 'Unsubscribe from #'
|
||||||
|
: 'Subscribe to #'}${this.hash.substring(2)}
|
||||||
</button>
|
</button>
|
||||||
|
`
|
||||||
|
: undefined}
|
||||||
</p>
|
</p>
|
||||||
<div>
|
<div>
|
||||||
Welcome, <tf-user id=${this.whoami} .users=${this.users}></tf-user>!
|
<div
|
||||||
|
id="show_sidebar"
|
||||||
|
class="w3-button w3-hide-large"
|
||||||
|
@click=${this.show_sidebar}
|
||||||
|
>
|
||||||
|
${this.unread_status()}☰
|
||||||
|
</div>
|
||||||
|
<span
|
||||||
|
style="display: inline-block; width: 100%; max-width: 100%; white-space: nowrap; overflow: hidden"
|
||||||
|
>
|
||||||
|
Welcome,
|
||||||
|
<tf-user id=${this.whoami} .users=${this.users}></tf-user>!
|
||||||
|
</span>
|
||||||
${edit_profile}
|
${edit_profile}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
@@ -143,6 +443,10 @@ class TfTabNewsElement extends LitElement {
|
|||||||
.users=${this.users}
|
.users=${this.users}
|
||||||
.drafts=${this.drafts}
|
.drafts=${this.drafts}
|
||||||
@tf-draft=${this.draft}
|
@tf-draft=${this.draft}
|
||||||
|
.channel=${this.channel()}
|
||||||
|
.recipients=${this.hash.startsWith('#🔐')
|
||||||
|
? this.hash.substring('#🔐'.length).split(',')
|
||||||
|
: undefined}
|
||||||
></tf-compose>
|
></tf-compose>
|
||||||
</div>
|
</div>
|
||||||
${profile}
|
${profile}
|
||||||
@@ -156,8 +460,15 @@ class TfTabNewsElement extends LitElement {
|
|||||||
.expanded=${this.expanded}
|
.expanded=${this.expanded}
|
||||||
@tf-draft=${this.draft}
|
@tf-draft=${this.draft}
|
||||||
@tf-expand=${this.on_expand}
|
@tf-expand=${this.on_expand}
|
||||||
|
.channels_unread=${this.channels_unread}
|
||||||
|
.channels_latest=${this.channels_latest}
|
||||||
|
.private_messages=${this.private_messages}
|
||||||
|
.grouped_private_messages=${this.grouped_private_messages}
|
||||||
|
.recent_reactions=${this.recent_reactions}
|
||||||
></tf-tab-news-feed>
|
></tf-tab-news-feed>
|
||||||
`;
|
</div>
|
||||||
|
</div>
|
||||||
|
`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,136 +0,0 @@
|
|||||||
import {LitElement, html, unsafeHTML} from './lit-all.min.js';
|
|
||||||
import * as tfrpc from '/static/tfrpc.js';
|
|
||||||
import {styles} from './tf-styles.js';
|
|
||||||
|
|
||||||
class TfTabQueryElement extends LitElement {
|
|
||||||
static get properties() {
|
|
||||||
return {
|
|
||||||
whoami: {type: String},
|
|
||||||
users: {type: Object},
|
|
||||||
following: {type: Array},
|
|
||||||
query: {type: String},
|
|
||||||
expanded: {type: Object},
|
|
||||||
results: {type: Array},
|
|
||||||
error: {type: Object},
|
|
||||||
duration: {type: Number},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
static styles = styles;
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
super();
|
|
||||||
let self = this;
|
|
||||||
this.whoami = null;
|
|
||||||
this.users = {};
|
|
||||||
this.following = [];
|
|
||||||
this.expanded = {};
|
|
||||||
this.duration = undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
async search(query) {
|
|
||||||
console.log('Searching...', this.whoami, query);
|
|
||||||
this.results = [];
|
|
||||||
this.error = undefined;
|
|
||||||
this.duration = undefined;
|
|
||||||
let search = this.renderRoot.getElementById('search');
|
|
||||||
if (search) {
|
|
||||||
search.value = query;
|
|
||||||
search.focus();
|
|
||||||
}
|
|
||||||
await tfrpc.rpc.setHash('#sql=' + encodeURIComponent(query));
|
|
||||||
let start_time = new Date();
|
|
||||||
try {
|
|
||||||
this.results = await tfrpc.rpc.query(query, []);
|
|
||||||
} catch (error) {
|
|
||||||
this.error = error;
|
|
||||||
}
|
|
||||||
let end_time = new Date();
|
|
||||||
this.duration = (end_time - start_time).valueOf();
|
|
||||||
console.log('Done.');
|
|
||||||
search = this.renderRoot.getElementById('search');
|
|
||||||
if (search) {
|
|
||||||
search.value = query;
|
|
||||||
search.focus();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
search_keydown(event) {
|
|
||||||
if (event.keyCode == 13 && event.ctrlKey) {
|
|
||||||
this.query = this.renderRoot.getElementById('search').value;
|
|
||||||
event.preventDefault();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
on_expand(event) {
|
|
||||||
if (event.detail.expanded) {
|
|
||||||
let expand = {};
|
|
||||||
expand[event.detail.id] = true;
|
|
||||||
this.expanded = Object.assign({}, this.expanded, expand);
|
|
||||||
} else {
|
|
||||||
delete this.expanded[event.detail.id];
|
|
||||||
this.expanded = Object.assign({}, this.expanded);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
render_results() {
|
|
||||||
if (!this.results?.length) {
|
|
||||||
return html`<div>No results.</div>`;
|
|
||||||
} else {
|
|
||||||
let keys = Object.keys(this.results[0]).sort();
|
|
||||||
return html`<table style="width: 100%; max-width: 100%">
|
|
||||||
<tr>
|
|
||||||
${keys.map((key) => html`<th>${key}</th>`)}
|
|
||||||
</tr>
|
|
||||||
${this.results.map(
|
|
||||||
(row) =>
|
|
||||||
html`<tr>
|
|
||||||
${keys.map((key) => html`<td>${row[key]}</td>`)}
|
|
||||||
</tr>`
|
|
||||||
)}
|
|
||||||
</table>`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
render_error() {
|
|
||||||
if (this.error) {
|
|
||||||
return html`<h2 style="color: red">${this.error.message}</h2>
|
|
||||||
<pre style="color: red">${this.error.stack}</pre>`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
if (this.query !== this.last_query) {
|
|
||||||
this.last_query = this.query;
|
|
||||||
this.search(this.query);
|
|
||||||
}
|
|
||||||
let self = this;
|
|
||||||
return html`
|
|
||||||
<div style="display: flex; flex-direction: row; gap: 4px">
|
|
||||||
<textarea
|
|
||||||
id="search"
|
|
||||||
rows="8"
|
|
||||||
class="w3-input w3-theme-d1"
|
|
||||||
style="flex: 1; resize: vertical"
|
|
||||||
@keydown=${this.search_keydown}
|
|
||||||
>
|
|
||||||
${this.query}</textarea
|
|
||||||
>
|
|
||||||
<button
|
|
||||||
class="w3-button w3-theme-d1"
|
|
||||||
@click=${(event) =>
|
|
||||||
self.search(self.renderRoot.getElementById('search').value)}
|
|
||||||
>
|
|
||||||
Execute
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div ?hidden=${this.duration === undefined}>
|
|
||||||
Took ${this.duration / 1000.0} seconds.
|
|
||||||
</div>
|
|
||||||
<div ?hidden=${this.duration !== undefined}>Executing...</div>
|
|
||||||
${this.render_error()} ${this.render_results()}
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
customElements.define('tf-tab-query', TfTabQueryElement);
|
|
||||||
@@ -1,15 +1,19 @@
|
|||||||
import {LitElement, html, unsafeHTML} from './lit-all.min.js';
|
import {LitElement, html, unsafeHTML} from './lit-all.min.js';
|
||||||
import * as tfrpc from '/static/tfrpc.js';
|
import * as tfrpc from '/static/tfrpc.js';
|
||||||
import {styles} from './tf-styles.js';
|
import {styles, generate_theme} from './tf-styles.js';
|
||||||
|
|
||||||
class TfTabSearchElement extends LitElement {
|
class TfTabSearchElement extends LitElement {
|
||||||
static get properties() {
|
static get properties() {
|
||||||
return {
|
return {
|
||||||
|
drafts: {type: Object},
|
||||||
whoami: {type: String},
|
whoami: {type: String},
|
||||||
users: {type: Object},
|
users: {type: Object},
|
||||||
following: {type: Array},
|
following: {type: Array},
|
||||||
query: {type: String},
|
query: {type: String},
|
||||||
expanded: {type: Object},
|
expanded: {type: Object},
|
||||||
|
messages: {type: Array},
|
||||||
|
results: {type: Array},
|
||||||
|
error: {type: Object},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -22,6 +26,10 @@ class TfTabSearchElement extends LitElement {
|
|||||||
this.users = {};
|
this.users = {};
|
||||||
this.following = [];
|
this.following = [];
|
||||||
this.expanded = {};
|
this.expanded = {};
|
||||||
|
this.drafts = {};
|
||||||
|
tfrpc.rpc.localStorageGet('drafts').then(function (d) {
|
||||||
|
self.drafts = JSON.parse(d || '{}');
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async search(query) {
|
async search(query) {
|
||||||
@@ -33,6 +41,21 @@ class TfTabSearchElement extends LitElement {
|
|||||||
search.select();
|
search.select();
|
||||||
}
|
}
|
||||||
await tfrpc.rpc.setHash('#q=' + encodeURIComponent(query));
|
await tfrpc.rpc.setHash('#q=' + encodeURIComponent(query));
|
||||||
|
this.error = undefined;
|
||||||
|
this.results = [];
|
||||||
|
this.messages = [];
|
||||||
|
if (query.startsWith('sql:')) {
|
||||||
|
this.messages = [];
|
||||||
|
try {
|
||||||
|
this.results = await tfrpc.rpc.query(
|
||||||
|
query.substring('sql:'.length),
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
this.results = [];
|
||||||
|
this.error = e;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
let results = await tfrpc.rpc.query(
|
let results = await tfrpc.rpc.query(
|
||||||
`
|
`
|
||||||
SELECT messages.id, messages.previous, messages.author, messages.sequence, messages.timestamp, messages.hash, json(messages.content) AS content, messages.signature
|
SELECT messages.id, messages.previous, messages.author, messages.sequence, messages.timestamp, messages.hash, json(messages.content) AS content, messages.signature
|
||||||
@@ -50,7 +73,8 @@ class TfTabSearchElement extends LitElement {
|
|||||||
search.focus();
|
search.focus();
|
||||||
search.select();
|
search.select();
|
||||||
}
|
}
|
||||||
this.renderRoot.getElementById('news').messages = results;
|
this.messages = results;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
search_keydown(event) {
|
search_keydown(event) {
|
||||||
@@ -70,6 +94,51 @@ class TfTabSearchElement extends LitElement {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
draft(event) {
|
||||||
|
let id = event.detail.id || '';
|
||||||
|
let previous = this.drafts[id];
|
||||||
|
if (event.detail.draft !== undefined) {
|
||||||
|
this.drafts[id] = event.detail.draft;
|
||||||
|
} else {
|
||||||
|
delete this.drafts[id];
|
||||||
|
}
|
||||||
|
this.drafts = Object.assign({}, this.drafts);
|
||||||
|
tfrpc.rpc.localStorageSet('drafts', JSON.stringify(this.drafts));
|
||||||
|
}
|
||||||
|
|
||||||
|
render_results() {
|
||||||
|
if (this.error) {
|
||||||
|
return html`<h2 style="color: red">${this.error.message}</h2>
|
||||||
|
<pre style="color: red">${this.error.stack}</pre>`;
|
||||||
|
} else if (this.messages?.length) {
|
||||||
|
return html`<tf-news
|
||||||
|
id="news"
|
||||||
|
whoami=${this.whoami}
|
||||||
|
.messages=${this.messages}
|
||||||
|
.users=${this.users}
|
||||||
|
.expanded=${this.expanded}
|
||||||
|
.drafts=${this.drafts}
|
||||||
|
@tf-expand=${this.on_expand}
|
||||||
|
@tf-draft=${this.draft}
|
||||||
|
></tf-news>`;
|
||||||
|
} else if (this.results?.length) {
|
||||||
|
let keys = Object.keys(this.results[0]).sort();
|
||||||
|
return html`<table style="width: 100%; max-width: 100%">
|
||||||
|
<tr>
|
||||||
|
${keys.map((key) => html`<th>${key}</th>`)}
|
||||||
|
</tr>
|
||||||
|
${this.results.map(
|
||||||
|
(row) =>
|
||||||
|
html`<tr>
|
||||||
|
${keys.map((key) => html`<td>${row[key]}</td>`)}
|
||||||
|
</tr>`
|
||||||
|
)}
|
||||||
|
</table>`;
|
||||||
|
} else {
|
||||||
|
return html`<div>No results.</div>`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
if (this.query !== this.last_query) {
|
if (this.query !== this.last_query) {
|
||||||
this.last_query = this.query;
|
this.last_query = this.query;
|
||||||
@@ -77,11 +146,14 @@ class TfTabSearchElement extends LitElement {
|
|||||||
}
|
}
|
||||||
let self = this;
|
let self = this;
|
||||||
return html`
|
return html`
|
||||||
|
<style>${generate_theme()}</style>
|
||||||
|
<div class="w3-padding">
|
||||||
<div style="display: flex; flex-direction: row; gap: 4px">
|
<div style="display: flex; flex-direction: row; gap: 4px">
|
||||||
<input type="text" class="w3-input w3-theme-d1" id="search" value=${this.query} style="flex: 1" @keydown=${this.search_keydown}></input>
|
<input type="text" class="w3-input w3-theme-d1" id="search" value=${this.query} style="flex: 1" @keydown=${this.search_keydown}></input>
|
||||||
<button class="w3-button w3-theme-d1" @click=${(event) => self.search(self.renderRoot.getElementById('search').value)}>Search</button>
|
<button class="w3-button w3-theme-d1" @click=${(event) => self.search(self.renderRoot.getElementById('search').value)}>Search</button>
|
||||||
</div>
|
</div>
|
||||||
<tf-news id="news" whoami=${this.whoami} .messages=${this.messages} .users=${this.users} .expanded=${this.expanded} @tf-expand=${this.on_expand}></tf-news>
|
${this.render_results()}
|
||||||
|
</div>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import {LitElement, html, unsafeHTML} from './lit-all.min.js';
|
import {LitElement, html, unsafeHTML} from './lit-all.min.js';
|
||||||
import {styles} from './tf-styles.js';
|
import {styles, generate_theme} from './tf-styles.js';
|
||||||
|
|
||||||
class TfTagElement extends LitElement {
|
class TfTagElement extends LitElement {
|
||||||
static get properties() {
|
static get properties() {
|
||||||
@@ -17,11 +17,15 @@ class TfTagElement extends LitElement {
|
|||||||
|
|
||||||
render() {
|
render() {
|
||||||
let number = this.count ? html` (${this.count})` : undefined;
|
let number = this.count ? html` (${this.count})` : undefined;
|
||||||
return html`<a
|
return html`
|
||||||
href="#q=${this.tag}"
|
<style>
|
||||||
style="display: inline-block; margin: 3px; border: 1px solid black; background-color: #444; padding: 4px; border-radius: 3px"
|
${generate_theme()}</style
|
||||||
|
><a
|
||||||
|
href=${'#' + encodeURIComponent(this.tag)}
|
||||||
|
class="w3-tag w3-theme-d1 w3-round-4 w3-button"
|
||||||
>${this.tag}${number}</a
|
>${this.tag}${number}</a
|
||||||
>`;
|
>
|
||||||
|
`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,15 @@
|
|||||||
import {LitElement, html} from './lit-all.min.js';
|
import {LitElement, html} from './lit-all.min.js';
|
||||||
import * as tfrpc from '/static/tfrpc.js';
|
import * as tfrpc from '/static/tfrpc.js';
|
||||||
import {styles} from './tf-styles.js';
|
import {styles, generate_theme} from './tf-styles.js';
|
||||||
|
|
||||||
class TfUserElement extends LitElement {
|
class TfUserElement extends LitElement {
|
||||||
static get properties() {
|
static get properties() {
|
||||||
return {
|
return {
|
||||||
id: {type: String},
|
id: {type: String},
|
||||||
|
fallback_name: {type: String},
|
||||||
|
icon_only: {type: Boolean},
|
||||||
users: {type: Object},
|
users: {type: Object},
|
||||||
|
nolink: {type: Boolean},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -15,32 +18,55 @@ class TfUserElement extends LitElement {
|
|||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
this.id = null;
|
this.id = null;
|
||||||
|
this.fallback_name = null;
|
||||||
|
this.icon_only = false;
|
||||||
this.users = {};
|
this.users = {};
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
let user = this.users[this.id];
|
||||||
|
let shape =
|
||||||
|
user?.follow_depth === undefined || user.follow_depth >= 2
|
||||||
|
? 'w3-circle'
|
||||||
|
: 'w3-round';
|
||||||
|
let image = html`<span
|
||||||
|
class=${'w3-theme-l4 ' + shape}
|
||||||
|
style="display: inline-block; width: 2em; height: 2em; text-align: center; line-height: 2em"
|
||||||
|
>😎</span
|
||||||
|
>`;
|
||||||
let name = this.users?.[this.id]?.name;
|
let name = this.users?.[this.id]?.name;
|
||||||
name =
|
let name_string = name ?? this.fallback_name ?? this.id;
|
||||||
name !== undefined
|
name = this.icon_only
|
||||||
? html`<a target="_top" href=${'#' + this.id}>${name}</a>`
|
? undefined
|
||||||
: html`<a target="_top" href=${'#' + this.id}>${this.id}</a>`;
|
: !this.nolink
|
||||||
|
? html`<a target="_top" href=${'#' + this.id}>${name_string}</a>`
|
||||||
|
: html`<span>${name_string}</span>`;
|
||||||
|
|
||||||
if (this.users[this.id]) {
|
if (user) {
|
||||||
let image = this.users[this.id].image;
|
let image_link = user.image;
|
||||||
image = typeof image == 'string' ? image : image?.link;
|
if (typeof image_link == 'string' && !image_link.startsWith('&')) {
|
||||||
return html` <div style="display: inline-block; font-weight: bold">
|
try {
|
||||||
<img
|
image_link = JSON.parse(image_link)?.link;
|
||||||
style="width: 2em; height: 2em; vertical-align: middle; border-radius: 50%"
|
} catch {}
|
||||||
?hidden=${image === undefined}
|
|
||||||
src="${image ? '/' + image + '/view' : undefined}"
|
|
||||||
/>
|
|
||||||
${name}
|
|
||||||
</div>`;
|
|
||||||
} else {
|
|
||||||
return html` <div style="display: inline-block; font-weight: bold">
|
|
||||||
${name}
|
|
||||||
</div>`;
|
|
||||||
}
|
}
|
||||||
|
if (image_link !== undefined) {
|
||||||
|
image = html`<img
|
||||||
|
class=${'w3-theme-l4 ' + shape}
|
||||||
|
style="width: 2em; height: 2em; vertical-align: middle; object-fit: cover"
|
||||||
|
src="/${image_link}/view"
|
||||||
|
title=${name_string + ' (' + this.id + ')'}
|
||||||
|
/>`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return html` <style>
|
||||||
|
${generate_theme()}
|
||||||
|
</style>
|
||||||
|
<div
|
||||||
|
style=${'display: inline-block; vertical-align: middle; text-wrap: nowrap; max-width: 100%; overflow: hidden; text-overflow: ellipsis' +
|
||||||
|
(this.nolink ? '' : '; font-weight: bold')}
|
||||||
|
>
|
||||||
|
${image} ${name}
|
||||||
|
</div>`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,12 @@ import * as hashtagify from './commonmark-hashtag.js';
|
|||||||
|
|
||||||
const k_code_classes = 'w3-theme-l4 w3-theme-border w3-round';
|
const k_code_classes = 'w3-theme-l4 w3-theme-border w3-round';
|
||||||
|
|
||||||
|
var reUnsafeProtocol = /^javascript:|vbscript:|file:|data:/i;
|
||||||
|
var reSafeDataProtocol = /^data:image\/(?:png|gif|jpeg|webp)/i;
|
||||||
|
var potentiallyUnsafe = function (url) {
|
||||||
|
return reUnsafeProtocol.test(url) && !reSafeDataProtocol.test(url);
|
||||||
|
};
|
||||||
|
|
||||||
function image(node, entering) {
|
function image(node, entering) {
|
||||||
if (
|
if (
|
||||||
node.firstChild?.type === 'text' &&
|
node.firstChild?.type === 'text' &&
|
||||||
@@ -44,9 +50,9 @@ function image(node, entering) {
|
|||||||
'</div>'
|
'</div>'
|
||||||
);
|
);
|
||||||
if (this.options.safe && potentiallyUnsafe(node.destination)) {
|
if (this.options.safe && potentiallyUnsafe(node.destination)) {
|
||||||
this.lit('<img src="" alt="');
|
this.lit('<img src="" title="');
|
||||||
} else {
|
} else {
|
||||||
this.lit('<img src="' + this.esc(node.destination) + '" alt="');
|
this.lit('<img src="' + this.esc(node.destination) + '" title="');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.disableTags += 1;
|
this.disableTags += 1;
|
||||||
@@ -81,8 +87,8 @@ function attrs(node) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function markdown(md) {
|
export function markdown(md) {
|
||||||
let reader = new commonmark.Parser({safe: true});
|
let reader = new commonmark.Parser();
|
||||||
let writer = new commonmark.HtmlRenderer();
|
let writer = new commonmark.HtmlRenderer({safe: true});
|
||||||
writer.image = image;
|
writer.image = image;
|
||||||
writer.code = code;
|
writer.code = code;
|
||||||
writer.attrs = attrs;
|
writer.attrs = attrs;
|
||||||
@@ -98,12 +104,12 @@ export function markdown(md) {
|
|||||||
node.destination.startsWith('@') &&
|
node.destination.startsWith('@') &&
|
||||||
node.destination.endsWith('.ed25519')
|
node.destination.endsWith('.ed25519')
|
||||||
) {
|
) {
|
||||||
node.destination = '#' + node.destination;
|
node.destination = '#' + encodeURIComponent(node.destination);
|
||||||
} else if (
|
} else if (
|
||||||
node.destination.startsWith('%') &&
|
node.destination.startsWith('%') &&
|
||||||
node.destination.endsWith('.sha256')
|
node.destination.endsWith('.sha256')
|
||||||
) {
|
) {
|
||||||
node.destination = '#' + node.destination;
|
node.destination = '#' + encodeURIComponent(node.destination);
|
||||||
} else if (
|
} else if (
|
||||||
node.destination.startsWith('&') &&
|
node.destination.startsWith('&') &&
|
||||||
node.destination.endsWith('.sha256')
|
node.destination.endsWith('.sha256')
|
||||||
|
|||||||
@@ -482,16 +482,7 @@ class TributeRange {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getDocument() {
|
getDocument() {
|
||||||
let iframe;
|
return document;
|
||||||
if (this.tribute.current.collection) {
|
|
||||||
iframe = this.tribute.current.collection.iframe;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!iframe) {
|
|
||||||
return document
|
|
||||||
}
|
|
||||||
|
|
||||||
return iframe.contentWindow.document
|
|
||||||
}
|
}
|
||||||
|
|
||||||
positionMenuAtCaret(scrollTo) {
|
positionMenuAtCaret(scrollTo) {
|
||||||
@@ -653,8 +644,8 @@ class TributeRange {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getWindowSelection() {
|
getWindowSelection() {
|
||||||
if (this.tribute.collection.iframe) {
|
if (this.tribute.collection[0].iframe?.getSelection) {
|
||||||
return this.tribute.collection.iframe.contentWindow.getSelection()
|
return this.tribute.collection[0].iframe.getSelection()
|
||||||
}
|
}
|
||||||
|
|
||||||
return window.getSelection()
|
return window.getSelection()
|
||||||
|
|||||||
5
apps/storage.json
Normal file
5
apps/storage.json
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"type": "tildefriends-app",
|
||||||
|
"emoji": "💾",
|
||||||
|
"previous": "&tzZFIe7Y54O4sx1QtAPdemkXh+p5qHXSG/dlS7NP6OQ=.sha256"
|
||||||
|
}
|
||||||
126
apps/storage/app.js
Normal file
126
apps/storage/app.js
Normal file
@@ -0,0 +1,126 @@
|
|||||||
|
async function query(sql, args) {
|
||||||
|
let rows = [];
|
||||||
|
await ssb.sqlAsync(sql, args ?? [], function (row) {
|
||||||
|
rows.push(row);
|
||||||
|
});
|
||||||
|
return rows;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function get_biggest() {
|
||||||
|
return query(`
|
||||||
|
select author, size from messages_stats group by author order by size desc limit 10;
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function get_total() {
|
||||||
|
return (
|
||||||
|
await query(`
|
||||||
|
select sum(length(content)) as size, count(distinct author) as count from messages;
|
||||||
|
`)
|
||||||
|
)[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
async function get_names(identities) {
|
||||||
|
return query(
|
||||||
|
`
|
||||||
|
SELECT author, name FROM (
|
||||||
|
SELECT
|
||||||
|
messages.author,
|
||||||
|
RANK() OVER (PARTITION BY messages.author ORDER BY messages.sequence DESC) AS author_rank,
|
||||||
|
messages.content ->> 'name' AS name
|
||||||
|
FROM messages
|
||||||
|
JOIN json_each(?) AS identities ON identities.value = messages.author
|
||||||
|
WHERE
|
||||||
|
json_extract(messages.content, '$.type') = 'about' AND
|
||||||
|
content ->> 'about' = messages.author AND name IS NOT NULL)
|
||||||
|
WHERE author_rank = 1
|
||||||
|
`,
|
||||||
|
[JSON.stringify(identities)]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function get_most_follows() {
|
||||||
|
return query(`
|
||||||
|
select author, count(*) as count
|
||||||
|
from messages
|
||||||
|
where content ->> 'type' = 'contact' and content ->> 'following' = true
|
||||||
|
group by author
|
||||||
|
order by count desc
|
||||||
|
limit 10;
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function nice_size(bytes) {
|
||||||
|
let value = bytes;
|
||||||
|
let index = 0;
|
||||||
|
let units = ['B', 'kB', 'MB', 'GB'];
|
||||||
|
while (value > 1024 && index < units.length - 1) {
|
||||||
|
value /= 1024;
|
||||||
|
index++;
|
||||||
|
}
|
||||||
|
return `${Math.round(value * 10) / 10} ${units[index]}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
await app.setDocument('<p style="color: #fff">Analyzing feeds...</p>');
|
||||||
|
let most_follows = get_most_follows();
|
||||||
|
let total = await get_total();
|
||||||
|
let identities = await ssb.getAllIdentities();
|
||||||
|
let following1 = await ssb.following(identities, 1);
|
||||||
|
let following2 = await ssb.following(identities, 2);
|
||||||
|
let biggest = await get_biggest();
|
||||||
|
most_follows = await most_follows;
|
||||||
|
let names = await get_names(
|
||||||
|
[].concat(
|
||||||
|
biggest.map((x) => x.author),
|
||||||
|
most_follows.map((x) => x.author)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
names = Object.fromEntries(names.map((x) => [x.author, x.name]));
|
||||||
|
for (let item of biggest) {
|
||||||
|
item.name = names[item.author];
|
||||||
|
item.following =
|
||||||
|
identities.indexOf(item.author) != -1
|
||||||
|
? 0
|
||||||
|
: following1[item.author] !== undefined
|
||||||
|
? 1
|
||||||
|
: following2[item.author] !== undefined
|
||||||
|
? 2
|
||||||
|
: undefined;
|
||||||
|
}
|
||||||
|
for (let item of most_follows) {
|
||||||
|
item.name = names[item.author];
|
||||||
|
}
|
||||||
|
let html = `<body style="color: #000; background-color: #ddd">\n
|
||||||
|
<h1>Storage Summary</h1>
|
||||||
|
<h2>Top Accounts by Size</h2>
|
||||||
|
<ol>`;
|
||||||
|
for (let item of biggest) {
|
||||||
|
html += `<li>
|
||||||
|
<span style="color: #888">${nice_size(item.size)}</span>
|
||||||
|
<a target="_top" href="/~core/ssb/#${encodeURI(item.author)}">${item.name ?? item.author}</a>
|
||||||
|
</li>
|
||||||
|
\n`;
|
||||||
|
}
|
||||||
|
html += `
|
||||||
|
</ol>
|
||||||
|
<h2>Top Accounts by Follows</h2>
|
||||||
|
<ol>`;
|
||||||
|
for (let item of most_follows) {
|
||||||
|
html += `<li>
|
||||||
|
<span style="color: #888">${item.count}</span>
|
||||||
|
${following2[item.author] ? '✅' : '🚫'}
|
||||||
|
<a target="_top" href="/~core/ssb/#${encodeURI(item.author)}">${item.name ?? item.author}</a>
|
||||||
|
</li>
|
||||||
|
\n`;
|
||||||
|
}
|
||||||
|
html += `
|
||||||
|
</ol>
|
||||||
|
<p>Total <span style="color: #888">${nice_size(total.size)}</span> in ${total.count} accounts.</p>
|
||||||
|
`;
|
||||||
|
await app.setDocument(html);
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch(function (e) {
|
||||||
|
print(e);
|
||||||
|
});
|
||||||
4
apps/test.json
Normal file
4
apps/test.json
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"type": "tildefriends-app",
|
||||||
|
"emoji": "📦"
|
||||||
|
}
|
||||||
3
apps/test/app.js
Normal file
3
apps/test/app.js
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
app.setDocument(
|
||||||
|
'<p style="color: #fff">Maybe one day this app will run tests, but for now there is nothing to see here.</p>'
|
||||||
|
);
|
||||||
1
apps/test/hello.txt
Normal file
1
apps/test/hello.txt
Normal file
@@ -0,0 +1 @@
|
|||||||
|
Hello, world!
|
||||||
5
apps/trace.json
Normal file
5
apps/trace.json
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"type": "tildefriends-app",
|
||||||
|
"emoji": "📦",
|
||||||
|
"previous": "&mhBOscDHiJ4VNnod27NOdRVC+4cXYZXIdYjsQBfmTYg=.sha256"
|
||||||
|
}
|
||||||
27
apps/trace/app.js
Normal file
27
apps/trace/app.js
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
async function main() {
|
||||||
|
let speedscope_js = await utf8Decode(
|
||||||
|
getFile('speedscope/speedscope-432XE7GS.js')
|
||||||
|
);
|
||||||
|
speedscope_js = speedscope_js.replace(/alert\(`Cannot load.*?return/, '');
|
||||||
|
app.setDocument(`
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<meta http-equiv="X-UA-Compatible" content="ie=edge">
|
||||||
|
<title>speedscope</title>
|
||||||
|
<link rel="stylesheet" href="speedscope/speedscope-GHPHNKXC.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<script>
|
||||||
|
delete window.localStorage;
|
||||||
|
window.location.hash = '#profileURL=${core.url}../../trace';
|
||||||
|
</script>
|
||||||
|
<script>${speedscope_js}</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
|
||||||
|
main();
|
||||||
BIN
apps/trace/speedscope/SourceCodePro-Regular.ttf-ILST5JV6.woff2
Normal file
BIN
apps/trace/speedscope/SourceCodePro-Regular.ttf-ILST5JV6.woff2
Normal file
Binary file not shown.
2
apps/trace/speedscope/favicon-16x16-V2DMIAZS.js
Normal file
2
apps/trace/speedscope/favicon-16x16-V2DMIAZS.js
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
(()=>{var D="./favicon-16x16-VSI62OPJ.png";})();
|
||||||
|
//# sourceMappingURL=favicon-16x16-V2DMIAZS.js.map
|
||||||
|
Before Width: | Height: | Size: 679 B After Width: | Height: | Size: 679 B |
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.5 KiB |
2
apps/trace/speedscope/favicon-32x32-THY3JDJL.js
Normal file
2
apps/trace/speedscope/favicon-32x32-THY3JDJL.js
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
(()=>{var T="./favicon-32x32-3EB2YCUY.png";})();
|
||||||
|
//# sourceMappingURL=favicon-32x32-THY3JDJL.js.map
|
||||||
BIN
apps/trace/speedscope/favicon-FOKUP5Y5.ico
Normal file
BIN
apps/trace/speedscope/favicon-FOKUP5Y5.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
2
apps/trace/speedscope/favicon-M34RF7BI.js
Normal file
2
apps/trace/speedscope/favicon-M34RF7BI.js
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
(()=>{var m="./favicon-FOKUP5Y5.ico";})();
|
||||||
|
//# sourceMappingURL=favicon-M34RF7BI.js.map
|
||||||
19
apps/trace/speedscope/index.html
Normal file
19
apps/trace/speedscope/index.html
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<meta http-equiv="X-UA-Compatible" content="ie=edge">
|
||||||
|
<title>speedscope</title>
|
||||||
|
<link rel="stylesheet" href="speedscope-GHPHNKXC.css">
|
||||||
|
<link rel="icon" type="image/png" sizes="32x32" href="favicon-32x32-3EB2YCUY.png">
|
||||||
|
<link rel="icon" type="image/png" sizes="16x16" href="favicon-16x16-VSI62OPJ.png">
|
||||||
|
<link rel="icon" type="image/x-icon" href="favicon-FOKUP5Y5.ico">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<script src="speedscope-432XE7GS.js"></script>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
3
apps/trace/speedscope/release.txt
Normal file
3
apps/trace/speedscope/release.txt
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
speedscope@1.24.0
|
||||||
|
Mon Oct 20 18:11:29 PDT 2025
|
||||||
|
fc76932551754a442cd5c4f0afdba28032d14d8a
|
||||||
93
apps/trace/speedscope/source-code-pro.LICENSE.md
Normal file
93
apps/trace/speedscope/source-code-pro.LICENSE.md
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
Copyright 2010-2019 Adobe (http://www.adobe.com/), with Reserved Font Name 'Source'. All Rights Reserved. Source is a trademark of Adobe in the United States and/or other countries.
|
||||||
|
|
||||||
|
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||||
|
|
||||||
|
This license is copied below, and is also available with a FAQ at: http://scripts.sil.org/OFL
|
||||||
|
|
||||||
|
|
||||||
|
-----------------------------------------------------------
|
||||||
|
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||||
|
-----------------------------------------------------------
|
||||||
|
|
||||||
|
PREAMBLE
|
||||||
|
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||||
|
development of collaborative font projects, to support the font creation
|
||||||
|
efforts of academic and linguistic communities, and to provide a free and
|
||||||
|
open framework in which fonts may be shared and improved in partnership
|
||||||
|
with others.
|
||||||
|
|
||||||
|
The OFL allows the licensed fonts to be used, studied, modified and
|
||||||
|
redistributed freely as long as they are not sold by themselves. The
|
||||||
|
fonts, including any derivative works, can be bundled, embedded,
|
||||||
|
redistributed and/or sold with any software provided that any reserved
|
||||||
|
names are not used by derivative works. The fonts and derivatives,
|
||||||
|
however, cannot be released under any other type of license. The
|
||||||
|
requirement for fonts to remain under this license does not apply
|
||||||
|
to any document created using the fonts or their derivatives.
|
||||||
|
|
||||||
|
DEFINITIONS
|
||||||
|
"Font Software" refers to the set of files released by the Copyright
|
||||||
|
Holder(s) under this license and clearly marked as such. This may
|
||||||
|
include source files, build scripts and documentation.
|
||||||
|
|
||||||
|
"Reserved Font Name" refers to any names specified as such after the
|
||||||
|
copyright statement(s).
|
||||||
|
|
||||||
|
"Original Version" refers to the collection of Font Software components as
|
||||||
|
distributed by the Copyright Holder(s).
|
||||||
|
|
||||||
|
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||||
|
or substituting -- in part or in whole -- any of the components of the
|
||||||
|
Original Version, by changing formats or by porting the Font Software to a
|
||||||
|
new environment.
|
||||||
|
|
||||||
|
"Author" refers to any designer, engineer, programmer, technical
|
||||||
|
writer or other person who contributed to the Font Software.
|
||||||
|
|
||||||
|
PERMISSION & CONDITIONS
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining
|
||||||
|
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||||
|
redistribute, and sell modified and unmodified copies of the Font
|
||||||
|
Software, subject to the following conditions:
|
||||||
|
|
||||||
|
1) Neither the Font Software nor any of its individual components,
|
||||||
|
in Original or Modified Versions, may be sold by itself.
|
||||||
|
|
||||||
|
2) Original or Modified Versions of the Font Software may be bundled,
|
||||||
|
redistributed and/or sold with any software, provided that each copy
|
||||||
|
contains the above copyright notice and this license. These can be
|
||||||
|
included either as stand-alone text files, human-readable headers or
|
||||||
|
in the appropriate machine-readable metadata fields within text or
|
||||||
|
binary files as long as those fields can be easily viewed by the user.
|
||||||
|
|
||||||
|
3) No Modified Version of the Font Software may use the Reserved Font
|
||||||
|
Name(s) unless explicit written permission is granted by the corresponding
|
||||||
|
Copyright Holder. This restriction only applies to the primary font name as
|
||||||
|
presented to the users.
|
||||||
|
|
||||||
|
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||||
|
Software shall not be used to promote, endorse or advertise any
|
||||||
|
Modified Version, except to acknowledge the contribution(s) of the
|
||||||
|
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||||
|
permission.
|
||||||
|
|
||||||
|
5) The Font Software, modified or unmodified, in part or in whole,
|
||||||
|
must be distributed entirely under this license, and must not be
|
||||||
|
distributed under any other license. The requirement for fonts to
|
||||||
|
remain under this license does not apply to any document created
|
||||||
|
using the Font Software.
|
||||||
|
|
||||||
|
TERMINATION
|
||||||
|
This license becomes null and void if any of the above conditions are
|
||||||
|
not met.
|
||||||
|
|
||||||
|
DISCLAIMER
|
||||||
|
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||||
|
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||||
|
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||||
|
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||||
|
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||||
|
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||||
|
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||||
|
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||||
189
apps/trace/speedscope/speedscope-432XE7GS.js
Normal file
189
apps/trace/speedscope/speedscope-432XE7GS.js
Normal file
File diff suppressed because one or more lines are too long
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user