Compare commits
	
		
			738 Commits
		
	
	
		
			9c8b922069
			...
			latest_rel
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 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 | 
@@ -1,4 +1,3 @@
 | 
			
		||||
.svn
 | 
			
		||||
db.sqlite
 | 
			
		||||
out/**/*.o
 | 
			
		||||
out/**/*.d
 | 
			
		||||
.git
 | 
			
		||||
db.sqlite*
 | 
			
		||||
out/
 | 
			
		||||
 
 | 
			
		||||
@@ -6,15 +6,51 @@ jobs:
 | 
			
		||||
  Build-All:
 | 
			
		||||
    runs-on: ubuntu-latest
 | 
			
		||||
    container:
 | 
			
		||||
      valid_volumes: ['/opt/keys']
 | 
			
		||||
      image: node:23-bookworm-slim
 | 
			
		||||
      valid_volumes:
 | 
			
		||||
        - '/opt/keys'
 | 
			
		||||
        - '/opt/deps'
 | 
			
		||||
      volumes:
 | 
			
		||||
        - /opt/keys:/opt/keys
 | 
			
		||||
        - /opt/deps:/opt/deps
 | 
			
		||||
    steps:
 | 
			
		||||
      - name: check out code
 | 
			
		||||
      - 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
 | 
			
		||||
      - run: ln -s /opt/keys .keys
 | 
			
		||||
      - 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:
 | 
			
		||||
@@ -23,15 +59,13 @@ jobs:
 | 
			
		||||
      - name: Setup Android SDK
 | 
			
		||||
        uses: android-actions/setup-android@v3
 | 
			
		||||
        with:
 | 
			
		||||
          packages: 'tools platform-tools build-tools;34.0.0 platforms;android-34 ndk;26.3.11579264'
 | 
			
		||||
      - run: sudo apt update && sudo apt install -y doxygen graphviz mingw-w64 libgpgme11 gcc-aarch64-linux-gnu
 | 
			
		||||
      - run: ANDROID_SDK=$HOME/.android/sdk make -j`nproc` all docs
 | 
			
		||||
      - run: docker build .
 | 
			
		||||
      - uses: actions/upload-artifact@v3
 | 
			
		||||
          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:
 | 
			
		||||
          path: |
 | 
			
		||||
            out/TildeFriends-release.fdroid.apk
 | 
			
		||||
            out/winrelease/tildefriends.exe
 | 
			
		||||
            out/tildefriends-x86_64.AppImage
 | 
			
		||||
            out/release/tildefriends.standalone
 | 
			
		||||
            out/armrelease/tildefriends.standalone
 | 
			
		||||
          name: dist
 | 
			
		||||
          path: dist/*
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										4
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						@@ -1,11 +1,13 @@
 | 
			
		||||
build/
 | 
			
		||||
*.core
 | 
			
		||||
db.*
 | 
			
		||||
deps/ios_toolchain/
 | 
			
		||||
deps/ios_toolchain
 | 
			
		||||
deps/macos_toolchain
 | 
			
		||||
deps/openssl/
 | 
			
		||||
dist/
 | 
			
		||||
.flatpak-builder
 | 
			
		||||
.keys
 | 
			
		||||
**/.DS_Store
 | 
			
		||||
logs/
 | 
			
		||||
**/node_modules
 | 
			
		||||
out
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										10
									
								
								.gitmodules
									
									
									
									
										vendored
									
									
								
							
							
						
						@@ -19,13 +19,9 @@
 | 
			
		||||
[submodule "deps/picohttpparser"]
 | 
			
		||||
	path = deps/picohttpparser
 | 
			
		||||
	url = https://github.com/h2o/picohttpparser.git
 | 
			
		||||
[submodule "deps/openssl_src"]
 | 
			
		||||
	path = deps/openssl_src
 | 
			
		||||
	url = https://github.com/openssl/openssl.git
 | 
			
		||||
	shallow = true
 | 
			
		||||
[submodule "deps/c-ares"]
 | 
			
		||||
	path = deps/c-ares
 | 
			
		||||
	url = https://github.com/c-ares/c-ares.git
 | 
			
		||||
[submodule "docs"]
 | 
			
		||||
	path = docs
 | 
			
		||||
	url = https://dev.tildefriends.net/cory/tildefriends.wiki.git
 | 
			
		||||
[submodule "deps/zsign"]
 | 
			
		||||
	path = deps/zsign
 | 
			
		||||
	url = https://github.com/zhlynn/zsign.git
 | 
			
		||||
 
 | 
			
		||||
@@ -1,19 +1,15 @@
 | 
			
		||||
FROM bitnami/minideb:bullseye AS build
 | 
			
		||||
FROM bitnami/minideb:bookworm AS build
 | 
			
		||||
 | 
			
		||||
RUN apt-get update && \
 | 
			
		||||
	apt-get install -y --no-install-recommends \
 | 
			
		||||
		gcc \
 | 
			
		||||
		libc6-dev \
 | 
			
		||||
		libssl-dev \
 | 
			
		||||
		make
 | 
			
		||||
 | 
			
		||||
COPY . /app
 | 
			
		||||
RUN make -C /app -j $(nproc) release
 | 
			
		||||
 | 
			
		||||
FROM bitnami/minideb:bullseye
 | 
			
		||||
RUN apt-get update && \
 | 
			
		||||
	apt-get install -y --no-install-recommends \
 | 
			
		||||
		libssl1.1
 | 
			
		||||
FROM bitnami/minideb:bookworm
 | 
			
		||||
 | 
			
		||||
COPY --from=build /app/out/release/tildefriends /app/out/release/tildefriends
 | 
			
		||||
COPY --from=build /app/apps /app/apps
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										374
									
								
								Doxyfile
									
									
									
									
									
								
							
							
						
						@@ -1,4 +1,4 @@
 | 
			
		||||
# Doxyfile 1.9.8
 | 
			
		||||
# Doxyfile 1.9.4
 | 
			
		||||
 | 
			
		||||
# This file describes the settings to be used by the documentation system
 | 
			
		||||
# doxygen (www.doxygen.org) for a project.
 | 
			
		||||
@@ -19,8 +19,7 @@
 | 
			
		||||
# configuration file:
 | 
			
		||||
# doxygen -x [configFile]
 | 
			
		||||
# Use doxygen to compare the used configuration file with the template
 | 
			
		||||
# configuration file without replacing the environment variables or CMake type
 | 
			
		||||
# replacement variables:
 | 
			
		||||
# configuration file without replacing the environment variables:
 | 
			
		||||
# doxygen -x_noenv [configFile]
 | 
			
		||||
 | 
			
		||||
#---------------------------------------------------------------------------
 | 
			
		||||
@@ -86,7 +85,7 @@ CREATE_SUBDIRS         = NO
 | 
			
		||||
# 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
 | 
			
		||||
# number of 16 directories.
 | 
			
		||||
# 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.
 | 
			
		||||
 | 
			
		||||
@@ -342,7 +341,7 @@ OPTIMIZE_OUTPUT_SLICE  = NO
 | 
			
		||||
#
 | 
			
		||||
# 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
 | 
			
		||||
# according to the Markdown format, which allows for more readable
 | 
			
		||||
@@ -363,17 +362,6 @@ MARKDOWN_SUPPORT       = YES
 | 
			
		||||
 | 
			
		||||
TOC_INCLUDE_HEADINGS   = 5
 | 
			
		||||
 | 
			
		||||
# The MARKDOWN_ID_STYLE tag can be used to specify the algorithm used to
 | 
			
		||||
# generate identifiers for the Markdown headings. Note: Every identifier is
 | 
			
		||||
# unique.
 | 
			
		||||
# Possible values are: DOXYGEN use a fixed 'autotoc_md' string followed by a
 | 
			
		||||
# sequence number starting at 0 and GITHUB use the lower case version of title
 | 
			
		||||
# with any whitespace replaced by '-' and punctuation characters removed.
 | 
			
		||||
# The default value is: DOXYGEN.
 | 
			
		||||
# This tag requires that the tag MARKDOWN_SUPPORT is set to YES.
 | 
			
		||||
 | 
			
		||||
MARKDOWN_ID_STYLE      = DOXYGEN
 | 
			
		||||
 | 
			
		||||
# When enabled doxygen tries to link words that correspond to documented
 | 
			
		||||
# classes, or namespaces to their corresponding documentation. Such a link can
 | 
			
		||||
# be prevented in individual cases by putting a % sign in front of the word or
 | 
			
		||||
@@ -498,14 +486,6 @@ LOOKUP_CACHE_SIZE      = 0
 | 
			
		||||
 | 
			
		||||
NUM_PROC_THREADS       = 1
 | 
			
		||||
 | 
			
		||||
# If the TIMESTAMP tag is set different from NO then each generated page will
 | 
			
		||||
# contain the date or date and time when the page was generated. Setting this to
 | 
			
		||||
# NO can help when comparing the output of multiple runs.
 | 
			
		||||
# Possible values are: YES, NO, DATETIME and DATE.
 | 
			
		||||
# The default value is: NO.
 | 
			
		||||
 | 
			
		||||
TIMESTAMP              = NO
 | 
			
		||||
 | 
			
		||||
#---------------------------------------------------------------------------
 | 
			
		||||
# Build related configuration options
 | 
			
		||||
#---------------------------------------------------------------------------
 | 
			
		||||
@@ -587,8 +567,7 @@ HIDE_UNDOC_MEMBERS     = NO
 | 
			
		||||
# If the HIDE_UNDOC_CLASSES tag is set to YES, doxygen will hide all
 | 
			
		||||
# undocumented classes that are normally visible in the class hierarchy. If set
 | 
			
		||||
# to NO, these classes will be included in the various overviews. This option
 | 
			
		||||
# will also hide undocumented C++ concepts if enabled. This option has no effect
 | 
			
		||||
# if EXTRACT_ALL is enabled.
 | 
			
		||||
# has no effect if EXTRACT_ALL is enabled.
 | 
			
		||||
# The default value is: NO.
 | 
			
		||||
 | 
			
		||||
HIDE_UNDOC_CLASSES     = NO
 | 
			
		||||
@@ -626,8 +605,7 @@ INTERNAL_DOCS          = NO
 | 
			
		||||
# Windows (including Cygwin) and MacOS, users should typically set this option
 | 
			
		||||
# to NO, whereas on Linux or other Unix flavors it should typically be set to
 | 
			
		||||
# YES.
 | 
			
		||||
# Possible values are: SYSTEM, NO and YES.
 | 
			
		||||
# The default value is: SYSTEM.
 | 
			
		||||
# The default value is: system dependent.
 | 
			
		||||
 | 
			
		||||
CASE_SENSE_NAMES       = YES
 | 
			
		||||
 | 
			
		||||
@@ -879,26 +857,11 @@ WARN_IF_INCOMPLETE_DOC = YES
 | 
			
		||||
 | 
			
		||||
WARN_NO_PARAMDOC       = NO
 | 
			
		||||
 | 
			
		||||
# If WARN_IF_UNDOC_ENUM_VAL option is set to YES, doxygen will warn about
 | 
			
		||||
# undocumented enumeration values. If set to NO, doxygen will accept
 | 
			
		||||
# undocumented enumeration values. If EXTRACT_ALL is set to YES then this flag
 | 
			
		||||
# will automatically be disabled.
 | 
			
		||||
# The default value is: NO.
 | 
			
		||||
 | 
			
		||||
WARN_IF_UNDOC_ENUM_VAL = NO
 | 
			
		||||
 | 
			
		||||
# If the WARN_AS_ERROR tag is set to YES then doxygen will immediately stop when
 | 
			
		||||
# a warning is encountered. If the WARN_AS_ERROR tag is set to FAIL_ON_WARNINGS
 | 
			
		||||
# then doxygen will continue running as if WARN_AS_ERROR tag is set to NO, but
 | 
			
		||||
# at the end of the doxygen process doxygen will return with a non-zero status.
 | 
			
		||||
# If the WARN_AS_ERROR tag is set to FAIL_ON_WARNINGS_PRINT then doxygen behaves
 | 
			
		||||
# like FAIL_ON_WARNINGS but in case no WARN_LOGFILE is defined doxygen will not
 | 
			
		||||
# write the warning messages in between other messages but write them at the end
 | 
			
		||||
# of a run, in case a WARN_LOGFILE is defined the warning messages will be
 | 
			
		||||
# besides being in the defined file also be shown at the end of a run, unless
 | 
			
		||||
# the WARN_LOGFILE is defined as - i.e. standard output (stdout) in that case
 | 
			
		||||
# the behavior will remain as with the setting FAIL_ON_WARNINGS.
 | 
			
		||||
# Possible values are: NO, YES, FAIL_ON_WARNINGS and FAIL_ON_WARNINGS_PRINT.
 | 
			
		||||
# Possible values are: NO, YES and FAIL_ON_WARNINGS.
 | 
			
		||||
# The default value is: NO.
 | 
			
		||||
 | 
			
		||||
WARN_AS_ERROR          = NO
 | 
			
		||||
@@ -943,28 +906,23 @@ WARN_LOGFILE           =
 | 
			
		||||
# spaces. See also FILE_PATTERNS and EXTENSION_MAPPING
 | 
			
		||||
# 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
 | 
			
		||||
# that doxygen parses. Internally doxygen uses the UTF-8 encoding. Doxygen uses
 | 
			
		||||
# libiconv (or the iconv built into libc) for the transcoding. See the libiconv
 | 
			
		||||
# documentation (see:
 | 
			
		||||
# https://www.gnu.org/software/libiconv/) for the list of possible encodings.
 | 
			
		||||
# See also: INPUT_FILE_ENCODING
 | 
			
		||||
# The default value is: UTF-8.
 | 
			
		||||
 | 
			
		||||
INPUT_ENCODING         = UTF-8
 | 
			
		||||
 | 
			
		||||
# This tag can be used to specify the character encoding of the source files
 | 
			
		||||
# that doxygen parses The INPUT_FILE_ENCODING tag can be used to specify
 | 
			
		||||
# character encoding on a per file pattern basis. Doxygen will compare the file
 | 
			
		||||
# name with each pattern and apply the encoding instead of the default
 | 
			
		||||
# INPUT_ENCODING) if there is a match. The character encodings are a list of the
 | 
			
		||||
# form: pattern=encoding (like *.php=ISO-8859-1). See cfg_input_encoding
 | 
			
		||||
# "INPUT_ENCODING" for further information on supported encodings.
 | 
			
		||||
 | 
			
		||||
INPUT_FILE_ENCODING    =
 | 
			
		||||
 | 
			
		||||
# If the value of the INPUT tag contains directories, you can use the
 | 
			
		||||
# FILE_PATTERNS tag to specify one or more wildcard patterns (like *.cpp and
 | 
			
		||||
# *.h) to filter out the source-files in the directories.
 | 
			
		||||
@@ -976,14 +934,15 @@ INPUT_FILE_ENCODING    =
 | 
			
		||||
# Note the list of default checked file patterns might differ from the list of
 | 
			
		||||
# default file extension mappings.
 | 
			
		||||
#
 | 
			
		||||
# If left blank the following patterns are tested:*.c, *.cc, *.cxx, *.cxxm,
 | 
			
		||||
# *.cpp, *.cppm, *.c++, *.c++m, *.java, *.ii, *.ixx, *.ipp, *.i++, *.inl, *.idl,
 | 
			
		||||
# *.ddl, *.odl, *.h, *.hh, *.hxx, *.hpp, *.h++, *.ixx, *.l, *.cs, *.d, *.php,
 | 
			
		||||
# *.php4, *.php5, *.phtml, *.inc, *.m, *.markdown, *.md, *.mm, *.dox (to be
 | 
			
		||||
# provided as doxygen C comment), *.py, *.pyw, *.f90, *.f95, *.f03, *.f08,
 | 
			
		||||
# *.f18, *.f, *.for, *.vhd, *.vhdl, *.ucf, *.qsf and *.ice.
 | 
			
		||||
# If left blank the following patterns are tested:*.c, *.cc, *.cxx, *.cpp,
 | 
			
		||||
# *.c++, *.java, *.ii, *.ixx, *.ipp, *.i++, *.inl, *.idl, *.ddl, *.odl, *.h,
 | 
			
		||||
# *.hh, *.hxx, *.hpp, *.h++, *.l, *.cs, *.d, *.php, *.php4, *.php5, *.phtml,
 | 
			
		||||
# *.inc, *.m, *.markdown, *.md, *.mm, *.dox (to be provided as doxygen C
 | 
			
		||||
# comment), *.py, *.pyw, *.f90, *.f95, *.f03, *.f08, *.f18, *.f, *.for, *.vhd,
 | 
			
		||||
# *.vhdl, *.ucf, *.qsf and *.ice.
 | 
			
		||||
 | 
			
		||||
FILE_PATTERNS          = *.h \
 | 
			
		||||
                         *.js \
 | 
			
		||||
                         *.md
 | 
			
		||||
 | 
			
		||||
# The RECURSIVE tag can be used to specify whether or not subdirectories should
 | 
			
		||||
@@ -1022,6 +981,9 @@ EXCLUDE_PATTERNS       =
 | 
			
		||||
# output. The symbol name can be a fully qualified name, a word, or if the
 | 
			
		||||
# wildcard * is used, a substring. Examples: ANamespace, AClass,
 | 
			
		||||
# ANamespace::AClass, ANamespace::*Test
 | 
			
		||||
#
 | 
			
		||||
# Note that the wildcards are matched against the file with absolute path, so to
 | 
			
		||||
# exclude all test directories use the pattern */test/*
 | 
			
		||||
 | 
			
		||||
EXCLUDE_SYMBOLS        =
 | 
			
		||||
 | 
			
		||||
@@ -1049,7 +1011,7 @@ EXAMPLE_RECURSIVE      = NO
 | 
			
		||||
# that contain images that are to be included in the documentation (see the
 | 
			
		||||
# \image command).
 | 
			
		||||
 | 
			
		||||
IMAGE_PATH             =
 | 
			
		||||
IMAGE_PATH             = docs/images/
 | 
			
		||||
 | 
			
		||||
# 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
 | 
			
		||||
@@ -1066,11 +1028,6 @@ IMAGE_PATH             =
 | 
			
		||||
# code is scanned, but not when the output code is generated. If lines are added
 | 
			
		||||
# or removed, the anchors will not be placed correctly.
 | 
			
		||||
#
 | 
			
		||||
# Note that doxygen will use the data processed and written to standard output
 | 
			
		||||
# for further processing, therefore nothing else, like debug statements or used
 | 
			
		||||
# commands (so in case of a Windows batch file always use @echo OFF), should be
 | 
			
		||||
# written to standard output.
 | 
			
		||||
#
 | 
			
		||||
# Note that for custom extensions or not directly supported extensions you also
 | 
			
		||||
# need to set EXTENSION_MAPPING for the extension otherwise the files are not
 | 
			
		||||
# properly processed by doxygen.
 | 
			
		||||
@@ -1110,16 +1067,7 @@ FILTER_SOURCE_PATTERNS =
 | 
			
		||||
# (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.
 | 
			
		||||
 | 
			
		||||
USE_MDFILE_AS_MAINPAGE =
 | 
			
		||||
 | 
			
		||||
# The Fortran standard specifies that for fixed formatted Fortran code all
 | 
			
		||||
# characters from position 72 are to be considered as comment. A common
 | 
			
		||||
# extension is to allow longer lines before the automatic comment starts. The
 | 
			
		||||
# setting FORTRAN_COMMENT_AFTER will also make it possible that longer lines can
 | 
			
		||||
# be processed before the automatic comment starts.
 | 
			
		||||
# Minimum value: 7, maximum value: 10000, default value: 72.
 | 
			
		||||
 | 
			
		||||
FORTRAN_COMMENT_AFTER  = 72
 | 
			
		||||
USE_MDFILE_AS_MAINPAGE = README.md
 | 
			
		||||
 | 
			
		||||
#---------------------------------------------------------------------------
 | 
			
		||||
# Configuration options related to source browsing
 | 
			
		||||
@@ -1258,11 +1206,10 @@ CLANG_DATABASE_PATH    =
 | 
			
		||||
 | 
			
		||||
ALPHABETICAL_INDEX     = YES
 | 
			
		||||
 | 
			
		||||
# The IGNORE_PREFIX tag can be used to specify a prefix (or a list of prefixes)
 | 
			
		||||
# that should be ignored while generating the index headers. The IGNORE_PREFIX
 | 
			
		||||
# tag works for classes, function and member names. The entity will be placed in
 | 
			
		||||
# the alphabetical list under the first letter of the entity name that remains
 | 
			
		||||
# after removing the prefix.
 | 
			
		||||
# In case all classes in a project start with a common prefix, all classes will
 | 
			
		||||
# be put under the same header in the alphabetical index. The IGNORE_PREFIX tag
 | 
			
		||||
# can be used to specify a prefix (or a list of prefixes) that should be ignored
 | 
			
		||||
# while generating the index headers.
 | 
			
		||||
# This tag requires that the tag ALPHABETICAL_INDEX is set to YES.
 | 
			
		||||
 | 
			
		||||
IGNORE_PREFIX          =
 | 
			
		||||
@@ -1341,12 +1288,7 @@ HTML_STYLESHEET        =
 | 
			
		||||
# Doxygen will copy the style sheet files to the output directory.
 | 
			
		||||
# Note: The order of the extra style sheet files is of importance (e.g. the last
 | 
			
		||||
# style sheet in the list overrules the setting of the previous ones in the
 | 
			
		||||
# list).
 | 
			
		||||
# Note: Since the styling of scrollbars can currently not be overruled in
 | 
			
		||||
# Webkit/Chromium, the styling will be left out of the default doxygen.css if
 | 
			
		||||
# one or more extra stylesheets have been specified. So if scrollbar
 | 
			
		||||
# customization is desired it has to be added explicitly. For an example see the
 | 
			
		||||
# documentation.
 | 
			
		||||
# list). For an example see the documentation.
 | 
			
		||||
# This tag requires that the tag GENERATE_HTML is set to YES.
 | 
			
		||||
 | 
			
		||||
HTML_EXTRA_STYLESHEET  =
 | 
			
		||||
@@ -1361,19 +1303,6 @@ HTML_EXTRA_STYLESHEET  =
 | 
			
		||||
 | 
			
		||||
HTML_EXTRA_FILES       =
 | 
			
		||||
 | 
			
		||||
# The HTML_COLORSTYLE tag can be used to specify if the generated HTML output
 | 
			
		||||
# should be rendered with a dark or light theme.
 | 
			
		||||
# Possible values are: LIGHT always generate light mode output, DARK always
 | 
			
		||||
# generate dark mode output, AUTO_LIGHT automatically set the mode according to
 | 
			
		||||
# the user preference, use light mode if no preference is set (the default),
 | 
			
		||||
# AUTO_DARK automatically set the mode according to the user preference, use
 | 
			
		||||
# dark mode if no preference is set and TOGGLE allow to user to switch between
 | 
			
		||||
# light and dark mode via a button.
 | 
			
		||||
# The default value is: AUTO_LIGHT.
 | 
			
		||||
# This tag requires that the tag GENERATE_HTML is set to YES.
 | 
			
		||||
 | 
			
		||||
HTML_COLORSTYLE        = AUTO_LIGHT
 | 
			
		||||
 | 
			
		||||
# 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
 | 
			
		||||
# this color. Hue is specified as an angle on a color-wheel, see
 | 
			
		||||
@@ -1404,6 +1333,15 @@ HTML_COLORSTYLE_SAT    = 100
 | 
			
		||||
 | 
			
		||||
HTML_COLORSTYLE_GAMMA  = 80
 | 
			
		||||
 | 
			
		||||
# If the HTML_TIMESTAMP tag is set to YES then the footer of each generated HTML
 | 
			
		||||
# page will contain the date and time when the page was generated. Setting this
 | 
			
		||||
# to YES can help to show when doxygen was last run and thus if the
 | 
			
		||||
# documentation is up to date.
 | 
			
		||||
# The default value is: NO.
 | 
			
		||||
# This tag requires that the tag GENERATE_HTML is set to YES.
 | 
			
		||||
 | 
			
		||||
#HTML_TIMESTAMP         = NO
 | 
			
		||||
 | 
			
		||||
# 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
 | 
			
		||||
# are dynamically created via JavaScript. If disabled, the navigation index will
 | 
			
		||||
@@ -1423,13 +1361,6 @@ HTML_DYNAMIC_MENUS     = YES
 | 
			
		||||
 | 
			
		||||
HTML_DYNAMIC_SECTIONS  = NO
 | 
			
		||||
 | 
			
		||||
# If the HTML_CODE_FOLDING tag is set to YES then classes and functions can be
 | 
			
		||||
# dynamically folded and expanded in the generated HTML source code.
 | 
			
		||||
# The default value is: YES.
 | 
			
		||||
# This tag requires that the tag GENERATE_HTML is set to YES.
 | 
			
		||||
 | 
			
		||||
HTML_CODE_FOLDING      = YES
 | 
			
		||||
 | 
			
		||||
# With HTML_INDEX_NUM_ENTRIES one can control the preferred number of entries
 | 
			
		||||
# shown in the various tree structured indices initially; the user can expand
 | 
			
		||||
# and collapse entries dynamically later on. Doxygen will expand the tree to
 | 
			
		||||
@@ -1560,16 +1491,6 @@ BINARY_TOC             = NO
 | 
			
		||||
 | 
			
		||||
TOC_EXPAND             = NO
 | 
			
		||||
 | 
			
		||||
# The SITEMAP_URL tag is used to specify the full URL of the place where the
 | 
			
		||||
# generated documentation will be placed on the server by the user during the
 | 
			
		||||
# deployment of the documentation. The generated sitemap is called sitemap.xml
 | 
			
		||||
# and placed on the directory specified by HTML_OUTPUT. In case no SITEMAP_URL
 | 
			
		||||
# is specified no sitemap is generated. For information about the sitemap
 | 
			
		||||
# protocol see https://www.sitemaps.org
 | 
			
		||||
# This tag requires that the tag GENERATE_HTML is set to YES.
 | 
			
		||||
 | 
			
		||||
SITEMAP_URL            =
 | 
			
		||||
 | 
			
		||||
# If the GENERATE_QHP tag is set to YES and both QHP_NAMESPACE and
 | 
			
		||||
# QHP_VIRTUAL_FOLDER are set, an additional index file will be generated that
 | 
			
		||||
# can be used as input for Qt's qhelpgenerator to generate a Qt Compressed Help
 | 
			
		||||
@@ -1680,7 +1601,7 @@ DISABLE_INDEX          = NO
 | 
			
		||||
# The default value is: NO.
 | 
			
		||||
# 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
 | 
			
		||||
@@ -1745,6 +1666,17 @@ HTML_FORMULA_FORMAT    = png
 | 
			
		||||
 | 
			
		||||
FORMULA_FONTSIZE       = 10
 | 
			
		||||
 | 
			
		||||
# Use the FORMULA_TRANSPARENT tag to determine whether or not the images
 | 
			
		||||
# generated for formulas are transparent PNGs. Transparent PNGs are not
 | 
			
		||||
# supported properly for IE 6.0, but are supported on all modern browsers.
 | 
			
		||||
#
 | 
			
		||||
# Note that when changing this option you need to delete any form_*.png files in
 | 
			
		||||
# the HTML output directory before the changes have effect.
 | 
			
		||||
# The default value is: YES.
 | 
			
		||||
# This tag requires that the tag GENERATE_HTML is set to YES.
 | 
			
		||||
 | 
			
		||||
#FORMULA_TRANSPARENT    = YES
 | 
			
		||||
 | 
			
		||||
# The FORMULA_MACROFILE can contain LaTeX \newcommand and \renewcommand commands
 | 
			
		||||
# to create new LaTeX commands to be used in formulas as building blocks. See
 | 
			
		||||
# the section "Including formulas" for details.
 | 
			
		||||
@@ -1806,8 +1738,8 @@ MATHJAX_RELPATH        = https://cdn.jsdelivr.net/npm/mathjax@2
 | 
			
		||||
 | 
			
		||||
# The MATHJAX_EXTENSIONS tag can be used to specify one or more MathJax
 | 
			
		||||
# 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):
 | 
			
		||||
# 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
 | 
			
		||||
# For example for MathJax version 3 (see
 | 
			
		||||
# http://docs.mathjax.org/en/latest/input/tex/extensions/index.html):
 | 
			
		||||
@@ -2058,16 +1990,9 @@ PDF_HYPERLINKS         = YES
 | 
			
		||||
 | 
			
		||||
USE_PDFLATEX           = YES
 | 
			
		||||
 | 
			
		||||
# The LATEX_BATCHMODE tag signals the behavior of LaTeX in case of an error.
 | 
			
		||||
# Possible values are: NO same as ERROR_STOP, YES same as BATCH, BATCH In batch
 | 
			
		||||
# mode nothing is printed on the terminal, errors are scrolled as if <return> is
 | 
			
		||||
# hit at every error; missing files that TeX tries to input or request from
 | 
			
		||||
# keyboard input (\read on a not open input stream) cause the job to abort,
 | 
			
		||||
# NON_STOP In nonstop mode the diagnostic message will appear on the terminal,
 | 
			
		||||
# but there is no possibility of user interaction just like in batch mode,
 | 
			
		||||
# SCROLL In scroll mode, TeX will stop only for missing files to input or if
 | 
			
		||||
# keyboard input is necessary and ERROR_STOP In errorstop mode, TeX will stop at
 | 
			
		||||
# each error, asking for user intervention.
 | 
			
		||||
# 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
 | 
			
		||||
# if errors occur, instead of asking the user for help.
 | 
			
		||||
# The default value is: NO.
 | 
			
		||||
# This tag requires that the tag GENERATE_LATEX is set to YES.
 | 
			
		||||
 | 
			
		||||
@@ -2088,6 +2013,14 @@ LATEX_HIDE_INDICES     = NO
 | 
			
		||||
 | 
			
		||||
LATEX_BIB_STYLE        = plain
 | 
			
		||||
 | 
			
		||||
# If the LATEX_TIMESTAMP tag is set to YES then the footer of each generated
 | 
			
		||||
# page will contain the date and time when the page was generated. Setting this
 | 
			
		||||
# to NO can help when comparing the output of multiple runs.
 | 
			
		||||
# The default value is: NO.
 | 
			
		||||
# This tag requires that the tag GENERATE_LATEX is set to YES.
 | 
			
		||||
 | 
			
		||||
#LATEX_TIMESTAMP        = NO
 | 
			
		||||
 | 
			
		||||
# 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,
 | 
			
		||||
# it will be relative to the LATEX_OUTPUT directory. If left blank the
 | 
			
		||||
@@ -2253,39 +2186,13 @@ DOCBOOK_OUTPUT         = docbook
 | 
			
		||||
#---------------------------------------------------------------------------
 | 
			
		||||
 | 
			
		||||
# If the GENERATE_AUTOGEN_DEF tag is set to YES, doxygen will generate an
 | 
			
		||||
# AutoGen Definitions (see https://autogen.sourceforge.net/) file that captures
 | 
			
		||||
# AutoGen Definitions (see http://autogen.sourceforge.net/) file that captures
 | 
			
		||||
# the structure of the code including all documentation. Note that this feature
 | 
			
		||||
# is still experimental and incomplete at the moment.
 | 
			
		||||
# The default value is: NO.
 | 
			
		||||
 | 
			
		||||
GENERATE_AUTOGEN_DEF   = NO
 | 
			
		||||
 | 
			
		||||
#---------------------------------------------------------------------------
 | 
			
		||||
# Configuration options related to Sqlite3 output
 | 
			
		||||
#---------------------------------------------------------------------------
 | 
			
		||||
 | 
			
		||||
# If the GENERATE_SQLITE3 tag is set to YES doxygen will generate a Sqlite3
 | 
			
		||||
# database with symbols found by doxygen stored in tables.
 | 
			
		||||
# The default value is: NO.
 | 
			
		||||
 | 
			
		||||
GENERATE_SQLITE3       = NO
 | 
			
		||||
 | 
			
		||||
# The SQLITE3_OUTPUT tag is used to specify where the Sqlite3 database will be
 | 
			
		||||
# put. If a relative path is entered the value of OUTPUT_DIRECTORY will be put
 | 
			
		||||
# in front of it.
 | 
			
		||||
# The default directory is: sqlite3.
 | 
			
		||||
# This tag requires that the tag GENERATE_SQLITE3 is set to YES.
 | 
			
		||||
 | 
			
		||||
SQLITE3_OUTPUT         = sqlite3
 | 
			
		||||
 | 
			
		||||
# The SQLITE3_OVERWRITE_DB tag is set to YES, the existing doxygen_sqlite3.db
 | 
			
		||||
# database file will be recreated with each doxygen run. If set to NO, doxygen
 | 
			
		||||
# will warn if an a database file is already found and not modify it.
 | 
			
		||||
# The default value is: YES.
 | 
			
		||||
# This tag requires that the tag GENERATE_SQLITE3 is set to YES.
 | 
			
		||||
 | 
			
		||||
SQLITE3_RECREATE_DB    = YES
 | 
			
		||||
 | 
			
		||||
#---------------------------------------------------------------------------
 | 
			
		||||
# Configuration options related to the Perl module output
 | 
			
		||||
#---------------------------------------------------------------------------
 | 
			
		||||
@@ -2428,15 +2335,15 @@ TAGFILES               =
 | 
			
		||||
 | 
			
		||||
GENERATE_TAGFILE       =
 | 
			
		||||
 | 
			
		||||
# If the ALLEXTERNALS tag is set to YES, all external classes and namespaces
 | 
			
		||||
# will be listed in the class and namespace index. If set to NO, only the
 | 
			
		||||
# inherited external classes will be listed.
 | 
			
		||||
# If the ALLEXTERNALS tag is set to YES, all external class will be listed in
 | 
			
		||||
# the class index. If set to NO, only the inherited external classes will be
 | 
			
		||||
# listed.
 | 
			
		||||
# The default value is: NO.
 | 
			
		||||
 | 
			
		||||
ALLEXTERNALS           = NO
 | 
			
		||||
 | 
			
		||||
# If the EXTERNAL_GROUPS tag is set to YES, all external groups will be listed
 | 
			
		||||
# in the topic index. If set to NO, only the current project's groups will be
 | 
			
		||||
# in the modules index. If set to NO, only the current project's groups will be
 | 
			
		||||
# listed.
 | 
			
		||||
# The default value is: YES.
 | 
			
		||||
 | 
			
		||||
@@ -2450,9 +2357,16 @@ EXTERNAL_GROUPS        = YES
 | 
			
		||||
EXTERNAL_PAGES         = YES
 | 
			
		||||
 | 
			
		||||
#---------------------------------------------------------------------------
 | 
			
		||||
# Configuration options related to diagram generator tools
 | 
			
		||||
# Configuration options related to the dot tool
 | 
			
		||||
#---------------------------------------------------------------------------
 | 
			
		||||
 | 
			
		||||
# 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
 | 
			
		||||
# DIA_PATH tag allows you to specify the directory where the dia binary resides.
 | 
			
		||||
# If left empty dia is assumed to be found in the default search path.
 | 
			
		||||
 | 
			
		||||
DIA_PATH               =
 | 
			
		||||
 | 
			
		||||
# If set to YES the inheritance and collaboration graphs will hide inheritance
 | 
			
		||||
# and usage relations if the target is undocumented or is not a class.
 | 
			
		||||
# The default value is: YES.
 | 
			
		||||
@@ -2461,10 +2375,10 @@ HIDE_UNDOC_RELATIONS   = YES
 | 
			
		||||
 | 
			
		||||
# If you set the HAVE_DOT tag to YES then doxygen will assume the dot tool is
 | 
			
		||||
# available from the path. This tool is part of Graphviz (see:
 | 
			
		||||
# https://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
 | 
			
		||||
# set to NO
 | 
			
		||||
# The default value is: YES.
 | 
			
		||||
# The default value is: NO.
 | 
			
		||||
 | 
			
		||||
HAVE_DOT               = YES
 | 
			
		||||
 | 
			
		||||
@@ -2478,51 +2392,37 @@ HAVE_DOT               = YES
 | 
			
		||||
 | 
			
		||||
DOT_NUM_THREADS        = 0
 | 
			
		||||
 | 
			
		||||
# DOT_COMMON_ATTR is common attributes for nodes, edges and labels of
 | 
			
		||||
# subgraphs. When you want a differently looking font in the dot files that
 | 
			
		||||
# doxygen generates you can specify fontname, fontcolor and fontsize attributes.
 | 
			
		||||
# For details please see <a href=https://graphviz.org/doc/info/attrs.html>Node,
 | 
			
		||||
# Edge and Graph Attributes specification</a> You need to make sure dot is able
 | 
			
		||||
# to find the font, which can be done by putting it in a standard location or by
 | 
			
		||||
# setting the DOTFONTPATH environment variable or by setting DOT_FONTPATH to the
 | 
			
		||||
# directory containing the font. Default graphviz fontsize is 14.
 | 
			
		||||
# The default value is: fontname=Helvetica,fontsize=10.
 | 
			
		||||
# When you want a differently looking font in the dot files that doxygen
 | 
			
		||||
# generates you can specify the font name using DOT_FONTNAME. You need to make
 | 
			
		||||
# sure dot is able to find the font, which can be done by putting it in a
 | 
			
		||||
# standard location or by setting the DOTFONTPATH environment variable or by
 | 
			
		||||
# setting DOT_FONTPATH to the directory containing the font.
 | 
			
		||||
# The default value is: Helvetica.
 | 
			
		||||
# This tag requires that the tag HAVE_DOT is set to YES.
 | 
			
		||||
 | 
			
		||||
DOT_COMMON_ATTR        = "fontname=Helvetica,fontsize=10"
 | 
			
		||||
#DOT_FONTNAME           = Helvetica
 | 
			
		||||
 | 
			
		||||
# DOT_EDGE_ATTR is concatenated with DOT_COMMON_ATTR. For elegant style you can
 | 
			
		||||
# add 'arrowhead=open, arrowtail=open, arrowsize=0.5'. <a
 | 
			
		||||
# href=https://graphviz.org/doc/info/arrows.html>Complete documentation about
 | 
			
		||||
# arrows shapes.</a>
 | 
			
		||||
# The default value is: labelfontname=Helvetica,labelfontsize=10.
 | 
			
		||||
# The DOT_FONTSIZE tag can be used to set the size (in points) of the font of
 | 
			
		||||
# dot graphs.
 | 
			
		||||
# Minimum value: 4, maximum value: 24, default value: 10.
 | 
			
		||||
# This tag requires that the tag HAVE_DOT is set to YES.
 | 
			
		||||
 | 
			
		||||
DOT_EDGE_ATTR          = "labelfontname=Helvetica,labelfontsize=10"
 | 
			
		||||
#DOT_FONTSIZE           = 10
 | 
			
		||||
 | 
			
		||||
# DOT_NODE_ATTR is concatenated with DOT_COMMON_ATTR. For view without boxes
 | 
			
		||||
# around nodes set 'shape=plain' or 'shape=plaintext' <a
 | 
			
		||||
# href=https://www.graphviz.org/doc/info/shapes.html>Shapes specification</a>
 | 
			
		||||
# The default value is: shape=box,height=0.2,width=0.4.
 | 
			
		||||
# 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
 | 
			
		||||
# the path where dot can find it using this tag.
 | 
			
		||||
# This tag requires that the tag HAVE_DOT is set to YES.
 | 
			
		||||
 | 
			
		||||
DOT_NODE_ATTR          = "shape=box,height=0.2,width=0.4"
 | 
			
		||||
#DOT_FONTPATH           =
 | 
			
		||||
 | 
			
		||||
# You can set the path where dot can find font specified with fontname in
 | 
			
		||||
# DOT_COMMON_ATTR and others dot attributes.
 | 
			
		||||
# This tag requires that the tag HAVE_DOT is set to YES.
 | 
			
		||||
 | 
			
		||||
DOT_FONTPATH           =
 | 
			
		||||
 | 
			
		||||
# If the CLASS_GRAPH tag is set to YES or GRAPH or BUILTIN then doxygen will
 | 
			
		||||
# generate a graph for each documented class showing the direct and indirect
 | 
			
		||||
# inheritance relations. In case the CLASS_GRAPH tag is set to YES or GRAPH and
 | 
			
		||||
# HAVE_DOT is enabled as well, then dot will be used to draw the graph. In case
 | 
			
		||||
# the CLASS_GRAPH tag is set to YES and HAVE_DOT is disabled or if the
 | 
			
		||||
# CLASS_GRAPH tag is set to BUILTIN, then 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, GRAPH and BUILTIN.
 | 
			
		||||
# If the CLASS_GRAPH tag is set to YES (or GRAPH) then doxygen will generate a
 | 
			
		||||
# graph for each documented class showing the direct and indirect inheritance
 | 
			
		||||
# 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.
 | 
			
		||||
 | 
			
		||||
CLASS_GRAPH            = YES
 | 
			
		||||
@@ -2530,21 +2430,15 @@ CLASS_GRAPH            = YES
 | 
			
		||||
# If the COLLABORATION_GRAPH tag is set to YES then doxygen will generate a
 | 
			
		||||
# graph for each documented class showing the direct and indirect implementation
 | 
			
		||||
# dependencies (inheritance, containment, and class references variables) of the
 | 
			
		||||
# class with other documented classes. Explicit enabling a collaboration graph,
 | 
			
		||||
# when COLLABORATION_GRAPH is set to NO, can be accomplished by means of the
 | 
			
		||||
# command \collaborationgraph. Disabling a collaboration graph can be
 | 
			
		||||
# accomplished by means of the command \hidecollaborationgraph.
 | 
			
		||||
# class with other documented classes.
 | 
			
		||||
# The default value is: YES.
 | 
			
		||||
# This tag requires that the tag HAVE_DOT is set to YES.
 | 
			
		||||
 | 
			
		||||
COLLABORATION_GRAPH    = YES
 | 
			
		||||
 | 
			
		||||
# If the GROUP_GRAPHS tag is set to YES then doxygen will generate a graph for
 | 
			
		||||
# groups, showing the direct groups dependencies. Explicit enabling a group
 | 
			
		||||
# dependency graph, when GROUP_GRAPHS is set to NO, can be accomplished by means
 | 
			
		||||
# of the command \groupgraph. Disabling a directory graph can be accomplished by
 | 
			
		||||
# means of the command \hidegroupgraph. See also the chapter Grouping in the
 | 
			
		||||
# manual.
 | 
			
		||||
# groups, showing the direct groups dependencies. See also the chapter Grouping
 | 
			
		||||
# in the manual.
 | 
			
		||||
# The default value is: YES.
 | 
			
		||||
# This tag requires that the tag HAVE_DOT is set to YES.
 | 
			
		||||
 | 
			
		||||
@@ -2604,9 +2498,7 @@ TEMPLATE_RELATIONS     = NO
 | 
			
		||||
# If the INCLUDE_GRAPH, ENABLE_PREPROCESSING and SEARCH_INCLUDES tags are set to
 | 
			
		||||
# YES then doxygen will generate a graph for each documented file showing the
 | 
			
		||||
# direct and indirect include dependencies of the file with other documented
 | 
			
		||||
# files. Explicit enabling an include graph, when INCLUDE_GRAPH is is set to NO,
 | 
			
		||||
# can be accomplished by means of the command \includegraph. Disabling an
 | 
			
		||||
# include graph can be accomplished by means of the command \hideincludegraph.
 | 
			
		||||
# files.
 | 
			
		||||
# The default value is: YES.
 | 
			
		||||
# This tag requires that the tag HAVE_DOT is set to YES.
 | 
			
		||||
 | 
			
		||||
@@ -2615,10 +2507,7 @@ INCLUDE_GRAPH          = YES
 | 
			
		||||
# If the INCLUDED_BY_GRAPH, ENABLE_PREPROCESSING and SEARCH_INCLUDES tags are
 | 
			
		||||
# set to YES then doxygen will generate a graph for each documented file showing
 | 
			
		||||
# the direct and indirect include dependencies of the file with other documented
 | 
			
		||||
# files. Explicit enabling an included by graph, when INCLUDED_BY_GRAPH is set
 | 
			
		||||
# to NO, can be accomplished by means of the command \includedbygraph. Disabling
 | 
			
		||||
# an included by graph can be accomplished by means of the command
 | 
			
		||||
# \hideincludedbygraph.
 | 
			
		||||
# files.
 | 
			
		||||
# The default value is: YES.
 | 
			
		||||
# This tag requires that the tag HAVE_DOT is set to YES.
 | 
			
		||||
 | 
			
		||||
@@ -2658,10 +2547,7 @@ GRAPHICAL_HIERARCHY    = YES
 | 
			
		||||
# If the DIRECTORY_GRAPH tag is set to YES then doxygen will show the
 | 
			
		||||
# dependencies a directory has on other directories in a graphical way. The
 | 
			
		||||
# dependency relations are determined by the #include relations between the
 | 
			
		||||
# files in the directories. Explicit enabling a directory graph, when
 | 
			
		||||
# DIRECTORY_GRAPH is set to NO, can be accomplished by means of the command
 | 
			
		||||
# \directorygraph. Disabling a directory graph can be accomplished by means of
 | 
			
		||||
# the command \hidedirectorygraph.
 | 
			
		||||
# files in the directories.
 | 
			
		||||
# The default value is: YES.
 | 
			
		||||
# This tag requires that the tag HAVE_DOT is set to YES.
 | 
			
		||||
 | 
			
		||||
@@ -2677,13 +2563,12 @@ DIR_GRAPH_MAX_DEPTH    = 1
 | 
			
		||||
# 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
 | 
			
		||||
# output formats in the documentation of the dot tool (Graphviz (see:
 | 
			
		||||
# https://www.graphviz.org/)).
 | 
			
		||||
# http://www.graphviz.org/)).
 | 
			
		||||
# 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
 | 
			
		||||
# requirement).
 | 
			
		||||
# Possible values are: png, 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
 | 
			
		||||
# Possible values are: png, jpg, gif, svg, png:gd, png:gd:gd, png:cairo,
 | 
			
		||||
# png:cairo:gd, png:cairo:cairo, png:cairo:gdiplus, png:gdiplus and
 | 
			
		||||
# png:gdiplus:gdiplus.
 | 
			
		||||
# The default value is: png.
 | 
			
		||||
# This tag requires that the tag HAVE_DOT is set to YES.
 | 
			
		||||
@@ -2715,12 +2600,11 @@ DOT_PATH               =
 | 
			
		||||
 | 
			
		||||
DOTFILE_DIRS           =
 | 
			
		||||
 | 
			
		||||
# 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
 | 
			
		||||
# DIA_PATH tag allows you to specify the directory where the dia binary resides.
 | 
			
		||||
# If left empty dia is assumed to be found in the default search path.
 | 
			
		||||
# The MSCFILE_DIRS tag can be used to specify one or more directories that
 | 
			
		||||
# contain msc files that are included in the documentation (see the \mscfile
 | 
			
		||||
# command).
 | 
			
		||||
 | 
			
		||||
DIA_PATH               =
 | 
			
		||||
MSCFILE_DIRS           =
 | 
			
		||||
 | 
			
		||||
# The DIAFILE_DIRS tag can be used to specify one or more directories that
 | 
			
		||||
# contain dia files that are included in the documentation (see the \diafile
 | 
			
		||||
@@ -2770,6 +2654,18 @@ DOT_GRAPH_MAX_NODES    = 50
 | 
			
		||||
 | 
			
		||||
MAX_DOT_GRAPH_DEPTH    = 0
 | 
			
		||||
 | 
			
		||||
# Set the DOT_TRANSPARENT tag to YES to generate images with a transparent
 | 
			
		||||
# background. This is disabled by default, because dot on Windows does not seem
 | 
			
		||||
# to support this out of the box.
 | 
			
		||||
#
 | 
			
		||||
# Warning: Depending on the platform used, enabling this option may lead to
 | 
			
		||||
# badly anti-aliased labels on the edges of a graph (i.e. they become hard to
 | 
			
		||||
# read).
 | 
			
		||||
# The default value is: NO.
 | 
			
		||||
# This tag requires that the tag HAVE_DOT is set to YES.
 | 
			
		||||
 | 
			
		||||
#DOT_TRANSPARENT        = NO
 | 
			
		||||
 | 
			
		||||
# 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
 | 
			
		||||
# makes dot run faster, but since only newer versions of dot (>1.8.10) support
 | 
			
		||||
@@ -2797,19 +2693,3 @@ GENERATE_LEGEND        = YES
 | 
			
		||||
# The default value is: YES.
 | 
			
		||||
 | 
			
		||||
DOT_CLEANUP            = YES
 | 
			
		||||
 | 
			
		||||
# You can define message sequence charts within doxygen comments using the \msc
 | 
			
		||||
# command. If the MSCGEN_TOOL tag is left empty (the default), then doxygen will
 | 
			
		||||
# use a built-in version of mscgen tool to produce the charts. Alternatively,
 | 
			
		||||
# the MSCGEN_TOOL tag can also specify the name an external tool. For instance,
 | 
			
		||||
# specifying prog as the value, doxygen will call the tool as prog -T
 | 
			
		||||
# <outfile_format> -o <outputfile> <inputfile>. The external tool should support
 | 
			
		||||
# output file formats "png", "eps", "svg", and "ismap".
 | 
			
		||||
 | 
			
		||||
MSCGEN_TOOL            =
 | 
			
		||||
 | 
			
		||||
# The MSCFILE_DIRS tag can be used to specify one or more directories that
 | 
			
		||||
# contain msc files that are included in the documentation (see the \mscfile
 | 
			
		||||
# command).
 | 
			
		||||
 | 
			
		||||
MSCFILE_DIRS           =
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										389
									
								
								GNUmakefile
									
									
									
									
									
								
							
							
						
						@@ -16,11 +16,14 @@ MAKEFLAGS += --no-builtin-rules
 | 
			
		||||
## LD := Linker.
 | 
			
		||||
## ANDROID_SDK := Path to the Android SDK.
 | 
			
		||||
 | 
			
		||||
VERSION_CODE := 32
 | 
			
		||||
VERSION_NUMBER := 0.0.27-wip
 | 
			
		||||
VERSION_CODE := 44
 | 
			
		||||
VERSION_CODE_IOS := 18
 | 
			
		||||
VERSION_NUMBER := 0.2025.10
 | 
			
		||||
VERSION_NAME := This program kills fascists.
 | 
			
		||||
 | 
			
		||||
SQLITE_URL := https://www.sqlite.org/2024/sqlite-amalgamation-3470200.zip
 | 
			
		||||
IPHONEOS_VERSION_MIN=14.0
 | 
			
		||||
 | 
			
		||||
SQLITE_URL := https://www.sqlite.org/2025/sqlite-amalgamation-3500400.zip
 | 
			
		||||
BUNDLETOOL_URL := https://github.com/google/bundletool/releases/download/1.17.0/bundletool-all-1.17.0.jar
 | 
			
		||||
APPIMAGETOOL_URL := https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-x86_64.AppImage
 | 
			
		||||
APPIMAGETOOL_MD5 := e989fadfc4d685fd3d6aeeb9b525d74d  out/appimagetool
 | 
			
		||||
@@ -33,21 +36,26 @@ UNAME_M := $(shell uname -m)
 | 
			
		||||
ANDROID_SDK ?= ~/Android/Sdk
 | 
			
		||||
BUNDLETOOL = out/bundletool.jar
 | 
			
		||||
 | 
			
		||||
HAVE_WIN := 0
 | 
			
		||||
HAVE_CROSS_AARCH64 := 0
 | 
			
		||||
HAVE_WIN :=
 | 
			
		||||
HAVE_CROSS_AARCH64 :=
 | 
			
		||||
 | 
			
		||||
export SOURCE_DATE_EPOCH=1
 | 
			
		||||
export TZ=UTC
 | 
			
		||||
 | 
			
		||||
ifeq ($(UNAME_S),Darwin)
 | 
			
		||||
BUILD_TYPES := macosdebug macosrelease iosdebug iosrelease iossimdebug iossimrelease
 | 
			
		||||
BUILD_TYPES := debug release iosdebug iosrelease iossimdebug iossimrelease
 | 
			
		||||
HAVE_ANDROID = 0
 | 
			
		||||
HAVE_LINUX_IOS = 0
 | 
			
		||||
HAVE_LINUX_MACOS = 0
 | 
			
		||||
HAVE_WIN = 0
 | 
			
		||||
else ifeq ($(UNAME_S),Linux)
 | 
			
		||||
BUILD_TYPES := debug release
 | 
			
		||||
HAVE_ANDROID = $(if $(shell which $(ANDROID_SDK)/platform-tools/adb),1,0)
 | 
			
		||||
HAVE_LINUX_IOS = $(if $(shell which deps/ios_toolchain/target/bin deps/ios_toolchain/target/bin/arm-apple-darwin11-clang),1,0)
 | 
			
		||||
HAVE_WIN = $(if $(shell which x86_64-w64-mingw32-gcc-win32),1,0)
 | 
			
		||||
HAVE_ANDROID = $(if $(shell which $(ANDROID_SDK)/platform-tools/adb),1)
 | 
			
		||||
HAVE_LINUX_IOS = $(if $(shell which deps/ios_toolchain/target/bin deps/ios_toolchain/target/bin/arm-apple-darwin11-clang),1)
 | 
			
		||||
HAVE_LINUX_MACOS = $(if $(shell which deps/macos_toolchain/bin/oa64-clang),1)
 | 
			
		||||
HAVE_WIN = $(if $(shell which x86_64-w64-mingw32-gcc-win32),1)
 | 
			
		||||
ifneq ($(UNAME_M),aarch64)
 | 
			
		||||
HAVE_CROSS_AARCH64 = $(if $(shell which aarch64-linux-gnu-gcc),1,0)
 | 
			
		||||
HAVE_CROSS_AARCH64 = $(if $(shell which aarch64-linux-gnu-gcc),1)
 | 
			
		||||
endif
 | 
			
		||||
else ifeq ($(UNAME_S),Haiku)
 | 
			
		||||
BUILD_TYPES := debug release
 | 
			
		||||
@@ -56,6 +64,10 @@ LDFLAGS += \
 | 
			
		||||
	-lbsd \
 | 
			
		||||
	-lnetwork \
 | 
			
		||||
	-Wno-stringop-overflow
 | 
			
		||||
HAVE_ANDROID = 0
 | 
			
		||||
HAVE_LINUX_IOS = 0
 | 
			
		||||
HAVE_LINUX_MACOS = 0
 | 
			
		||||
HAVE_WIN = 0
 | 
			
		||||
else ifeq ($(UNAME_S),OpenBSD)
 | 
			
		||||
BUILD_TYPES := debug release
 | 
			
		||||
CFLAGS += \
 | 
			
		||||
@@ -63,18 +75,23 @@ CFLAGS += \
 | 
			
		||||
LDFLAGS += \
 | 
			
		||||
	-lexecinfo \
 | 
			
		||||
	-lc++abi
 | 
			
		||||
HAVE_ANDROID := 0
 | 
			
		||||
HAVE_LINUX_IOS := 0
 | 
			
		||||
HAVE_ANDROID :=
 | 
			
		||||
HAVE_LINUX_IOS :=
 | 
			
		||||
HAVE_LINUX_MACOS :=
 | 
			
		||||
else
 | 
			
		||||
$(error Unexpected host platform $(UNAME_S).)
 | 
			
		||||
endif
 | 
			
		||||
 | 
			
		||||
# Everything is set above.
 | 
			
		||||
$(info Building Tilde Friends $(VERSION_NUMBER) android=$(if $(HAVE_ANDROID),1,0) win=$(if $(HAVE_WIN),1,0) cross_aarch64=$(if $(HAVE_CROSS_AARCH64),1,0) cross_ios=$(if $(HAVE_LINUX_IOS),1,0) cross_macos=$(if $(HAVE_LINUX_MACOS),1,0))
 | 
			
		||||
 | 
			
		||||
CFLAGS += \
 | 
			
		||||
	-std=gnu11 \
 | 
			
		||||
	-Wall \
 | 
			
		||||
	-Wextra \
 | 
			
		||||
	-Wno-unused-parameter \
 | 
			
		||||
	-Wno-cast-function-type-mismatch \
 | 
			
		||||
	-Wno-unknown-warning-option \
 | 
			
		||||
	-Wno-unused-parameter \
 | 
			
		||||
	-MMD \
 | 
			
		||||
	-MP \
 | 
			
		||||
	-ffunction-sections \
 | 
			
		||||
@@ -83,14 +100,13 @@ CFLAGS += \
 | 
			
		||||
	-g
 | 
			
		||||
LDFLAGS += \
 | 
			
		||||
	-Wno-attributes \
 | 
			
		||||
	-Wno-aggressive-loop-optimizations \
 | 
			
		||||
	-flto=auto
 | 
			
		||||
	-Wno-aggressive-loop-optimizations
 | 
			
		||||
 | 
			
		||||
ANDROID_MIN_SDK_VERSION := 24
 | 
			
		||||
ANDROID_TARGET_SDK_VERSION := 34
 | 
			
		||||
ANDROID_BUILD_TOOLS := $(ANDROID_SDK)/build-tools/34.0.0
 | 
			
		||||
ANDROID_TARGET_SDK_VERSION := 35
 | 
			
		||||
ANDROID_BUILD_TOOLS := $(ANDROID_SDK)/build-tools/35.0.0
 | 
			
		||||
ANDROID_PLATFORM := $(ANDROID_SDK)/platforms/android-$(ANDROID_TARGET_SDK_VERSION)
 | 
			
		||||
ANDROID_NDK ?= $(ANDROID_SDK)/ndk/26.3.11579264
 | 
			
		||||
ANDROID_NDK ?= $(ANDROID_SDK)/ndk/27.2.12479018
 | 
			
		||||
 | 
			
		||||
ANDROID_ARMV7A_TARGETS := \
 | 
			
		||||
	out/androiddebug-armv7a/tildefriends \
 | 
			
		||||
@@ -127,6 +143,7 @@ WINDOWS_TARGETS := \
 | 
			
		||||
	out/winrelease/tildefriends.exe
 | 
			
		||||
ifeq ($(HAVE_WIN),1)
 | 
			
		||||
BUILD_TYPES += windebug winrelease
 | 
			
		||||
all: out/windebug/tildefriends.standalone.exe out/winrelease/tildefriends.standalone.exe
 | 
			
		||||
endif
 | 
			
		||||
 | 
			
		||||
AARCH64_TARGETS := \
 | 
			
		||||
@@ -136,12 +153,28 @@ ifeq ($(HAVE_CROSS_AARCH64),1)
 | 
			
		||||
BUILD_TYPES += armdebug armrelease
 | 
			
		||||
endif
 | 
			
		||||
 | 
			
		||||
LINUX_TARGETS := \
 | 
			
		||||
HOST_TARGETS := \
 | 
			
		||||
	out/debug/tildefriends \
 | 
			
		||||
	out/release/tildefriends
 | 
			
		||||
ifeq ($(UNAME_S),Darwin)
 | 
			
		||||
MACOS_TARGETS := \
 | 
			
		||||
	out/macosdebug/tildefriends \
 | 
			
		||||
	out/macosrelease/tildefriends
 | 
			
		||||
	out/debug/tildefriends \
 | 
			
		||||
	out/release/tildefriends
 | 
			
		||||
else ifeq ($(UNAME_S),Linux)
 | 
			
		||||
ifeq ($(HAVE_LINUX_MACOS),1)
 | 
			
		||||
MACOS_TARGETS := \
 | 
			
		||||
	out/macosdebug-arm/tildefriends \
 | 
			
		||||
	out/macosrelease-arm/tildefriends \
 | 
			
		||||
	out/macosdebug-x86_64/tildefriends \
 | 
			
		||||
	out/macosrelease-x86_64/tildefriends
 | 
			
		||||
all: out/macosdebug/tildefriends.standalone
 | 
			
		||||
all: out/macosrelease/tildefriends.standalone
 | 
			
		||||
else
 | 
			
		||||
MACOS_TARGETS :=
 | 
			
		||||
endif
 | 
			
		||||
else
 | 
			
		||||
MACOS_TARGETS :=
 | 
			
		||||
endif
 | 
			
		||||
IOS_TARGETS := \
 | 
			
		||||
	out/iosdebug/tildefriends \
 | 
			
		||||
	out/iosrelease/tildefriends
 | 
			
		||||
@@ -155,6 +188,14 @@ ifeq ($(HAVE_LINUX_IOS),1)
 | 
			
		||||
BUILD_TYPES += iosdebug iosrelease
 | 
			
		||||
all: $(IOS_APPS)
 | 
			
		||||
endif
 | 
			
		||||
ifeq ($(HAVE_LINUX_MACOS),1)
 | 
			
		||||
BUILD_TYPES += \
 | 
			
		||||
	macosdebug-arm \
 | 
			
		||||
	macosrelease-arm \
 | 
			
		||||
	macosdebug-x86_64 \
 | 
			
		||||
	macosrelease-x86_64
 | 
			
		||||
all: $(IOS_APPS)
 | 
			
		||||
endif
 | 
			
		||||
ifeq ($(UNAME_S),Darwin)
 | 
			
		||||
all: $(IOS_APPS) \
 | 
			
		||||
	out/tildefriends-iossimdebug.app/tildefriends \
 | 
			
		||||
@@ -169,35 +210,38 @@ DEBUG_TARGETS := \
 | 
			
		||||
	out/windebug/tildefriends.exe \
 | 
			
		||||
	out/iosdebug/tildefriends \
 | 
			
		||||
	out/iossimdebug/tildefriends \
 | 
			
		||||
	out/macosdebug/tildefriends \
 | 
			
		||||
	out/androiddebug/tildefriends \
 | 
			
		||||
	out/androiddebug-armv7a/tildefriends \
 | 
			
		||||
	out/androiddebug-x86_64/tildefriends \
 | 
			
		||||
	out/androiddebug-x86/tildefriends \
 | 
			
		||||
	out/armdebug/tildefriends
 | 
			
		||||
	out/armdebug/tildefriends \
 | 
			
		||||
	out/macosdebug-arm/tildefriends \
 | 
			
		||||
	out/macosdebug-x86_64/tildefriends
 | 
			
		||||
RELEASE_TARGETS := \
 | 
			
		||||
	out/release/tildefriends \
 | 
			
		||||
	out/winrelease/tildefriends.exe \
 | 
			
		||||
	out/iosrelease/tildefriends \
 | 
			
		||||
	out/iossimrelease/tildefriends \
 | 
			
		||||
	out/macosrelease/tildefriends \
 | 
			
		||||
	out/androidrelease/tildefriends \
 | 
			
		||||
	out/androidrelease-armv7a/tildefriends \
 | 
			
		||||
	out/androidrelease-x86_64/tildefriends \
 | 
			
		||||
	out/androidrelease-x86/tildefriends \
 | 
			
		||||
	out/armrelease/tildefriends
 | 
			
		||||
	out/armrelease/tildefriends \
 | 
			
		||||
	out/macosrelease-arm/tildefriends \
 | 
			
		||||
	out/macosrelease-x86_64/tildefriends
 | 
			
		||||
ALL_TARGETS = $(DEBUG_TARGETS) $(RELEASE_TARGETS)
 | 
			
		||||
ANDROID_RELEASE_TARGETS := $(filter-out $(DEBUG_TARGETS),$(ANDROID_TARGETS))
 | 
			
		||||
NONANDROID_RELEASE_TARGETS := $(filter-out $(ANDROID_ARM64_TARGETS),$(RELEASE_TARGETS))
 | 
			
		||||
NONANDROID_TARGETS := $(filter-out $(ANDROID_TARGETS),$(ALL_TARGETS))
 | 
			
		||||
NONMACOS_TARGETS := $(filter-out $(MACOS_TARGETS) $(IOS_TARGETS) $(IOSSIM_TARGETS),$(ALL_TARGETS))
 | 
			
		||||
DEADSTRIP_TARGETS := $(filter-out $(ANDROID_TARGETS),$(NONMACOS_TARGETS))
 | 
			
		||||
ifneq ($(UNAME_S),OpenBSD)
 | 
			
		||||
$(NONMACOS_TARGETS): LDFLAGS += -static-libgcc
 | 
			
		||||
endif
 | 
			
		||||
 | 
			
		||||
$(NONANDROID_TARGETS): CFLAGS += -fno-omit-frame-pointer
 | 
			
		||||
$(filter-out $(WINDOWS_TARGETS),$(ALL_TARGETS)): LDFLAGS += -rdynamic
 | 
			
		||||
$(filter-out $(WINDOWS_TARGETS),$(ALL_TARGETS)): LDFLAGS += \
 | 
			
		||||
	-rdynamic \
 | 
			
		||||
	-gz=zlib
 | 
			
		||||
$(ANDROID_TARGETS): CFLAGS += \
 | 
			
		||||
	--sysroot $(ANDROID_NDK)/toolchains/llvm/prebuilt/linux-x86_64/sysroot \
 | 
			
		||||
	-fPIC \
 | 
			
		||||
@@ -206,38 +250,43 @@ $(ANDROID_TARGETS): CFLAGS += \
 | 
			
		||||
	-fno-asynchronous-unwind-tables \
 | 
			
		||||
	-funwind-tables \
 | 
			
		||||
	-Wno-unknown-warning-option
 | 
			
		||||
$(ANDROID_TARGETS): LDFLAGS += --sysroot $(ANDROID_NDK)/toolchains/llvm/prebuilt/linux-x86_64/sysroot -fPIC
 | 
			
		||||
$(ANDROID_TARGETS): LDFLAGS += \
 | 
			
		||||
	--sysroot $(ANDROID_NDK)/toolchains/llvm/prebuilt/linux-x86_64/sysroot \
 | 
			
		||||
	-Wl,-z,max-page-size=16384 \
 | 
			
		||||
	-fPIC
 | 
			
		||||
$(DEBUG_TARGETS): CFLAGS += -DDEBUG -Og
 | 
			
		||||
$(DEBUG_TARGETS): LDFLAGS += -Og
 | 
			
		||||
$(RELEASE_TARGETS): CFLAGS += \
 | 
			
		||||
	-DNDEBUG \
 | 
			
		||||
	-flto \
 | 
			
		||||
	-Oz
 | 
			
		||||
$(RELEASE_TARGETS): LDFLAGS += -Oz
 | 
			
		||||
	-DNDEBUG
 | 
			
		||||
$(ANDROID_RELEASE_TARGETS): CFLAGS += -Oz
 | 
			
		||||
$(ANDROID_RELEASE_TARGETS): LDFLAGS += -Oz
 | 
			
		||||
$(NONANDROID_RELEASE_TARGETS): CFLAGS += -Os
 | 
			
		||||
$(NONANDROID_RELEASE_TARGETS): LDFLAGS += -Os
 | 
			
		||||
$(WINDOWS_TARGETS): CC = x86_64-w64-mingw32-gcc-win32
 | 
			
		||||
$(WINDOWS_TARGETS): AS = $(CC)
 | 
			
		||||
$(WINDOWS_TARGETS): CFLAGS += \
 | 
			
		||||
	-D_WIN32_WINNT=0x0A00 \
 | 
			
		||||
	-DWINVER=0x0A00 \
 | 
			
		||||
	-DNTDDI_VERSION=NTDDI_WIN10 \
 | 
			
		||||
	-Ideps/openssl/mingw64/usr/local/include
 | 
			
		||||
	-DNTDDI_VERSION=NTDDI_WIN10
 | 
			
		||||
$(WINDOWS_TARGETS): LDFLAGS += \
 | 
			
		||||
	-static \
 | 
			
		||||
	-lm \
 | 
			
		||||
	-Ldeps/openssl/mingw64/usr/local/lib
 | 
			
		||||
	-lm
 | 
			
		||||
$(AARCH64_TARGETS): CC = aarch64-linux-gnu-gcc
 | 
			
		||||
$(AARCH64_TARGETS): AS = $(CC)
 | 
			
		||||
$(AARCH64_TARGETS): CFLAGS += -Ideps/openssl/Linux/aarch64/usr/local/include
 | 
			
		||||
$(AARCH64_TARGETS): LDFLAGS += -Ldeps/openssl/Linux/aarch64/usr/local/lib
 | 
			
		||||
ifeq ($(UNAME_S),Darwin)
 | 
			
		||||
$(MACOS_TARGETS): CC = xcrun clang
 | 
			
		||||
$(HOST_TARGETS): CC = xcrun clang
 | 
			
		||||
$(IOS_TARGETS): IOS_SYSROOT := $(shell xcrun --sdk iphoneos --show-sdk-path)
 | 
			
		||||
$(IOS_TARGETS): CC = xcrun --sdk iphoneos clang -isysroot $(IOS_SYSROOT) -arch arm64
 | 
			
		||||
$(IOSSIM_TARGETS): IOSSIM_SYSROOT := $(shell xcrun --sdk iphonesimulator --show-sdk-path)
 | 
			
		||||
$(IOSSIM_TARGETS): CC = xcrun --sdk iphonesimulator clang -isysroot $(IOSSIM_SYSROOT) -arch x86_64
 | 
			
		||||
else ifeq ($(UNAME_S),Linux)
 | 
			
		||||
$(IOS_TARGETS): IOS_SYSROOT := deps/iPhoneOS17.0.sdk
 | 
			
		||||
$(IOS_TARGETS): CC = PATH=$$PATH:deps/ios_toolchain/target/bin deps/ios_toolchain/target/bin/arm-apple-darwin11-clang
 | 
			
		||||
$(IOS_TARGETS): CFLAGS += -isysroot deps/ios_toolchain/target/SDKs/iPhoneOS18.2.sdk -arch arm64 -DTARGET_OS_IPHONE=1
 | 
			
		||||
$(IOS_TARGETS): LDFLAGS += -isysroot deps/ios_toolchain/target/SDKs/iPhoneOS18.2.sdk
 | 
			
		||||
$(IOS_TARGETS): CC = PATH=$$PATH:deps/ios_toolchain/target/bin LD_LIBRARY_PATH=$$LD_LIBRARY_PATH:deps/ios_toolchain/target/lib deps/ios_toolchain/target/bin/arm-apple-darwin11-clang
 | 
			
		||||
$(filter $(BUILD_DIR)/macosdebug-x86_64/%,$(ALL_TARGETS)): CC = PATH=deps/macos_toolchain/bin:$$PATH LD_LIBRARY_PATH=$$LD_LIBRARY_PATH:deps/macos_toolchain/lib deps/macos_toolchain/bin/o64-clang
 | 
			
		||||
$(filter $(BUILD_DIR)/macosdebug-arm/%,$(ALL_TARGETS)): CC = PATH=deps/macos_toolchain/bin:$$PATH LD_LIBRARY_PATH=$$LD_LIBRARY_PATH:deps/macos_toolchain/lib deps/macos_toolchain/bin/oa64-clang
 | 
			
		||||
$(filter $(BUILD_DIR)/macosrelease-x86_64/%,$(ALL_TARGETS)): CC = PATH=deps/macos_toolchain/bin:$$PATH LD_LIBRARY_PATH=$$LD_LIBRARY_PATH:deps/macos_toolchain/lib deps/macos_toolchain/bin/o64-clang
 | 
			
		||||
$(filter $(BUILD_DIR)/macosrelease-arm/%,$(ALL_TARGETS)): CC = PATH=deps/macos_toolchain/bin:$$PATH LD_LIBRARY_PATH=$$LD_LIBRARY_PATH:deps/macos_toolchain/lib deps/macos_toolchain/bin/oa64-clang
 | 
			
		||||
endif
 | 
			
		||||
$(ANDROID_X86_64_TARGETS): ANDROID_NDK_TARGET_TRIPLE := x86_64-linux-android
 | 
			
		||||
$(ANDROID_X86_TARGETS): ANDROID_NDK_TARGET_TRIPLE := i686-linux-android
 | 
			
		||||
@@ -248,23 +297,12 @@ $(ANDROID_TARGETS): AS = $(CC)
 | 
			
		||||
$(ANDROID_TARGETS): CFLAGS += \
 | 
			
		||||
	-target $(ANDROID_NDK_TARGET_TRIPLE)$(ANDROID_MIN_SDK_VERSION) \
 | 
			
		||||
	-Wno-unknown-warning-option
 | 
			
		||||
$(ANDROID_ARMV7A_TARGETS): CFLAGS += -Ideps/openssl/android/armeabi-v7a/usr/local/include
 | 
			
		||||
$(ANDROID_ARMV7A_TARGETS): LDFLAGS += -Ldeps/openssl/android/armeabi-v7a/usr/local/lib
 | 
			
		||||
$(ANDROID_ARM64_TARGETS): CFLAGS += -Ideps/openssl/android/arm64-v8a/usr/local/include
 | 
			
		||||
$(ANDROID_ARM64_TARGETS): LDFLAGS += -Ldeps/openssl/android/arm64-v8a/usr/local/lib
 | 
			
		||||
$(ANDROID_X86_TARGETS): CFLAGS += -Ideps/openssl/android/x86/usr/local/include
 | 
			
		||||
$(ANDROID_X86_TARGETS): CFLAGS += -Wno-atomic-alignment
 | 
			
		||||
$(ANDROID_X86_TARGETS): LDFLAGS += -Ldeps/openssl/android/x86/usr/local/lib
 | 
			
		||||
$(ANDROID_X86_64_TARGETS): CFLAGS += -Ideps/openssl/android/x86_64/usr/local/include
 | 
			
		||||
$(ANDROID_X86_64_TARGETS): LDFLAGS += -Ldeps/openssl/android/x86_64/usr/local/lib
 | 
			
		||||
$(NONMACOS_TARGETS): CFLAGS += -Wno-cast-function-type
 | 
			
		||||
$(DEADSTRIP_TARGETS): LDFLAGS += -Wl,--gc-sections
 | 
			
		||||
$(IOS_TARGETS): CFLAGS += -miphoneos-version-min=9.0 -Ideps/openssl/ios/ios64-xcrun/usr/local/include
 | 
			
		||||
$(IOS_TARGETS): LDFLAGS += -miphoneos-version-min=9.0 -Ldeps/openssl/ios/ios64-xcrun/usr/local/lib
 | 
			
		||||
$(IOSSIM_TARGETS): CFLAGS += -Ideps/openssl/ios/iossimulator-xcrun/usr/local/include
 | 
			
		||||
$(IOSSIM_TARGETS): LDFLAGS += -Ldeps/openssl/ios/iossimulator-xcrun/usr/local/lib
 | 
			
		||||
$(LINUX_TARGETS) $(MACOS_TARGETS): CFLAGS += -Ideps/openssl/$(UNAME_S)/$(UNAME_M)/usr/local/include
 | 
			
		||||
$(LINUX_TARGETS) $(MACOS_TARGETS): LDFLAGS += -Ldeps/openssl/$(UNAME_S)/$(UNAME_M)/usr/local/lib
 | 
			
		||||
$(MACOS_TARGETS): LDFLAGS += -Wl,-dead_strip
 | 
			
		||||
$(NONMACOS_TARGETS): LDFLAGS += -Wl,--gc-sections -Wl,--as-needed
 | 
			
		||||
$(IOS_TARGETS): CFLAGS += -miphoneos-version-min=$(IPHONEOS_VERSION_MIN)
 | 
			
		||||
$(IOS_TARGETS): LDFLAGS += -miphoneos-version-min=$(IPHONEOS_VERSION_MIN)
 | 
			
		||||
 | 
			
		||||
ifeq ($(UNAME_M),x86_64)
 | 
			
		||||
ifeq ($(UNAME_S),Linux)
 | 
			
		||||
@@ -283,13 +321,14 @@ endif
 | 
			
		||||
 | 
			
		||||
get_objs = \
 | 
			
		||||
	$(foreach build_type,$(BUILD_TYPES),$(addprefix $(BUILD_DIR)/$(build_type)/,$(addsuffix .o,$(basename $(value $(1)))))) \
 | 
			
		||||
	$(foreach build_type,debug release armdebug armrelease,$(addprefix $(BUILD_DIR)/$(build_type)/,$(addsuffix .o,$(basename $(value $(1)_unix))))) \
 | 
			
		||||
	$(foreach build_type,windebug winrelease,$(addprefix $(BUILD_DIR)/$(build_type)/,$(addsuffix .o,$(basename $(value $(1)_win))))) \
 | 
			
		||||
	$(foreach build_type,androiddebug androidrelease androiddebug-x86 androidrelease-x86 androiddebug-x86_64 androidrelease-x86_64 androiddebug-armv7a androiddebug-armv7a,$(addprefix $(BUILD_DIR)/$(build_type)/,$(addsuffix .o,$(basename $(value $(1)_android))))) \
 | 
			
		||||
	$(foreach build_type,androiddebug androidrelease androiddebug-x86 androidrelease-x86 androiddebug-x86_64 androidrelease-x86_64 androiddebug-armv7a androidrelease-armv7a,$(addprefix $(BUILD_DIR)/$(build_type)/,$(addsuffix .o,$(basename $(value $(1)_android))))) \
 | 
			
		||||
	$(foreach build_type,androiddebug androidrelease androiddebug-x86 androidrelease-x86 androiddebug-x86_64 androidrelease-x86_64 androiddebug-armv7a androidrelease-armv7a,$(addprefix $(BUILD_DIR)/$(build_type)/,$(addsuffix .o,$(basename $(value $(1)_unix))))) \
 | 
			
		||||
	$(foreach build_type,macosdebug macosrelease iosdebug iosrelease iossimdebug iossimrelease,$(addprefix $(BUILD_DIR)/$(build_type)/,$(addsuffix .o,$(basename $(value $(1)_macos))))) \
 | 
			
		||||
	$(foreach build_type,iosdebug iosrelease iossimdebug iossimrelease,$(addprefix $(BUILD_DIR)/$(build_type)/,$(addsuffix .o,$(basename $(value $(1)_ios))))) \
 | 
			
		||||
	$(foreach build_type,androiddebug-x86 androidrelease-x86,$(addprefix $(BUILD_DIR)/$(build_type)/,$(addsuffix .o,$(basename $(value $(1)_x86)))))
 | 
			
		||||
	$(foreach build_type,iosdebug iosrelease iossimdebug iossimrelease macosdebug-arm macosrelease-arm macosdebug-x86_64 macosrelease-x86_64,$(addprefix $(BUILD_DIR)/$(build_type)/,$(addsuffix .o,$(basename $(value $(1)_macos))))) \
 | 
			
		||||
	$(foreach build_type,androiddebug-x86 androidrelease-x86,$(addprefix $(BUILD_DIR)/$(build_type)/,$(addsuffix .o,$(basename $(value $(1)_x86))))) \
 | 
			
		||||
	$(if $(findstring Darwin,$(UNAME_S)),$(foreach build_type,debug release,$(addprefix $(BUILD_DIR)/$(build_type)/,$(addsuffix .o,$(basename $(value $(1)_macos)))))) \
 | 
			
		||||
	$(if $(findstring Darwin,$(UNAME_S)),,$(foreach build_type,debug release armdebug armrelease,$(addprefix $(BUILD_DIR)/$(build_type)/,$(addsuffix .o,$(basename $(value $(1)_unix))))))
 | 
			
		||||
 | 
			
		||||
APP_SOURCES := $(wildcard src/*.c)
 | 
			
		||||
APP_SOURCES_ios := $(wildcard src/*.m)
 | 
			
		||||
@@ -311,10 +350,12 @@ $(APP_OBJS): CFLAGS += \
 | 
			
		||||
	-Ideps/valgrind \
 | 
			
		||||
	-Wdouble-promotion \
 | 
			
		||||
	-Werror
 | 
			
		||||
ifneq ($(UNAME_S),Darwin)
 | 
			
		||||
ifeq ($(UNAME_M),x86_64)
 | 
			
		||||
$(filter-out $(BUILD_DIR)/android% $(BUILD_DIR)/macos% $(BUILD_DIR)/ios%,$(APP_OBJS)): CFLAGS += \
 | 
			
		||||
$(filter-out $(BUILD_DIR)/android% $(BUILD_DIR)/ios% $(BUILD_DIR)/macos%,$(APP_OBJS)): CFLAGS += \
 | 
			
		||||
	-fanalyzer
 | 
			
		||||
endif
 | 
			
		||||
endif
 | 
			
		||||
 | 
			
		||||
ARES_SOURCES := \
 | 
			
		||||
	deps/c-ares/src/lib/ares_addrinfo2hostent.c \
 | 
			
		||||
@@ -542,16 +583,16 @@ $(UV_OBJS): CFLAGS += \
 | 
			
		||||
	-Ideps/libuv/include \
 | 
			
		||||
	-Ideps/libuv/src \
 | 
			
		||||
	-Wno-dangling-pointer \
 | 
			
		||||
	-Wno-format-truncation \
 | 
			
		||||
	-Wno-incompatible-pointer-types \
 | 
			
		||||
	-Wno-maybe-uninitialized \
 | 
			
		||||
	-Wno-nonnull \
 | 
			
		||||
	-Wno-sign-compare \
 | 
			
		||||
	-Wno-unknown-attributes \
 | 
			
		||||
	-Wno-unused-but-set-parameter \
 | 
			
		||||
	-Wno-unused-but-set-variable \
 | 
			
		||||
	-Wno-unused-result \
 | 
			
		||||
	-Wno-unused-variable \
 | 
			
		||||
	-Wno-nonnull
 | 
			
		||||
$(UV_OBJS): CFLAGS += -fno-lto
 | 
			
		||||
	-Wno-unused-variable
 | 
			
		||||
$(filter out/win%,$(UV_OBJS)): \
 | 
			
		||||
	CFLAGS += \
 | 
			
		||||
		-Wno-cast-function-type \
 | 
			
		||||
@@ -578,6 +619,7 @@ SODIUM_SOURCES := \
 | 
			
		||||
	deps/libsodium/src/libsodium/crypto_core/hsalsa20/ref2/core_hsalsa20_ref2.c \
 | 
			
		||||
	deps/libsodium/src/libsodium/crypto_core/salsa/ref/core_salsa_ref.c \
 | 
			
		||||
	deps/libsodium/src/libsodium/crypto_core/softaes/softaes.c \
 | 
			
		||||
	deps/libsodium/src/libsodium/crypto_generichash/crypto_generichash.c \
 | 
			
		||||
	deps/libsodium/src/libsodium/crypto_generichash/blake2b/ref/blake2b-compress-ref.c \
 | 
			
		||||
	deps/libsodium/src/libsodium/crypto_generichash/blake2b/ref/blake2b-ref.c \
 | 
			
		||||
	deps/libsodium/src/libsodium/crypto_generichash/blake2b/ref/generichash_blake2b.c \
 | 
			
		||||
@@ -647,12 +689,12 @@ $(SQLITE_OBJS): CFLAGS += \
 | 
			
		||||
	-DSQLITE_MAX_COMPOUND_SELECT=300 \
 | 
			
		||||
	-DSQLITE_MAX_EXPR_DEPTH=40 \
 | 
			
		||||
	-DSQLITE_MAX_FUNCTION_ARG=8 \
 | 
			
		||||
	-DSQLITE_MAX_LENGTH=5242880 \
 | 
			
		||||
	-DSQLITE_MAX_LENGTH=10485760 \
 | 
			
		||||
	-DSQLITE_MAX_LIKE_PATTERN_LENGTH=50 \
 | 
			
		||||
	-DSQLITE_MAX_SQL_LENGTH=100000 \
 | 
			
		||||
	-DSQLITE_MAX_TRIGGER_DEPTH=10 \
 | 
			
		||||
	-DSQLITE_MAX_VARIABLE_NUMBER=100 \
 | 
			
		||||
	-DSQLITE_MAX_VDBE_OP=25000 \
 | 
			
		||||
	-DSQLITE_MAX_VDBE_OP=50000 \
 | 
			
		||||
	-DSQLITE_OMIT_DEPRECATED \
 | 
			
		||||
	-DSQLITE_OMIT_DESERIALIZE \
 | 
			
		||||
	-DSQLITE_OMIT_LOAD_EXTENSION \
 | 
			
		||||
@@ -671,7 +713,7 @@ $(SQLITE_OBJS): CFLAGS += \
 | 
			
		||||
 | 
			
		||||
QUICKJS_SOURCES := \
 | 
			
		||||
	deps/quickjs/cutils.c \
 | 
			
		||||
	deps/quickjs/libbf.c \
 | 
			
		||||
	deps/quickjs/dtoa.c \
 | 
			
		||||
	deps/quickjs/libregexp.c \
 | 
			
		||||
	deps/quickjs/libunicode.c \
 | 
			
		||||
	deps/quickjs/quickjs.c
 | 
			
		||||
@@ -748,18 +790,13 @@ $(MINIUNZIP_OBJS): CFLAGS += \
 | 
			
		||||
LDFLAGS += \
 | 
			
		||||
	-pthread \
 | 
			
		||||
	-lm
 | 
			
		||||
$(LINUX_TARGETS) $(MACOS_TARGETS) $(IOS_TARGETS) $(IOSSIM_TARGETS) $(AARCH64_TARGETS): LDFLAGS += \
 | 
			
		||||
	-lssl \
 | 
			
		||||
	-lcrypto
 | 
			
		||||
ifneq ($(UNAME_S),Haiku)
 | 
			
		||||
ifneq ($(UNAME_S),OpenBSD)
 | 
			
		||||
debug release $(MACOS_TARGETS) $(IOS_TARGETS) $(IOSSIM_TARGETS): LDFLAGS += \
 | 
			
		||||
$(HOST_TARGETS) $(IOS_TARGETS) $(IOSSIM_TARGETS): LDFLAGS += \
 | 
			
		||||
	-ldl
 | 
			
		||||
endif
 | 
			
		||||
endif
 | 
			
		||||
$(WINDOWS_TARGETS): LDFLAGS += \
 | 
			
		||||
	-lssl \
 | 
			
		||||
	-lcrypto \
 | 
			
		||||
	-lcrypt32 \
 | 
			
		||||
	-ldbghelp \
 | 
			
		||||
	-liphlpapi \
 | 
			
		||||
@@ -772,9 +809,7 @@ $(WINDOWS_TARGETS): LDFLAGS += \
 | 
			
		||||
$(ANDROID_TARGETS): LDFLAGS += \
 | 
			
		||||
	-target $(ANDROID_NDK_TARGET_TRIPLE)$(ANDROID_MIN_SDK_VERSION) \
 | 
			
		||||
	-ldl \
 | 
			
		||||
	-llog \
 | 
			
		||||
	-lssl \
 | 
			
		||||
	-lcrypto
 | 
			
		||||
	-llog
 | 
			
		||||
$(MACOS_TARGETS) $(IOS_TARGETS) $(IOSSIM_TARGETS): CFLAGS += \
 | 
			
		||||
	-Wno-unknown-warning-option
 | 
			
		||||
$(IOS_TARGETS) $(IOSSIM_TARGETS): LDFLAGS += \
 | 
			
		||||
@@ -786,27 +821,15 @@ $(IOS_TARGETS) $(IOSSIM_TARGETS): LDFLAGS += \
 | 
			
		||||
##
 | 
			
		||||
## Common targets:
 | 
			
		||||
##
 | 
			
		||||
debug: ## Build a debug executable for the current platform.
 | 
			
		||||
release: ## Build a release executable for the current platform.
 | 
			
		||||
all: $(BUILD_TYPES) ## Build all targets that appear possible to build on this machine.
 | 
			
		||||
debug: ## Build a debug executable for the current host platform.
 | 
			
		||||
release: ## Build a release executable for the current host platform.
 | 
			
		||||
armdebug: ## Cross-compile aarch64 debug on Linux.
 | 
			
		||||
armrelease: ## Cross-compile aarch64 release on Linux.
 | 
			
		||||
all: $(BUILD_TYPES) ## Build all targets that appear possible to build on this machine.
 | 
			
		||||
unix: debug release ## Build all UNIX targets.
 | 
			
		||||
win: windebug winrelease ## Build all Windows targets.
 | 
			
		||||
windebug: ## Cross-compile a debug win32 executable on Linux.
 | 
			
		||||
winrelease: ## Cross-compile a release win32 executable on Linux.
 | 
			
		||||
.PHONY: all win unix
 | 
			
		||||
 | 
			
		||||
##
 | 
			
		||||
## Windows targets:
 | 
			
		||||
##
 | 
			
		||||
windebug: ## Build a debug win32 executable.
 | 
			
		||||
winrelease: ## Build a release win32 executable.
 | 
			
		||||
 | 
			
		||||
##
 | 
			
		||||
## MacOS targets:
 | 
			
		||||
##
 | 
			
		||||
macosdebug: ## Build a MacOS debug executable.
 | 
			
		||||
macosrelease: ## Build a MacOS release executable.
 | 
			
		||||
 | 
			
		||||
ALL_APP_OBJS := \
 | 
			
		||||
	$(APP_OBJS) \
 | 
			
		||||
	$(ARES_OBJS) \
 | 
			
		||||
@@ -862,6 +885,17 @@ src/android/AndroidManifest.xml : $(firstword $(MAKEFILE_LIST))
 | 
			
		||||
		-e 's/android:targetSdkVersion="[[:digit:]]*"/android:targetSdkVersion="$(ANDROID_TARGET_SDK_VERSION)"/' \
 | 
			
		||||
		$@
 | 
			
		||||
 | 
			
		||||
src/ios/Info.plist : $(firstword $(MAKEFILE_LIST))
 | 
			
		||||
	@echo "[ios_version] $@"
 | 
			
		||||
	@cat $@ | \
 | 
			
		||||
		tr '\n' '^' | \
 | 
			
		||||
		sed -r \
 | 
			
		||||
			-e 's@(<key>CFBundleShortVersionString</key>\^[[:space:]]*<string>)[0-9.]*(</string>)@\1$(VERSION_NUMBER:%-wip=%)\2@' \
 | 
			
		||||
			-e 's@(<key>CFBundleVersion</key>\^[[:space:]]*<string>)[[:digit:]]+(</string>)@\1$(VERSION_CODE_IOS)\2@' \
 | 
			
		||||
			-e 's@(<key>MinimumOSVersion</key>\^[[:space:]]*<string>)[0-9.]*(</string>)@\1$(IPHONEOS_VERSION_MIN)\2@' | \
 | 
			
		||||
		tr '^' '\n' > \
 | 
			
		||||
		$@.tmp && mv $@.tmp $@ || rm -f $@.tmp
 | 
			
		||||
 | 
			
		||||
##
 | 
			
		||||
## Android targets:
 | 
			
		||||
##
 | 
			
		||||
@@ -879,29 +913,30 @@ out/res/layout_activity_main.xml.flat: src/android/res/layout/activity_main.xml
 | 
			
		||||
	@echo "[aapt2] $@"
 | 
			
		||||
	@$(ANDROID_BUILD_TOOLS)/aapt2 compile -o out/res/ src/android/res/layout/activity_main.xml
 | 
			
		||||
 | 
			
		||||
out/res/drawable_icon.xml.flat: src/android/res/drawable/icon.xml
 | 
			
		||||
out/res/drawable_%.xml.flat: src/android/res/drawable/%.xml
 | 
			
		||||
	@mkdir -p $(dir $@)
 | 
			
		||||
	@echo "[aapt2] $@"
 | 
			
		||||
	@$(ANDROID_BUILD_TOOLS)/aapt2 compile -o out/res/ src/android/res/drawable/icon.xml
 | 
			
		||||
	@$(ANDROID_BUILD_TOOLS)/aapt2 compile -o out/res/ $(<)
 | 
			
		||||
 | 
			
		||||
out/apk/res.apk out/gen/com/unprompted/tildefriends/R.java: out/res/layout_activity_main.xml.flat out/res/drawable_icon.xml.flat src/android/AndroidManifest.xml
 | 
			
		||||
out/apk/res.apk out/gen/com/unprompted/tildefriends/R.java: out/res/layout_activity_main.xml.flat out/res/drawable_icon.xml.flat out/res/drawable_logo.xml.flat src/android/AndroidManifest.xml
 | 
			
		||||
	@echo [aapt2 link] res.apk
 | 
			
		||||
	@mkdir -p out/apk/
 | 
			
		||||
	@$(ANDROID_BUILD_TOOLS)/aapt2 link -I $(ANDROID_PLATFORM)/android.jar out/res/layout_activity_main.xml.flat out/res/drawable_icon.xml.flat \
 | 
			
		||||
	@$(ANDROID_BUILD_TOOLS)/aapt2 link -I $(ANDROID_PLATFORM)/android.jar out/res/layout_activity_main.xml.flat out/res/drawable_icon.xml.flat out/res/drawable_logo.xml.flat \
 | 
			
		||||
		--min-sdk-version $(ANDROID_MIN_SDK_VERSION) \
 | 
			
		||||
		--target-sdk-version $(ANDROID_TARGET_SDK_VERSION) \
 | 
			
		||||
		--manifest src/android/AndroidManifest.xml \
 | 
			
		||||
		-o out/apk/res.apk \
 | 
			
		||||
		--java out/gen/
 | 
			
		||||
 | 
			
		||||
out/apk/res.fdroid.apk out/gen_fdroid/com/unprompted/tildefriends/R.java: out/res/layout_activity_main.xml.flat out/res/drawable_icon.xml.flat src/android/AndroidManifest.xml
 | 
			
		||||
out/apk/res.fdroid.apk out/gen_fdroid/com/unprompted/tildefriends/R.java: out/res/layout_activity_main.xml.flat out/res/drawable_icon_fdroid.xml.flat out/res/drawable_logo.xml.flat src/android/AndroidManifest.xml
 | 
			
		||||
	@echo [aapt2 link] res.fdroid.apk
 | 
			
		||||
	@mkdir -p out/apk/
 | 
			
		||||
	@$(ANDROID_BUILD_TOOLS)/aapt2 link -I $(ANDROID_PLATFORM)/android.jar out/res/layout_activity_main.xml.flat out/res/drawable_icon.xml.flat \
 | 
			
		||||
	@sed -e 's@drawable/icon@drawable/icon_fdroid@' src/android/AndroidManifest.xml > out/apk/AndroidManifest.fdroid.xml
 | 
			
		||||
	@$(ANDROID_BUILD_TOOLS)/aapt2 link -I $(ANDROID_PLATFORM)/android.jar out/res/layout_activity_main.xml.flat out/res/drawable_icon_fdroid.xml.flat out/res/drawable_logo.xml.flat \
 | 
			
		||||
		--min-sdk-version $(ANDROID_MIN_SDK_VERSION) \
 | 
			
		||||
		--target-sdk-version $(ANDROID_TARGET_SDK_VERSION) \
 | 
			
		||||
		--rename-manifest-package com.unprompted.tildefriends.fdroid \
 | 
			
		||||
		--manifest src/android/AndroidManifest.xml \
 | 
			
		||||
		--manifest out/apk/AndroidManifest.fdroid.xml \
 | 
			
		||||
		-o out/apk/res.fdroid.apk \
 | 
			
		||||
		--java out/gen_fdroid/
 | 
			
		||||
 | 
			
		||||
@@ -918,11 +953,12 @@ out/apk/classes.dex: $(CLASS_FILES)
 | 
			
		||||
	@$(ANDROID_BUILD_TOOLS)/d8 --lib $(ANDROID_PLATFORM)/android.jar --output $(dir $@) out/classes/com/unprompted/tildefriends/*.class
 | 
			
		||||
 | 
			
		||||
PACKAGE_DIRS := \
 | 
			
		||||
	apps/ \
 | 
			
		||||
	core/ \
 | 
			
		||||
	deps/codemirror/ \
 | 
			
		||||
	deps/prettier/ \
 | 
			
		||||
	deps/lit/
 | 
			
		||||
	apps \
 | 
			
		||||
	core \
 | 
			
		||||
	deps/codemirror \
 | 
			
		||||
	deps/prettier \
 | 
			
		||||
	deps/lit \
 | 
			
		||||
	deps/speedscope
 | 
			
		||||
 | 
			
		||||
RAW_FILES := $(sort $(filter-out apps/blog% apps/issues% apps/welcome% apps/journal% %.map, $(shell find $(PACKAGE_DIRS) -type f -not -name '.*')))
 | 
			
		||||
 | 
			
		||||
@@ -943,7 +979,7 @@ $(BUNDLETOOL):
 | 
			
		||||
	@curl -q -L --create-dirs -o $@ $(BUNDLETOOL_URL)
 | 
			
		||||
 | 
			
		||||
out/TildeFriends.aab: out/apk/classes.dex $(filter-out %debug%, $(ANDROID_TARGETS)) $(RAW_FILES) out/apk/res.apk src/android/AndroidManifest.xml $(BUNDLETOOL)
 | 
			
		||||
	@rm -rf out/aab/staging/
 | 
			
		||||
	@rm -rf out/aab/staging/ out/aab/base.zip
 | 
			
		||||
	@mkdir -p out/aab/staging
 | 
			
		||||
	@$(ANDROID_BUILD_TOOLS)/aapt2 link --proto-format -o out/aab/temporary.apk \
 | 
			
		||||
		-I $(ANDROID_PLATFORM)/android.jar \
 | 
			
		||||
@@ -952,6 +988,7 @@ out/TildeFriends.aab: out/apk/classes.dex $(filter-out %debug%, $(ANDROID_TARGET
 | 
			
		||||
		--manifest src/android/AndroidManifest.xml \
 | 
			
		||||
		-R out/res/layout_activity_main.xml.flat \
 | 
			
		||||
		-R out/res/drawable_icon.xml.flat \
 | 
			
		||||
		-R out/res/drawable_logo.xml.flat \
 | 
			
		||||
		--auto-add-overlay
 | 
			
		||||
	@unzip out/aab/temporary.apk -d out/aab/staging/
 | 
			
		||||
	@mkdir -p out/aab/staging/root/deps
 | 
			
		||||
@@ -962,14 +999,11 @@ out/TildeFriends.aab: out/apk/classes.dex $(filter-out %debug%, $(ANDROID_TARGET
 | 
			
		||||
	@cp out/apk/classes.dex out/aab/staging/dex/
 | 
			
		||||
	@rm -fv out/base.zip
 | 
			
		||||
	@mkdir -p out/aab/staging/lib/arm64-v8a out/aab/staging/lib/armeabi-v7a out/aab/staging/lib/x86_64 out/aab/staging/lib/x86
 | 
			
		||||
	@cp out/androidrelease/tildefriends out/aab/staging/lib/arm64-v8a/libtildefriends.so
 | 
			
		||||
	@cp out/androidrelease-armv7a/tildefriends out/aab/staging/lib/armeabi-v7a/libtildefriends.so
 | 
			
		||||
	@cp out/androidrelease-x86_64/tildefriends out/aab/staging/lib/x86_64/libtildefriends.so
 | 
			
		||||
	@cp out/androidrelease-x86/tildefriends out/aab/staging/lib/x86/libtildefriends.so
 | 
			
		||||
	@$(ANDROID_NDK)/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-strip out/aab/staging/lib/arm64-v8a/libtildefriends.so
 | 
			
		||||
	@$(ANDROID_NDK)/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-strip out/aab/staging/lib/armeabi-v7a/libtildefriends.so
 | 
			
		||||
	@$(ANDROID_NDK)/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-strip out/aab/staging/lib/x86_64/libtildefriends.so
 | 
			
		||||
	@$(ANDROID_NDK)/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-strip out/aab/staging/lib/x86/libtildefriends.so
 | 
			
		||||
	@mkdir -p out/aab/staging/BUNDLE-METADATA/com.android.tools.build.debugsymbols/arm64-v8a out/aab/staging/BUNDLE-METADATA/com.android.tools.build.debugsymbols/armeabi-v7a out/aab/staging/BUNDLE-METADATA/com.android.tools.build.debugsymbols/x86_64 out/aab/staging/BUNDLE-METADATA/com.android.tools.build.debugsymbols/x86
 | 
			
		||||
	@$(ANDROID_NDK)/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-strip out/androidrelease/tildefriends -o out/aab/staging/lib/arm64-v8a/libtildefriends.so
 | 
			
		||||
	@$(ANDROID_NDK)/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-strip out/androidrelease-armv7a/tildefriends -o out/aab/staging/lib/armeabi-v7a/libtildefriends.so
 | 
			
		||||
	@$(ANDROID_NDK)/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-strip out/androidrelease-x86_64/tildefriends -o out/aab/staging/lib/x86_64/libtildefriends.so
 | 
			
		||||
	@$(ANDROID_NDK)/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-strip out/androidrelease-x86/tildefriends -o out/aab/staging/lib/x86/libtildefriends.so
 | 
			
		||||
	@cp -r apps/ out/aab/staging/root/
 | 
			
		||||
	@rm -rf out/aab/staging/root/apps/welcome*
 | 
			
		||||
	@cp -r core/ out/aab/staging/root/
 | 
			
		||||
@@ -978,7 +1012,12 @@ out/TildeFriends.aab: out/apk/classes.dex $(filter-out %debug%, $(ANDROID_TARGET
 | 
			
		||||
	@cp -r deps/codemirror/ out/aab/staging/root/deps/
 | 
			
		||||
	@cd out/aab/staging/; zip -r ../base.zip *; cd ../../../
 | 
			
		||||
	@java -jar $(BUNDLETOOL) build-bundle --overwrite --config=src/android/BundleConfig.json --modules=out/aab/base.zip --output=$@
 | 
			
		||||
	@jarsigner -keystore .keys/android.jks $@ androidKey -storepass android
 | 
			
		||||
	@$(ANDROID_NDK)/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-strip --only-keep-debug out/androidrelease/tildefriends -o out/aab/staging/BUNDLE-METADATA/com.android.tools.build.debugsymbols/arm64-v8a/libtildefriends.so.sym
 | 
			
		||||
	@$(ANDROID_NDK)/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-strip --only-keep-debug out/androidrelease-armv7a/tildefriends -o out/aab/staging/BUNDLE-METADATA/com.android.tools.build.debugsymbols/armeabi-v7a/libtildefriends.so.sym
 | 
			
		||||
	@$(ANDROID_NDK)/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-strip --only-keep-debug out/androidrelease-x86_64/tildefriends -o out/aab/staging/BUNDLE-METADATA/com.android.tools.build.debugsymbols/x86_64/libtildefriends.so.sym
 | 
			
		||||
	@$(ANDROID_NDK)/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-strip --only-keep-debug out/androidrelease-x86/tildefriends -o out/aab/staging/BUNDLE-METADATA/com.android.tools.build.debugsymbols/x86/libtildefriends.so.sym
 | 
			
		||||
	@cd out/aab/staging; zip -u ../../../$@ BUNDLE-METADATA/com.android.tools.build.debugsymbols/arm64-v8a/libtildefriends.so.sym BUNDLE-METADATA/com.android.tools.build.debugsymbols/armeabi-v7a/libtildefriends.so.sym BUNDLE-METADATA/com.android.tools.build.debugsymbols/x86_64/libtildefriends.so.sym BUNDLE-METADATA/com.android.tools.build.debugsymbols/x86/libtildefriends.so.sym; cd ../../../
 | 
			
		||||
	@$(ANDROID_BUILD_TOOLS)/apksigner sign -ks .keys/android.jks --ks-key-alias androidKey -ks-pass pass:android --min-sdk-version=$(ANDROID_MIN_SDK_VERSION) --alignment-preserved $@
 | 
			
		||||
 | 
			
		||||
aab: out/TildeFriends.aab ## Build an Android App Bundle.
 | 
			
		||||
.PHONY: aab
 | 
			
		||||
@@ -1040,12 +1079,12 @@ out/apk/TildeFriends-%.fdroid.unsigned.apk:
 | 
			
		||||
 | 
			
		||||
out/%.apk: out/apk/%.unsigned.apk
 | 
			
		||||
	@echo "[apksigner] $(notdir $@)"
 | 
			
		||||
	@$(ANDROID_BUILD_TOOLS)/apksigner sign --ks .keys/android.jks --ks-key-alias androidKey --ks-pass pass:android --key-pass pass:android --min-sdk-version $(ANDROID_MIN_SDK_VERSION) --out $@ $<
 | 
			
		||||
	@$(ANDROID_BUILD_TOOLS)/apksigner sign --ks .keys/android.jks --ks-key-alias androidKey --ks-pass pass:android --key-pass pass:android --min-sdk-version $(ANDROID_MIN_SDK_VERSION) --out $@ --alignment-preserved $<
 | 
			
		||||
 | 
			
		||||
out/%.zopfli.apk: out/%.apk
 | 
			
		||||
	@echo "[zopfli] $(notdir $@)"
 | 
			
		||||
	$(ANDROID_BUILD_TOOLS)/zipalign -f -z 4 $< $@.zopfli
 | 
			
		||||
	@$(ANDROID_BUILD_TOOLS)/apksigner sign --ks .keys/android.jks --ks-key-alias androidKey --ks-pass pass:android --key-pass pass:android --min-sdk-version $(ANDROID_MIN_SDK_VERSION) --out $@ $@.zopfli
 | 
			
		||||
	@$(ANDROID_BUILD_TOOLS)/apksigner sign --ks .keys/android.jks --ks-key-alias androidKey --ks-pass pass:android --key-pass pass:android --min-sdk-version $(ANDROID_MIN_SDK_VERSION) --out $@ --alignment-preserved $@.zopfli
 | 
			
		||||
 | 
			
		||||
release-apk: out/TildeFriends-arm-release.zopfli.apk out/TildeFriends-x86-release.zopfli.apk ## Build an Android release APK.
 | 
			
		||||
.PHONY: release-apk
 | 
			
		||||
@@ -1063,6 +1102,11 @@ releaseapkgo: out/TildeFriends-arm-release.apk ## Build, install, and run a rele
 | 
			
		||||
	@adb shell am start com.unprompted.tildefriends/.TildeFriendsActivity
 | 
			
		||||
.PHONY: releaseapkgo
 | 
			
		||||
 | 
			
		||||
x86releaseapkgo: out/TildeFriends-x86-release.apk ## Build, install, and run an x86 release Android APK.
 | 
			
		||||
	@adb install -r $<
 | 
			
		||||
	@adb shell am start com.unprompted.tildefriends/.TildeFriendsActivity
 | 
			
		||||
.PHONY: x86releaseapkgo
 | 
			
		||||
 | 
			
		||||
apklog: ## Display Android log output.
 | 
			
		||||
	@adb logcat *:S tildefriends
 | 
			
		||||
.PHONY: apklog
 | 
			
		||||
@@ -1081,20 +1125,34 @@ out/%.app/tildefriends.png: src/ios/tildefriends.png
 | 
			
		||||
	@cp -v $< $@
 | 
			
		||||
 | 
			
		||||
out/data.zip: $(RAW_FILES)
 | 
			
		||||
	@echo [zip] $@
 | 
			
		||||
	@zip -u $@ -q -9 $(RAW_FILES)
 | 
			
		||||
 | 
			
		||||
out/tildefriends-%.app/tildefriends: out/%/tildefriends out/tildefriends-%.app/Info.plist out/tildefriends-%.app/tildefriends.png out/data.zip
 | 
			
		||||
out/zsign_build/zsign: $(wildcard deps/zsign/*.cpp deps/zsign/*.h deps/zsign/*.txt deps/zsign/common/*)
 | 
			
		||||
	@+echo [cmake] $@
 | 
			
		||||
	@cmake -B out/zsign_build deps/zsign
 | 
			
		||||
	@cmake --build out/zsign_build -- COLOR=0 VERBOSE=0 MAKESILENT=-s
 | 
			
		||||
 | 
			
		||||
ifeq ($(HAVE_LINUX_IOS),1)
 | 
			
		||||
ZSIGN_DEP = out/zsign_build/zsign
 | 
			
		||||
else
 | 
			
		||||
ZSIGN_DEP =
 | 
			
		||||
endif
 | 
			
		||||
 | 
			
		||||
out/tildefriends-%.app/tildefriends: out/%/tildefriends out/tildefriends-%.app/Info.plist out/tildefriends-%.app/tildefriends.png out/data.zip $(ZSIGN_DEP)
 | 
			
		||||
	@mkdir -p $(dir $@)
 | 
			
		||||
	@cp -v $< $@
 | 
			
		||||
	@cp -v $(filter-out out/zsign%,$<) $@
 | 
			
		||||
	@cp -v out/data.zip $(@D)/
 | 
			
		||||
ifeq ($(HAVE_LINUX_IOS),1)
 | 
			
		||||
	@zsign -q -k .keys/apple.p12 -f -m src/ios/embedded.mobileprovision $(realpath $(dir $@))
 | 
			
		||||
	@mkdir -p $(realpath $(dir $@))/_CodeSignature
 | 
			
		||||
	@out/zsign_build/zsign -q -k .keys/apple.p12 -f -m src/ios/embedded.mobileprovision $(realpath $(dir $@))
 | 
			
		||||
endif
 | 
			
		||||
.SECONDARY:
 | 
			
		||||
out/tildefriends-%.ipa: out/tildefriends-ios%.app/tildefriends
 | 
			
		||||
	@echo "[ipa] $@"
 | 
			
		||||
	@rm -rf $@.tmp $@
 | 
			
		||||
	@mkdir -p $@.tmp/Payload/tildefriends.app/
 | 
			
		||||
	@cp src/ios/tildefriends512.png $@.tmp/iTunesArtwork
 | 
			
		||||
	@cp -R $(dir $<)/* $@.tmp/Payload/tildefriends.app/
 | 
			
		||||
	@cd $@.tmp/ && zip -u ../../$@ -q -9 -r ./
 | 
			
		||||
	@rm -rf $@.tmp/
 | 
			
		||||
@@ -1126,45 +1184,10 @@ iossimdebuggo: out/tildefriends-iossimdebug.app/tildefriends ## Build, install,
 | 
			
		||||
	xcrun simctl launch booted com.unprompted.tildefriends
 | 
			
		||||
.PHONY: iossimdebuggo
 | 
			
		||||
 | 
			
		||||
ANDROID_DEPS := deps/openssl/android/arm64-v8a/usr/local/lib/libssl.a
 | 
			
		||||
$(ANDROID_DEPS):
 | 
			
		||||
	+@ANDROID_NDK_ROOT=$(ANDROID_NDK) tools/ssl-android
 | 
			
		||||
$(filter $(BUILD_DIR)/android%,$(APP_OBJS)): | $(ANDROID_DEPS)
 | 
			
		||||
 | 
			
		||||
ifeq ($(UNAME_S),Linux)
 | 
			
		||||
LOCAL_DEPS := deps/openssl/$(UNAME_S)/$(UNAME_M)/usr/local/lib/libssl.a
 | 
			
		||||
$(LOCAL_DEPS):
 | 
			
		||||
	+@tools/ssl-local
 | 
			
		||||
$(filter $(BUILD_DIR)/debug/%,$(APP_OBJS)) $(filter $(BUILD_DIR)/release/%,$(APP_OBJS)): | $(LOCAL_DEPS)
 | 
			
		||||
 | 
			
		||||
ifeq ($(HAVE_CROSS_AARCH64),1)
 | 
			
		||||
LOCAL_DEPS := deps/openssl/$(UNAME_S)/aarch64/usr/local/lib/libssl.a
 | 
			
		||||
$(LOCAL_DEPS):
 | 
			
		||||
	+@OPTIONS=--cross-compile-prefix=aarch64-linux-gnu- BUILD_TARGET=aarch64 tools/ssl-local
 | 
			
		||||
$(filter $(BUILD_DIR)/armdebug/%,$(APP_OBJS)) $(filter $(BUILD_DIR)/armrelease/%,$(APP_OBJS)): | $(LOCAL_DEPS)
 | 
			
		||||
endif
 | 
			
		||||
endif
 | 
			
		||||
 | 
			
		||||
ifeq ($(UNAME_S),Darwin)
 | 
			
		||||
LOCAL_DEPS := deps/openssl/$(UNAME_S)/$(UNAME_M)/usr/local/lib/libssl.a
 | 
			
		||||
$(LOCAL_DEPS):
 | 
			
		||||
	+@tools/ssl-local
 | 
			
		||||
$(filter $(BUILD_DIR)/macosdebug/%,$(APP_OBJS)) $(filter $(BUILD_DIR)/macosrelease/%,$(APP_OBJS)): | $(LOCAL_DEPS)
 | 
			
		||||
endif
 | 
			
		||||
 | 
			
		||||
ifeq ($(HAVE_WIN),1)
 | 
			
		||||
WINDOWS_DEPS := deps/openssl/mingw64/usr/local/lib/libssl.a
 | 
			
		||||
$(WINDOWS_DEPS):
 | 
			
		||||
	+@tools/ssl-mingw64
 | 
			
		||||
$(filter $(BUILD_DIR)/win%,$(APP_OBJS)): | $(WINDOWS_DEPS)
 | 
			
		||||
endif
 | 
			
		||||
 | 
			
		||||
ifeq ($(UNAME_S),Darwin)
 | 
			
		||||
IOS_DEPS := deps/openssl/ios/ios64-xcrun/usr/local/lib/libssl.a
 | 
			
		||||
$(IOS_DEPS):
 | 
			
		||||
	+@tools/ssl-ios
 | 
			
		||||
$(filter $(BUILD_DIR)/ios%,$(APP_OBJS)): | $(IOS_DEPS)
 | 
			
		||||
endif
 | 
			
		||||
out/macos%/tildefriends: out/macos%-arm/tildefriends out/macos%-x86_64/tildefriends
 | 
			
		||||
	@echo [lipo] $@
 | 
			
		||||
	@mkdir -p $(@D)
 | 
			
		||||
	@deps/macos_toolchain/bin/lipo -create -output $@ $^
 | 
			
		||||
 | 
			
		||||
##
 | 
			
		||||
## Linux package targets:
 | 
			
		||||
@@ -1193,7 +1216,8 @@ appimage: out/tildefriends-x86_64.AppImage ## Build an AppImage.
 | 
			
		||||
.PHONY: appimage
 | 
			
		||||
 | 
			
		||||
flatpak: out/ ## Build a flatpak.
 | 
			
		||||
	flatpak-builder --force-clean --user --install-deps-from=flathub --install --repo=out/flatpak-repo out/flatpak src/com.unprompted.tildefriends.yml
 | 
			
		||||
	flatpak remote-add --user --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo
 | 
			
		||||
	flatpak-builder --user --disable-rofiles-fuse --install-deps-from=flathub --install --repo=out/flatpak-repo out/flatpak src/com.unprompted.tildefriends.yml
 | 
			
		||||
	flatpak build-bundle out/flatpak-repo out/tildefriends.flatpak com.unprompted.tildefriends
 | 
			
		||||
.PHONY: flatpak
 | 
			
		||||
 | 
			
		||||
@@ -1233,7 +1257,6 @@ tarball: ## Build an all-inclusive source tarball (.tar.xz).
 | 
			
		||||
		--exclude=deps/libsodium/test \
 | 
			
		||||
		--exclude=deps/libuv/docs \
 | 
			
		||||
		--exclude=deps/libuv/test \
 | 
			
		||||
		--exclude=deps/openssl \
 | 
			
		||||
		--exclude=deps/speedscope/*.map \
 | 
			
		||||
		--exclude=deps/sqlite/shell.c \
 | 
			
		||||
		--exclude=deps/zlib/contrib/vstudio \
 | 
			
		||||
@@ -1244,7 +1267,20 @@ tarball: ## Build an all-inclusive source tarball (.tar.xz).
 | 
			
		||||
.PHONY: tarball
 | 
			
		||||
 | 
			
		||||
dist: ## Build versions of all distributables for release.
 | 
			
		||||
dist: release-apk iosrelease-ipa aab $(if $(HAVE_WIN), out/winrelease/tildefriends.standalone.exe) out/TildeFriends-release.fdroid.apk appimage tarball out/release/tildefriends.standalone $(if $(HAVE_CROSS_AARCH64), out/armrelease/tildefriends.standalone)
 | 
			
		||||
dist: release-apk aab out/TildeFriends-release.fdroid.apk appimage tarball out/release/tildefriends.standalone
 | 
			
		||||
ifeq ($(HAVE_LINUX_IOS),1)
 | 
			
		||||
dist: iosrelease-ipa
 | 
			
		||||
endif
 | 
			
		||||
ifeq ($(HAVE_LINUX_MACOS),1)
 | 
			
		||||
dist: out/macosrelease/tildefriends.standalone
 | 
			
		||||
endif
 | 
			
		||||
ifeq ($(HAVE_WIN),1)
 | 
			
		||||
dist: out/winrelease/tildefriends.standalone.exe
 | 
			
		||||
endif
 | 
			
		||||
ifeq ($(HAVE_CROSS_AARCH64),1)
 | 
			
		||||
dist: out/armrelease/tildefriends.standalone
 | 
			
		||||
endif
 | 
			
		||||
dist:
 | 
			
		||||
	@mkdir -p dist/
 | 
			
		||||
	@echo "[cp] tildefriends-$(VERSION_NUMBER).tar.xz"
 | 
			
		||||
	@cp out/tildefriends-$(VERSION_NUMBER).tar.xz dist/tildefriends-$(VERSION_NUMBER).tar.xz
 | 
			
		||||
@@ -1252,8 +1288,10 @@ dist: release-apk iosrelease-ipa aab $(if $(HAVE_WIN), out/winrelease/tildefrien
 | 
			
		||||
	@cp out/TildeFriends-x86-release.zopfli.apk dist/TildeFriends-x86-$(VERSION_NUMBER).apk
 | 
			
		||||
	@echo "[cp] TildeFriends-arm-$(VERSION_NUMBER).apk"
 | 
			
		||||
	@cp out/TildeFriends-arm-release.zopfli.apk dist/TildeFriends-arm-$(VERSION_NUMBER).apk
 | 
			
		||||
	@echo "[cp] TildeFriends-$(VERSION_NUMBER).ipa"
 | 
			
		||||
	@cp out/tildefriends-release.ipa dist/TildeFriends-$(VERSION_NUMBER).ipa
 | 
			
		||||
	@test $(HAVE_LINUX_IOS) && echo "[cp] TildeFriends-$(VERSION_NUMBER).ipa"
 | 
			
		||||
	@test $(HAVE_LINUX_IOS) && cp out/tildefriends-release.ipa dist/TildeFriends-$(VERSION_NUMBER).ipa
 | 
			
		||||
	@test $(HAVE_LINUX_MACOS) && echo "[cp] tildefriends-macos-$(VERSION_NUMBER)"
 | 
			
		||||
	@test $(HAVE_LINUX_MACOS) && cp out/macosrelease/tildefriends.standalone dist/tildefriends-macos-$(VERSION_NUMBER)
 | 
			
		||||
	@test $(HAVE_WIN) && echo "[cp] tildefriends-$(VERSION_NUMBER).exe"
 | 
			
		||||
	@test $(HAVE_WIN) && cp out/winrelease/tildefriends.standalone.exe dist/tildefriends-$(VERSION_NUMBER).exe
 | 
			
		||||
	@echo "[cp] TildeFriends-$(VERSION_NUMBER).aab"
 | 
			
		||||
@@ -1275,6 +1313,17 @@ dist-test: dist ## Exercise some built distributable files, making sure they wor
 | 
			
		||||
	@rm -rf tildefriends-$(VERSION_NUMBER)
 | 
			
		||||
.PHONY: dist-test
 | 
			
		||||
 | 
			
		||||
dist-ios: iosrelease-app
 | 
			
		||||
	rm -rfv out/Payload out/tildefriends.ipa
 | 
			
		||||
	mkdir -p out/Payload/tildefriends.app
 | 
			
		||||
	cp -avR out/tildefriends-iosrelease.app/* out/Payload/tildefriends.app/
 | 
			
		||||
	cp src/ios/tildefriends.png out/Payload/tildefriends.app/
 | 
			
		||||
	xcrun -sdk iphoneos actool --compile out/Payload/tildefriends.app/ --platform iphoneos --minimum-deployment-target $(IPHONEOS_VERSION_MIN) --app-icon AppIcon src/ios/icons/Assets.xcassets src/ios/icons/*.png --output-partial-info-plist out/actool.plist
 | 
			
		||||
	cp src/ios/distribution.mobileprovision out/Payload/tildefriends.app/embedded.mobileprovision
 | 
			
		||||
	xcrun -sdk iphoneos codesign -f -s 'Apple Distribution' --entitlements src/ios/Entitlements.plist --generate-entitlement-der out/Payload/tildefriends.app
 | 
			
		||||
	cd out; zip -r tildefriends.ipa Payload; cd ..
 | 
			
		||||
	xcrun -sdk iphoneos altool --upload-app -f out/tildefriends.ipa -t ios -u $$(cat .keys/altool-user) -p $$(cat .keys/altool-password)
 | 
			
		||||
 | 
			
		||||
##
 | 
			
		||||
## Targets for tidying up:
 | 
			
		||||
##
 | 
			
		||||
@@ -1315,6 +1364,18 @@ help: ## Display this help message.
 | 
			
		||||
.PHONY: help
 | 
			
		||||
.DEFAULT_GOAL := help
 | 
			
		||||
 | 
			
		||||
docs: debug
 | 
			
		||||
docs: ## Build HTML docs.
 | 
			
		||||
	@echo '# CLI Usage\n' > docs/usage.md
 | 
			
		||||
	@echo "## tildefriends -h" >> docs/usage.md
 | 
			
		||||
	@echo '\n```' >> docs/usage.md
 | 
			
		||||
	@out/debug/tildefriends -h >> docs/usage.md
 | 
			
		||||
	@echo '```' >> docs/usage.md
 | 
			
		||||
	@for command in $$(out/debug/tildefriends -h | grep -Po '[A-Za-z_]*(?= - )'); do
 | 
			
		||||
	@  echo "\n## tildefriends $$command -h" >> docs/usage.md
 | 
			
		||||
	@  echo '\n```' >> docs/usage.md
 | 
			
		||||
	@  out/debug/tildefriends $$command -h >> docs/usage.md
 | 
			
		||||
	@  echo '```' >> docs/usage.md
 | 
			
		||||
	@done
 | 
			
		||||
	@doxygen
 | 
			
		||||
.PHONY: docs
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										27
									
								
								README.md
									
									
									
									
									
								
							
							
						
						@@ -1,18 +1,16 @@
 | 
			
		||||
# 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/.
 | 
			
		||||
 | 
			
		||||
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
 | 
			
		||||
 | 
			
		||||
1. Make it easy and fun to run all sorts of web applications.
 | 
			
		||||
2. Provide security that is easy to understand and protects your data.
 | 
			
		||||
3. Make creating and sharing web applications accessible to anyone with a
 | 
			
		||||
   browser.
 | 
			
		||||
1. Be the fanciest, best-maintained Secure Scuttlebutt client in town.
 | 
			
		||||
1. Make it easy to make, share, and run all sorts of applications while
 | 
			
		||||
   respecting the privacy and safety of your data.
 | 
			
		||||
 | 
			
		||||
## Getting the Source
 | 
			
		||||
 | 
			
		||||
@@ -40,9 +38,6 @@ dependencies in the right places.
 | 
			
		||||
 | 
			
		||||
### Requirements
 | 
			
		||||
 | 
			
		||||
On Linux only, system OpenSSL libraries (`libssl-dev`, in debian-speak) are
 | 
			
		||||
assumed to be available.
 | 
			
		||||
 | 
			
		||||
On MacOS, Xcode's command-line tools are expected to be available.
 | 
			
		||||
 | 
			
		||||
### Build Commands
 | 
			
		||||
@@ -58,18 +53,16 @@ standard.
 | 
			
		||||
## Running
 | 
			
		||||
 | 
			
		||||
By default, running the built `out/debug/tildefriends` executable will start a
 | 
			
		||||
web server at <http://localhost:12345/>. It expects to be run with the
 | 
			
		||||
repository root as the current working directory. `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
 | 
			
		||||
privileges. Further administration can be done at
 | 
			
		||||
privileges. Further administration can be done in the `admin` app at
 | 
			
		||||
<http://localhost:12345/~core/admin/>.
 | 
			
		||||
 | 
			
		||||
## Documentation
 | 
			
		||||
 | 
			
		||||
Docs are a work in progress:
 | 
			
		||||
<https://dev.tildefriends.net/cory/tildefriends/wiki>.
 | 
			
		||||
Docs live here: <https://docs.tildefriends.net/>.
 | 
			
		||||
 | 
			
		||||
## License
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,5 @@
 | 
			
		||||
{
 | 
			
		||||
	"type": "tildefriends-app",
 | 
			
		||||
	"emoji": "🎛",
 | 
			
		||||
	"previous": "&R49FywYF8CXPhoSEydLbSCgvCddeyTiBwGuDU/gqY+M=.sha256"
 | 
			
		||||
	"previous": "&kmKNyb/uaXNb24gCinJtfS8iWx4cLUWdtl0y2DwEUas=.sha256"
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -72,7 +72,7 @@ ${description.value}</textarea
 | 
			
		||||
					</button>
 | 
			
		||||
				</li>
 | 
			
		||||
			`;
 | 
			
		||||
		} else {
 | 
			
		||||
		} else if (description.type != 'hidden') {
 | 
			
		||||
			return html`
 | 
			
		||||
				<li class="w3-row">
 | 
			
		||||
					<label class="w3-quarter" for=${'gs_' + key} style="font-weight: bold">${title_case(key)}</label>
 | 
			
		||||
 
 | 
			
		||||
@@ -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}
 | 
			
		||||
/* 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}
 | 
			
		||||
@@ -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-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%}
 | 
			
		||||
@@ -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-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}
 | 
			
		||||
@@ -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-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}
 | 
			
		||||
@@ -232,4 +248,4 @@ hr{border:0;border-top:1px solid #eee;margin:20px 0}
 | 
			
		||||
.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}
 | 
			
		||||
.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",
 | 
			
		||||
	"emoji": "📜",
 | 
			
		||||
	"previous": "&BEf0nraBdHk/+PWqx6tOSu5rheWVaxaL7orAOz3285M=.sha256"
 | 
			
		||||
	"previous": "&sJqeyYjHys6Z8IqqtZ2ij2ZC1E2xieu/FU/u2hE+O1U=.sha256"
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -55,6 +55,9 @@ app.setDocument(`<head>
 | 
			
		||||
</head>
 | 
			
		||||
<body style="color:#fff">
 | 
			
		||||
	${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')}
 | 
			
		||||
	<a id="Database"></a>
 | 
			
		||||
	${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.
 | 
			
		||||
`;
 | 
			
		||||
 | 
			
		||||
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()'] = `
 | 
			
		||||
Exits the app.  But why would you want to do that?
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,5 @@
 | 
			
		||||
{
 | 
			
		||||
	"type": "tildefriends-app",
 | 
			
		||||
	"emoji": "💻",
 | 
			
		||||
	"previous": "&icsPplXHgmpkbNWyo/WTvRdT/A8BXxW4lJixOtP4ueQ=.sha256"
 | 
			
		||||
	"previous": "&sFRTDn/RpxP1NJeECXHrXKwCRUJsEOEDVaCMPl50zpM=.sha256"
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -19,10 +19,6 @@ async function fetch_info(apps) {
 | 
			
		||||
	return result;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 *
 | 
			
		||||
 *
 | 
			
		||||
 */
 | 
			
		||||
async function fetch_shared_apps() {
 | 
			
		||||
	let messages = {};
 | 
			
		||||
 | 
			
		||||
@@ -69,17 +65,17 @@ async function main() {
 | 
			
		||||
	const stylesheet = `
 | 
			
		||||
		body {
 | 
			
		||||
			color: whitesmoke;
 | 
			
		||||
			font-family: sans-serif;
 | 
			
		||||
			margin: 16px;
 | 
			
		||||
			margin: 8px;
 | 
			
		||||
		}
 | 
			
		||||
		.container {
 | 
			
		||||
 | 
			
		||||
		.iconbox {
 | 
			
		||||
			display: grid;
 | 
			
		||||
			grid-template-columns: repeat(auto-fill, 64px);
 | 
			
		||||
			gap: 1em;
 | 
			
		||||
			justify-content: space-around;
 | 
			
		||||
			background-color: #ffffff10;
 | 
			
		||||
			border: 2px solid #073642;
 | 
			
		||||
			border-radius: 8px;
 | 
			
		||||
			grid-template-columns: repeat(auto-fill, minmax(96px, 1fr));
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		.iconbox::after {
 | 
			
		||||
			content: "";
 | 
			
		||||
			flex: auto;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		.app {
 | 
			
		||||
@@ -101,16 +97,28 @@ async function main() {
 | 
			
		||||
	`;
 | 
			
		||||
 | 
			
		||||
	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 id="apps" class="container"></div>
 | 
			
		||||
		<div class="w3-card-4 w3-margin-top">
 | 
			
		||||
			<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 id="shared_apps" class="container"></div>
 | 
			
		||||
		<div class="w3-card-4 w3-margin-top">
 | 
			
		||||
			<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 id="core_apps" class="container"></div>
 | 
			
		||||
		<div class="w3-card-4 w3-margin-top">
 | 
			
		||||
			<header class="w3-container w3-light-blue">
 | 
			
		||||
				<h2>Core Apps</h2>
 | 
			
		||||
			</header>
 | 
			
		||||
			<div id="core_apps" class="w3-indigo iconbox"></div>
 | 
			
		||||
		</div>
 | 
			
		||||
	`;
 | 
			
		||||
 | 
			
		||||
	const script = `
 | 
			
		||||
@@ -126,9 +134,13 @@ async function main() {
 | 
			
		||||
 | 
			
		||||
			// For each app in the provided list
 | 
			
		||||
			for (let app of Object.keys(apps).sort()) {
 | 
			
		||||
 | 
			
		||||
				// 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');
 | 
			
		||||
 | 
			
		||||
				// The app's icon
 | 
			
		||||
@@ -161,12 +173,13 @@ async function main() {
 | 
			
		||||
	<!DOCTYPE html>
 | 
			
		||||
	<html>
 | 
			
		||||
		<head>
 | 
			
		||||
			<link type="text/css" rel="stylesheet" href="w3.css"></link>
 | 
			
		||||
			<style>
 | 
			
		||||
				${stylesheet}
 | 
			
		||||
			</style>
 | 
			
		||||
		</head>
 | 
			
		||||
 | 
			
		||||
		<body>
 | 
			
		||||
		<body class="w3-darkgray">
 | 
			
		||||
			${body}
 | 
			
		||||
		</body>
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										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}
 | 
			
		||||
							
								
								
									
										42
									
								
								apps/blog/lit-all.min.js
									
									
									
									
										vendored
									
									
								
							
							
						
						@@ -1,4 +1,5 @@
 | 
			
		||||
{
 | 
			
		||||
	"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 || {};
 | 
			
		||||
	let contacts = await query(
 | 
			
		||||
		`
 | 
			
		||||
				SELECT content FROM messages
 | 
			
		||||
				SELECT json(content) AS content FROM messages
 | 
			
		||||
				WHERE author = ? AND
 | 
			
		||||
				rowid > ? AND
 | 
			
		||||
				rowid <= ? AND
 | 
			
		||||
@@ -189,50 +189,6 @@ async function fetch_about(db, ids, 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) {
 | 
			
		||||
	let size = 0;
 | 
			
		||||
	await ssb.sqlAsync(
 | 
			
		||||
 
 | 
			
		||||
@@ -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}
 | 
			
		||||
/* 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}
 | 
			
		||||
@@ -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-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%}
 | 
			
		||||
@@ -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-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}
 | 
			
		||||
@@ -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-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}
 | 
			
		||||
@@ -232,4 +248,4 @@ hr{border:0;border-top:1px solid #eee;margin:20px 0}
 | 
			
		||||
.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}
 | 
			
		||||
.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}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										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
									
								
							
							
						
						@@ -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
									
								
							
							
						
						@@ -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
									
								
							
							
						
						@@ -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}
 | 
			
		||||
							
								
								
									
										42
									
								
								apps/issues/lit-all.min.js
									
									
									
									
										vendored
									
									
								
							
							
						
						
							
								
								
									
										42
									
								
								apps/journal/lit-all.min.js
									
									
									
									
										vendored
									
									
								
							
							
						
						@@ -1,5 +1,5 @@
 | 
			
		||||
{
 | 
			
		||||
	"type": "tildefriends-app",
 | 
			
		||||
	"emoji": "🚪",
 | 
			
		||||
	"previous": "&HXCdDG8gGYXElTyEFbg85jqa6lDXNL2ENPIA9UoJNbI=.sha256"
 | 
			
		||||
	"previous": "&DJwkqNfYWtW9yBtJQMseEXm7l04Enpi+yAxZulLq9Vk=.sha256"
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,9 @@
 | 
			
		||||
async function main() {
 | 
			
		||||
	let host = core.url.match(/.*\/\/(.*?)\//)[1];
 | 
			
		||||
	let id = (await ssb.getServerIdentity()).substring(1);
 | 
			
		||||
	let room = `net:${host}:${ssb.port}~shs:${id}:SSB+Room+SK3TLYC2T86EHQCUHBUHASCASE18JBV24=`;
 | 
			
		||||
	print(core.url);
 | 
			
		||||
	let host = core.url.match(/.*?\/\/([^:/]*)/)[1];
 | 
			
		||||
	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(`
 | 
			
		||||
		<body style="color: #fff">
 | 
			
		||||
			<h1>Server</h1>
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										42
									
								
								apps/sneaker/lit-all.min.js
									
									
									
									
										vendored
									
									
								
							
							
						
						@@ -1,5 +1,5 @@
 | 
			
		||||
{
 | 
			
		||||
	"type": "tildefriends-app",
 | 
			
		||||
	"emoji": "🦀",
 | 
			
		||||
	"previous": "&QuEaXfdWGAl42dkJBoHocZbtKRT3zT25BNgCo88CSfE=.sha256"
 | 
			
		||||
	"previous": "&Gic1e3jOZ7z5131jSCclbFXRpjyu8JlWJrjE7Fvn5dc=.sha256"
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -21,9 +21,6 @@ tfrpc.register(async function createIdentity() {
 | 
			
		||||
tfrpc.register(async function getServerIdentity() {
 | 
			
		||||
	return ssb.getServerIdentity();
 | 
			
		||||
});
 | 
			
		||||
tfrpc.register(async function setServerFollowingMe(id, following) {
 | 
			
		||||
	return ssb.setServerFollowingMe(id, following);
 | 
			
		||||
});
 | 
			
		||||
tfrpc.register(async function getIdentities() {
 | 
			
		||||
	return ssb.getIdentities();
 | 
			
		||||
});
 | 
			
		||||
@@ -79,6 +76,9 @@ tfrpc.register(function setHash(hash) {
 | 
			
		||||
core.register('onMessage', async function (id) {
 | 
			
		||||
	await tfrpc.rpc.notifyNewMessage(id);
 | 
			
		||||
});
 | 
			
		||||
core.register('onBlob', async function (id) {
 | 
			
		||||
	await tfrpc.rpc.notifyNewBlob(id);
 | 
			
		||||
});
 | 
			
		||||
tfrpc.register(async function store_blob(blob) {
 | 
			
		||||
	if (Array.isArray(blob)) {
 | 
			
		||||
		blob = Uint8Array.from(blob);
 | 
			
		||||
@@ -106,6 +106,19 @@ tfrpc.register(async function getActiveIdentity() {
 | 
			
		||||
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());
 | 
			
		||||
});
 | 
			
		||||
 
 | 
			
		||||
@@ -7,7 +7,7 @@ function textNode(text) {
 | 
			
		||||
function linkNode(text, link) {
 | 
			
		||||
	const linkNode = new commonmark.Node('link', undefined);
 | 
			
		||||
	if (link.startsWith('#')) {
 | 
			
		||||
		linkNode.destination = `#${encodeURIComponent('#' + link)}`;
 | 
			
		||||
		linkNode.destination = `#${encodeURIComponent(link)}`;
 | 
			
		||||
	} else {
 | 
			
		||||
		linkNode.destination = link;
 | 
			
		||||
	}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,6 @@
 | 
			
		||||
import * as tfrpc from '/static/tfrpc.js';
 | 
			
		||||
import {html, render} from './lit-all.min.js';
 | 
			
		||||
import {styles} from './tf-styles.js';
 | 
			
		||||
import {styles, generate_theme} from './tf-styles.js';
 | 
			
		||||
 | 
			
		||||
let g_emojis;
 | 
			
		||||
 | 
			
		||||
@@ -14,23 +14,8 @@ function get_emojis() {
 | 
			
		||||
	});
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function get_recent(author) {
 | 
			
		||||
	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) {
 | 
			
		||||
export async function picker(callback, anchor, author, recent) {
 | 
			
		||||
	let json = await get_emojis();
 | 
			
		||||
	let recent = await get_recent(author);
 | 
			
		||||
 | 
			
		||||
	let div = document.createElement('div');
 | 
			
		||||
	div.id = 'emoji_picker';
 | 
			
		||||
@@ -155,6 +140,9 @@ export async function picker(callback, anchor, author) {
 | 
			
		||||
		<style>
 | 
			
		||||
			${styles}
 | 
			
		||||
		</style>
 | 
			
		||||
		<style>
 | 
			
		||||
			${generate_theme()}
 | 
			
		||||
		</style>
 | 
			
		||||
		<div
 | 
			
		||||
			class="w3-modal"
 | 
			
		||||
			style="display: block; box-sizing: border-box; z-index: 10"
 | 
			
		||||
@@ -162,7 +150,7 @@ export async function picker(callback, anchor, author) {
 | 
			
		||||
			<div class="w3-modal-content w3-card-4">
 | 
			
		||||
				<div
 | 
			
		||||
					class="w3-content w3-theme-d1"
 | 
			
		||||
					style="display: flex; flex-direction: column; max-height: 50vh"
 | 
			
		||||
					style="display: flex; flex-direction: column; max-height: 80vh"
 | 
			
		||||
				>
 | 
			
		||||
					<header class="w3-container" style="flex: 0 0">
 | 
			
		||||
						<h1>Choose a Reaction</h1>
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										42
									
								
								apps/ssb/lit-all.min.js
									
									
									
									
										vendored
									
									
								
							
							
						
						@@ -12,12 +12,14 @@ 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_search from './tf-tab-search.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_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,17 +1,17 @@
 | 
			
		||||
import {LitElement, html, css, guard, until} from './lit-all.min.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 {
 | 
			
		||||
	static get properties() {
 | 
			
		||||
		return {
 | 
			
		||||
			whoami: {type: String},
 | 
			
		||||
			hash: {type: String},
 | 
			
		||||
			unread: {type: Array},
 | 
			
		||||
			tab: {type: String},
 | 
			
		||||
			broadcasts: {type: Array},
 | 
			
		||||
			connections: {type: Array},
 | 
			
		||||
			loading: {type: Boolean},
 | 
			
		||||
			loading_about: {type: Number},
 | 
			
		||||
			loaded: {type: Boolean},
 | 
			
		||||
			following: {type: Array},
 | 
			
		||||
			users: {type: Object},
 | 
			
		||||
@@ -19,6 +19,15 @@ class TfElement extends LitElement {
 | 
			
		||||
			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},
 | 
			
		||||
		};
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
@@ -28,18 +37,20 @@ class TfElement extends LitElement {
 | 
			
		||||
		super();
 | 
			
		||||
		let self = this;
 | 
			
		||||
		this.hash = '#';
 | 
			
		||||
		this.unread = [];
 | 
			
		||||
		this.tab = 'news';
 | 
			
		||||
		this.broadcasts = [];
 | 
			
		||||
		this.connections = [];
 | 
			
		||||
		this.following = [];
 | 
			
		||||
		this.users = {};
 | 
			
		||||
		this.loaded = false;
 | 
			
		||||
		this.loading_about = 0;
 | 
			
		||||
		this.channels = [];
 | 
			
		||||
		this.channels_unread = {};
 | 
			
		||||
		this.channels_latest = {};
 | 
			
		||||
		this.loading_channels_latest = 0;
 | 
			
		||||
		this.loading_channels_latest_scheduled = 0;
 | 
			
		||||
		this.loading_latest = 0;
 | 
			
		||||
		this.loading_latest_scheduled = 0;
 | 
			
		||||
		this.recent_reactions = [];
 | 
			
		||||
		this.private_closed = {};
 | 
			
		||||
		tfrpc.rpc.getBroadcasts().then((b) => {
 | 
			
		||||
			self.broadcasts = b || [];
 | 
			
		||||
		});
 | 
			
		||||
@@ -49,10 +60,22 @@ class TfElement extends LitElement {
 | 
			
		||||
		tfrpc.rpc.getHash().then((hash) => self.set_hash(hash));
 | 
			
		||||
		tfrpc.register(function hashChanged(hash) {
 | 
			
		||||
			self.set_hash(hash);
 | 
			
		||||
			self.reset_progress();
 | 
			
		||||
		});
 | 
			
		||||
		tfrpc.register(async function notifyNewMessage(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) {
 | 
			
		||||
			if (name === 'broadcasts') {
 | 
			
		||||
				self.broadcasts = value;
 | 
			
		||||
@@ -68,11 +91,30 @@ class TfElement extends LitElement {
 | 
			
		||||
	async initial_load() {
 | 
			
		||||
		let whoami = await tfrpc.rpc.getActiveIdentity();
 | 
			
		||||
		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.guest = !this.whoami?.length;
 | 
			
		||||
		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(
 | 
			
		||||
			`
 | 
			
		||||
@@ -120,8 +162,34 @@ class TfElement extends LitElement {
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	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 = ['', '@', '🔐', ...this.channels.map((x) => '#' + x)];
 | 
			
		||||
		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(
 | 
			
		||||
@@ -138,15 +206,15 @@ class TfElement extends LitElement {
 | 
			
		||||
			this.tab = 'search';
 | 
			
		||||
		} else if (this.hash === '#connections') {
 | 
			
		||||
			this.tab = 'connections';
 | 
			
		||||
		} else if (this.hash.startsWith('#sql=')) {
 | 
			
		||||
			this.tab = 'query';
 | 
			
		||||
		} else {
 | 
			
		||||
			this.tab = 'news';
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	async fetch_about(ids, users) {
 | 
			
		||||
		const k_cache_version = 1;
 | 
			
		||||
	async fetch_about(following, users) {
 | 
			
		||||
		this.loading_about++;
 | 
			
		||||
		let ids = Object.keys(following).sort();
 | 
			
		||||
		const k_cache_version = 3;
 | 
			
		||||
		let cache = await tfrpc.rpc.databaseGet('about');
 | 
			
		||||
		let original_cache = cache;
 | 
			
		||||
		cache = cache ? JSON.parse(cache) : {};
 | 
			
		||||
@@ -154,77 +222,86 @@ class TfElement extends LitElement {
 | 
			
		||||
			cache = {
 | 
			
		||||
				version: k_cache_version,
 | 
			
		||||
				about: {},
 | 
			
		||||
				last_row_id: 0,
 | 
			
		||||
			};
 | 
			
		||||
		}
 | 
			
		||||
		let max_row_id = (
 | 
			
		||||
			await tfrpc.rpc.query(
 | 
			
		||||
				`
 | 
			
		||||
			SELECT MAX(rowid) AS max_row_id FROM messages
 | 
			
		||||
		`,
 | 
			
		||||
				[]
 | 
			
		||||
			)
 | 
			
		||||
		)[0].max_row_id;
 | 
			
		||||
 | 
			
		||||
		let ids_out_of_date = ids.filter(
 | 
			
		||||
			(x) =>
 | 
			
		||||
				(users[x]?.seq && !cache.about[x]?.seq) ||
 | 
			
		||||
				(users[x]?.seq && users[x]?.seq > cache.about[x].seq)
 | 
			
		||||
		);
 | 
			
		||||
 | 
			
		||||
		for (let id of Object.keys(cache.about)) {
 | 
			
		||||
			if (ids.indexOf(id) == -1) {
 | 
			
		||||
				delete cache.about[id];
 | 
			
		||||
			} else {
 | 
			
		||||
				users[id] = Object.assign(cache.about[id], users[id] || {});
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		let abouts = 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,
 | 
			
		||||
					json_each(?1) AS following
 | 
			
		||||
				WHERE
 | 
			
		||||
					messages.author = following.value AND
 | 
			
		||||
					messages.rowid > ?3 AND
 | 
			
		||||
					messages.rowid <= ?4 AND
 | 
			
		||||
					json_extract(messages.content, '$.type') = 'about'
 | 
			
		||||
				UNION
 | 
			
		||||
				SELECT
 | 
			
		||||
					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.filter((id) => cache.about[id])),
 | 
			
		||||
				JSON.stringify(ids.filter((id) => !cache.about[id])),
 | 
			
		||||
				cache.last_row_id,
 | 
			
		||||
				max_row_id,
 | 
			
		||||
			]
 | 
			
		||||
		console.log(
 | 
			
		||||
			'loading about for',
 | 
			
		||||
			ids.length,
 | 
			
		||||
			'accounts',
 | 
			
		||||
			ids_out_of_date.length,
 | 
			
		||||
			'out of date'
 | 
			
		||||
		);
 | 
			
		||||
		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
 | 
			
		||||
		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
 | 
			
		||||
								messages.author,
 | 
			
		||||
								fields.key,
 | 
			
		||||
								RANK() OVER (PARTITION BY messages.author, fields.key ORDER BY messages.sequence DESC) AS rank,
 | 
			
		||||
								fields.value
 | 
			
		||||
							FROM messages JOIN json_each(messages.content) AS fields
 | 
			
		||||
							WHERE
 | 
			
		||||
								messages.content ->> '$.type' = 'about' AND
 | 
			
		||||
								messages.content ->> '$.about' = messages.author AND
 | 
			
		||||
								NOT fields.key IN ('about', 'type')) all_abouts
 | 
			
		||||
						JOIN json_each(?) AS following ON all_abouts.author = following.value
 | 
			
		||||
						WHERE rank = 1
 | 
			
		||||
						GROUP BY all_abouts.author
 | 
			
		||||
					`,
 | 
			
		||||
					[JSON.stringify(ids_out_of_date)]
 | 
			
		||||
				);
 | 
			
		||||
				users = users || {};
 | 
			
		||||
				for (let row of rows) {
 | 
			
		||||
					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);
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
		cache.last_row_id = max_row_id;
 | 
			
		||||
 | 
			
		||||
		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) {
 | 
			
		||||
		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);
 | 
			
		||||
			});
 | 
			
		||||
		}
 | 
			
		||||
		users = users || {};
 | 
			
		||||
		for (let id of Object.keys(cache.about)) {
 | 
			
		||||
			users[id] = Object.assign(users[id] || {}, cache.about[id]);
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		return Object.assign({}, users);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
@@ -239,18 +316,14 @@ class TfElement extends LitElement {
 | 
			
		||||
			[JSON.stringify(this.following), id]
 | 
			
		||||
		);
 | 
			
		||||
		for (let message of messages) {
 | 
			
		||||
			if (message.author == this.whoami) {
 | 
			
		||||
				let content = JSON.parse(message.content);
 | 
			
		||||
				if (content?.type == 'channel') {
 | 
			
		||||
					this.load_channels();
 | 
			
		||||
				}
 | 
			
		||||
			if (
 | 
			
		||||
				message.author == this.whoami &&
 | 
			
		||||
				JSON.parse(message.content)?.type == 'channel'
 | 
			
		||||
			) {
 | 
			
		||||
				this.load_channels();
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
		if (messages && messages.length) {
 | 
			
		||||
			this.unread = [...this.unread, ...messages];
 | 
			
		||||
			this.unread = this.unread.slice(this.unread.length - 1024);
 | 
			
		||||
		}
 | 
			
		||||
		this.schedule_load_channels_latest();
 | 
			
		||||
		this.schedule_load_latest();
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	async _handle_whoami_changed(event) {
 | 
			
		||||
@@ -265,136 +338,321 @@ class TfElement extends LitElement {
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	async create_identity() {
 | 
			
		||||
		if (confirm('Are you sure you want to create a new identity?')) {
 | 
			
		||||
			await tfrpc.rpc.createIdentity();
 | 
			
		||||
			this.ids = (await tfrpc.rpc.getIdentities()) || [];
 | 
			
		||||
			if (this.ids && !this.whoami) {
 | 
			
		||||
				this.whoami = this.ids[0];
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	async get_latest_private(following) {
 | 
			
		||||
		const k_version = 1;
 | 
			
		||||
		// { "version": 1, "range": [1234, 5678], messages: [ "%1.sha256", "%2.sha256", ... ], latest: rowid }
 | 
			
		||||
		let cache = JSON.parse(
 | 
			
		||||
			(await tfrpc.rpc.databaseGet(`private:${this.whoami}`)) ?? '{}'
 | 
			
		||||
		);
 | 
			
		||||
		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;
 | 
			
		||||
		const k_chunk_count = 256;
 | 
			
		||||
		while (latest - k_chunk_count >= 0) {
 | 
			
		||||
		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, previous, author, sequence, timestamp, hash, json(content) AS content, signature
 | 
			
		||||
					SELECT messages.rowid, messages.id, json(content) AS content
 | 
			
		||||
						FROM messages
 | 
			
		||||
						JOIN json_each(?1) AS following ON messages.author = following.value
 | 
			
		||||
						WHERE
 | 
			
		||||
							messages.rowid > ?2 AND
 | 
			
		||||
							messages.rowid <= ?3 AND
 | 
			
		||||
							messages.rowid > ?1 AND
 | 
			
		||||
							messages.rowid <= ?2 AND
 | 
			
		||||
							json(messages.content) LIKE '"%'
 | 
			
		||||
						ORDER BY sequence DESC
 | 
			
		||||
						ORDER BY messages.rowid DESC
 | 
			
		||||
					`,
 | 
			
		||||
				[JSON.stringify(following), latest - k_chunk_count, latest]
 | 
			
		||||
				[range[0], range[1]]
 | 
			
		||||
			);
 | 
			
		||||
			messages = (await this.decrypt(messages)).filter((x) => x.decrypted);
 | 
			
		||||
			if (messages.length) {
 | 
			
		||||
				return Math.max(...messages.map((x) => x.rowid));
 | 
			
		||||
				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];
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
			latest -= k_chunk_count;
 | 
			
		||||
			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 -1;
 | 
			
		||||
		return [cache.latest, cache.messages];
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	async query_timed(sql, args) {
 | 
			
		||||
		let start = new Date();
 | 
			
		||||
		let result = await tfrpc.rpc.query(sql, args);
 | 
			
		||||
		let end = new Date();
 | 
			
		||||
		console.log((end - start) / 1000, sql.replaceAll(/\s+/g, ' ').trim());
 | 
			
		||||
		return result;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	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) {
 | 
			
		||||
		this.loading_channels_latest++;
 | 
			
		||||
		try {
 | 
			
		||||
			let start_time = new Date();
 | 
			
		||||
			let latest_private = this.get_latest_private(following);
 | 
			
		||||
			let channels = await 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
 | 
			
		||||
				GROUP by channel
 | 
			
		||||
				UNION
 | 
			
		||||
				SELECT '' AS channel, MAX(messages.rowid) AS rowid FROM messages
 | 
			
		||||
				JOIN json_each(?2) AS following ON messages.author = following.value
 | 
			
		||||
				UNION
 | 
			
		||||
				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
 | 
			
		||||
			`,
 | 
			
		||||
				[
 | 
			
		||||
					JSON.stringify(this.channels),
 | 
			
		||||
					JSON.stringify(following),
 | 
			
		||||
					'"' + this.whoami.replace('"', '""') + '"',
 | 
			
		||||
				]
 | 
			
		||||
			);
 | 
			
		||||
			this.channels_latest = Object.fromEntries(
 | 
			
		||||
				channels.map((x) => [x.channel, x.rowid])
 | 
			
		||||
			);
 | 
			
		||||
			console.log('latest', this.channels_latest);
 | 
			
		||||
			console.log('unread', this.channels_unread);
 | 
			
		||||
			console.log('channels took', (new Date() - start_time) / 1000.0);
 | 
			
		||||
			let self = this;
 | 
			
		||||
			latest_private.then(function (latest) {
 | 
			
		||||
				self.channels_latest = Object.assign({}, self.channels_latest, {
 | 
			
		||||
					'🔐': latest,
 | 
			
		||||
				});
 | 
			
		||||
				console.log('private took', (new Date() - start_time) / 1000.0);
 | 
			
		||||
		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([
 | 
			
		||||
				this.query_timed(
 | 
			
		||||
					`
 | 
			
		||||
					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
 | 
			
		||||
				),
 | 
			
		||||
				this.query_timed(
 | 
			
		||||
					`
 | 
			
		||||
					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
 | 
			
		||||
				),
 | 
			
		||||
				this.query_timed(
 | 
			
		||||
					`
 | 
			
		||||
					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
 | 
			
		||||
				),
 | 
			
		||||
				this.query_timed(
 | 
			
		||||
					`
 | 
			
		||||
					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 = {};
 | 
			
		||||
		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) {
 | 
			
		||||
			self.channels_latest = Object.assign({}, self.channels_latest, {
 | 
			
		||||
				'🔐': latest[0],
 | 
			
		||||
			});
 | 
			
		||||
		} finally {
 | 
			
		||||
			this.loading_channels_latest--;
 | 
			
		||||
			console.log('private took', (new Date() - start_time) / 1000.0);
 | 
			
		||||
			self.private_messages = latest[1];
 | 
			
		||||
			self.grouped_private_messages = await self.group_private_messages(
 | 
			
		||||
				latest[1]
 | 
			
		||||
			);
 | 
			
		||||
		});
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	_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));
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	_schedule_load_channels_latest_timer() {
 | 
			
		||||
		--this.loading_channels_latest_scheduled;
 | 
			
		||||
		this.schedule_load_channels_latest();
 | 
			
		||||
	update_progress() {
 | 
			
		||||
		if (
 | 
			
		||||
			!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_channels_latest() {
 | 
			
		||||
		if (!this.loading_channels_latest) {
 | 
			
		||||
			this.load_channels_latest(this.following);
 | 
			
		||||
		} else if (!this.loading_channels_latest_scheduled) {
 | 
			
		||||
			this.loading_channels_latest_scheduled++;
 | 
			
		||||
			setTimeout(this._schedule_load_channels_latest_timer, 5000);
 | 
			
		||||
	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(
 | 
			
		||||
			`
 | 
			
		||||
				SELECT messages_stats.author, messages_stats.max_sequence, messages_stats.max_timestamp AS max_ts FROM messages_stats
 | 
			
		||||
				JOIN json_each(?) AS following
 | 
			
		||||
				ON messages_stats.author = following.value
 | 
			
		||||
			`,
 | 
			
		||||
			[JSON.stringify(Object.keys(users))]
 | 
			
		||||
		);
 | 
			
		||||
		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() {
 | 
			
		||||
		let whoami = this.whoami;
 | 
			
		||||
		let following = await tfrpc.rpc.following([whoami], 2);
 | 
			
		||||
		let users = {};
 | 
			
		||||
		let by_count = [];
 | 
			
		||||
		for (let [id, v] of Object.entries(following)) {
 | 
			
		||||
			users[id] = {
 | 
			
		||||
				following: v.of,
 | 
			
		||||
				blocking: v.ob,
 | 
			
		||||
				followed: v.if,
 | 
			
		||||
				blocked: v.ib,
 | 
			
		||||
			};
 | 
			
		||||
			by_count.push({count: v.of, id: id});
 | 
			
		||||
		this.loading_latest = true;
 | 
			
		||||
		this.reset_progress();
 | 
			
		||||
		try {
 | 
			
		||||
			let start_time = new Date();
 | 
			
		||||
			let whoami = this.whoami;
 | 
			
		||||
			let following = await tfrpc.rpc.following([whoami], 2);
 | 
			
		||||
			let old_users = this.users ?? {};
 | 
			
		||||
			let users = {};
 | 
			
		||||
			let by_count = [];
 | 
			
		||||
			for (let [id, v] of Object.entries(following)) {
 | 
			
		||||
				users[id] = Object.assign(
 | 
			
		||||
					{
 | 
			
		||||
						following: v.of,
 | 
			
		||||
						blocking: v.ob,
 | 
			
		||||
						followed: v.if,
 | 
			
		||||
						blocked: v.ib,
 | 
			
		||||
						follow_depth: following[id]?.d,
 | 
			
		||||
					},
 | 
			
		||||
					old_users[id]
 | 
			
		||||
				);
 | 
			
		||||
				by_count.push({count: v.of, id: id});
 | 
			
		||||
			}
 | 
			
		||||
			let reactions = this.load_recent_reactions();
 | 
			
		||||
			this.load_channels_latest(Object.keys(following));
 | 
			
		||||
			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(
 | 
			
		||||
					'about took',
 | 
			
		||||
					(new Date() - about_start_time) / 1000.0,
 | 
			
		||||
					'seconds for',
 | 
			
		||||
					Object.keys(users).length,
 | 
			
		||||
					'users'
 | 
			
		||||
				);
 | 
			
		||||
			});
 | 
			
		||||
			console.log(
 | 
			
		||||
				`load finished ${whoami} => ${this.whoami} in ${(new Date() - start_time) / 1000}`
 | 
			
		||||
			);
 | 
			
		||||
			await reactions;
 | 
			
		||||
			this.whoami = whoami;
 | 
			
		||||
			this.loaded = whoami;
 | 
			
		||||
		} finally {
 | 
			
		||||
			this.loading_latest = false;
 | 
			
		||||
		}
 | 
			
		||||
		let channels_latest = this.load_channels_latest(Object.keys(following));
 | 
			
		||||
		this.channels_unread = JSON.parse(
 | 
			
		||||
			(await tfrpc.rpc.databaseGet('unread')) ?? '{}'
 | 
			
		||||
		);
 | 
			
		||||
		let start_time = new Date();
 | 
			
		||||
		users = await this.fetch_about(Object.keys(following).sort(), users);
 | 
			
		||||
		console.log(
 | 
			
		||||
			'about took',
 | 
			
		||||
			(new Date() - start_time) / 1000.0,
 | 
			
		||||
			'seconds for',
 | 
			
		||||
			Object.keys(users).length,
 | 
			
		||||
			'users'
 | 
			
		||||
		);
 | 
			
		||||
		start_time = new Date();
 | 
			
		||||
		await channels_latest;
 | 
			
		||||
		this.following = Object.keys(following);
 | 
			
		||||
		this.users = users;
 | 
			
		||||
		console.log(`load finished ${whoami} => ${this.whoami}`);
 | 
			
		||||
		this.whoami = whoami;
 | 
			
		||||
		this.loaded = whoami;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	channel_set_unread(event) {
 | 
			
		||||
@@ -440,13 +698,22 @@ class TfElement extends LitElement {
 | 
			
		||||
					whoami=${this.whoami}
 | 
			
		||||
					.users=${this.users}
 | 
			
		||||
					hash=${this.hash}
 | 
			
		||||
					.unread=${this.unread}
 | 
			
		||||
					@refresh=${() => (this.unread = [])}
 | 
			
		||||
					?loading=${this.loading}
 | 
			
		||||
					?loading=${this.loading || this.loading_about != 0}
 | 
			
		||||
					.channels=${this.channels}
 | 
			
		||||
					.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>
 | 
			
		||||
			`;
 | 
			
		||||
		} else if (this.tab === 'connections') {
 | 
			
		||||
@@ -468,28 +735,16 @@ class TfElement extends LitElement {
 | 
			
		||||
						: null}
 | 
			
		||||
				></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) {
 | 
			
		||||
		this.tab = tab;
 | 
			
		||||
		if (tab === 'news') {
 | 
			
		||||
			this.schedule_load_latest();
 | 
			
		||||
			await tfrpc.rpc.setHash('#');
 | 
			
		||||
		} else if (tab === 'connections') {
 | 
			
		||||
			await tfrpc.rpc.setHash('#connections');
 | 
			
		||||
		} else if (tab === 'query') {
 | 
			
		||||
			await tfrpc.rpc.setHash('#sql=');
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
@@ -497,6 +752,29 @@ class TfElement extends LitElement {
 | 
			
		||||
		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() {
 | 
			
		||||
		let self = this;
 | 
			
		||||
 | 
			
		||||
@@ -511,20 +789,32 @@ class TfElement extends LitElement {
 | 
			
		||||
			'📰': 'news',
 | 
			
		||||
			'📡': 'connections',
 | 
			
		||||
			'🔍': 'search',
 | 
			
		||||
			'👩💻': 'query',
 | 
			
		||||
		};
 | 
			
		||||
 | 
			
		||||
		let tabs = html`
 | 
			
		||||
			<div
 | 
			
		||||
				class="w3-bar w3-theme-l1"
 | 
			
		||||
				style="position: sticky; top: 0; z-index: 10"
 | 
			
		||||
				style="position: static; top: 0; z-index: 10"
 | 
			
		||||
			>
 | 
			
		||||
				<button
 | 
			
		||||
					class="w3-bar-item w3-button w3-circle w3-ripple"
 | 
			
		||||
					@click=${this.refresh}
 | 
			
		||||
				>
 | 
			
		||||
					↻
 | 
			
		||||
				</button>
 | 
			
		||||
				${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(
 | 
			
		||||
					([k, v]) => html`
 | 
			
		||||
						<button
 | 
			
		||||
@@ -541,10 +831,28 @@ class TfElement extends LitElement {
 | 
			
		||||
						</button>
 | 
			
		||||
					`
 | 
			
		||||
				)}
 | 
			
		||||
				<button
 | 
			
		||||
					class="w3-bar-item w3-button w3-right"
 | 
			
		||||
					@click=${this.pick_color}
 | 
			
		||||
				>
 | 
			
		||||
					🎨<span class="w3-hide-small">Color</span>
 | 
			
		||||
				</button>
 | 
			
		||||
			</div>
 | 
			
		||||
		`;
 | 
			
		||||
		let contents =
 | 
			
		||||
			!this.loaded || this.loading
 | 
			
		||||
		let contents = this.guest
 | 
			
		||||
			? html`<div
 | 
			
		||||
					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"
 | 
			
		||||
					>
 | 
			
		||||
@@ -552,12 +860,30 @@ class TfElement extends LitElement {
 | 
			
		||||
						Loading...
 | 
			
		||||
					</div>`
 | 
			
		||||
				: this.render_tab();
 | 
			
		||||
		let progress =
 | 
			
		||||
			this.progress !== undefined
 | 
			
		||||
				? html`
 | 
			
		||||
						<div style="position: absolute; width: 100%" id="progress">
 | 
			
		||||
							<div
 | 
			
		||||
								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: 100%"
 | 
			
		||||
				style="width: 100vw; min-height: 100vh; height: 100vh; display: flex; flex-direction: column"
 | 
			
		||||
				class="w3-theme-dark"
 | 
			
		||||
			>
 | 
			
		||||
				${tabs} ${contents}
 | 
			
		||||
				${progress}
 | 
			
		||||
				<div style="flex: 0 0">${tabs}</div>
 | 
			
		||||
				<div style="flex: 1 1; overflow: auto; contain: layout">
 | 
			
		||||
					${contents}
 | 
			
		||||
				</div>
 | 
			
		||||
			</div>
 | 
			
		||||
		`;
 | 
			
		||||
	}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,7 @@
 | 
			
		||||
import {LitElement, html, unsafeHTML, live} from './lit-all.min.js';
 | 
			
		||||
import * as tfutils from './tf-utils.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';
 | 
			
		||||
 | 
			
		||||
class TfComposeElement extends LitElement {
 | 
			
		||||
@@ -16,6 +16,7 @@ class TfComposeElement extends LitElement {
 | 
			
		||||
			author: {type: String},
 | 
			
		||||
			channel: {type: String},
 | 
			
		||||
			new_thread: {type: Boolean},
 | 
			
		||||
			recipients: {type: Array},
 | 
			
		||||
		};
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
@@ -91,7 +92,9 @@ class TfComposeElement extends LitElement {
 | 
			
		||||
				bubbles: true,
 | 
			
		||||
				composed: true,
 | 
			
		||||
				detail: {
 | 
			
		||||
					id: this.branch,
 | 
			
		||||
					id:
 | 
			
		||||
						this.branch ??
 | 
			
		||||
						(this.recipients ? this.recipients.join(',') : undefined),
 | 
			
		||||
					draft: draft,
 | 
			
		||||
				},
 | 
			
		||||
			})
 | 
			
		||||
@@ -255,10 +258,12 @@ class TfComposeElement extends LitElement {
 | 
			
		||||
		let self = this;
 | 
			
		||||
		let input = document.createElement('input');
 | 
			
		||||
		input.type = 'file';
 | 
			
		||||
		input.onchange = function (event) {
 | 
			
		||||
		input.addEventListener('change', function (event) {
 | 
			
		||||
			input.parentNode.removeChild(input);
 | 
			
		||||
			let file = event.target.files[0];
 | 
			
		||||
			self.add_file(file);
 | 
			
		||||
		};
 | 
			
		||||
		});
 | 
			
		||||
		document.body.appendChild(input);
 | 
			
		||||
		input.click();
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
@@ -289,7 +294,7 @@ class TfComposeElement extends LitElement {
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	firstUpdated() {
 | 
			
		||||
	get_values() {
 | 
			
		||||
		let values = Object.entries(this.users).map((x) => ({
 | 
			
		||||
			key: x[1].name ?? x[0],
 | 
			
		||||
			value: x[0],
 | 
			
		||||
@@ -305,11 +310,15 @@ class TfComposeElement extends LitElement {
 | 
			
		||||
				values
 | 
			
		||||
			);
 | 
			
		||||
		}
 | 
			
		||||
		return values;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	firstUpdated() {
 | 
			
		||||
		let tribute = new Tribute({
 | 
			
		||||
			iframe: this.shadowRoot,
 | 
			
		||||
			collection: [
 | 
			
		||||
				{
 | 
			
		||||
					values: values,
 | 
			
		||||
					values: this.get_values(),
 | 
			
		||||
					selectTemplate: function (item) {
 | 
			
		||||
						return item
 | 
			
		||||
							? `[@${item.original.key}](${item.original.value})`
 | 
			
		||||
@@ -328,6 +337,7 @@ class TfComposeElement extends LitElement {
 | 
			
		||||
			],
 | 
			
		||||
		});
 | 
			
		||||
		tribute.attach(this.renderRoot.getElementById('edit'));
 | 
			
		||||
		this._tribute = tribute;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	updated() {
 | 
			
		||||
@@ -338,6 +348,7 @@ class TfComposeElement extends LitElement {
 | 
			
		||||
			preview.innerHTML = this.process_text(edit.innerText);
 | 
			
		||||
			this.last_updated_text = edit.innerText;
 | 
			
		||||
		}
 | 
			
		||||
		this._tribute.collection[0].values = this.get_values();
 | 
			
		||||
		let encrypt = this.renderRoot.getElementById('encrypt_to');
 | 
			
		||||
		if (encrypt) {
 | 
			
		||||
			let tribute = new Tribute({
 | 
			
		||||
@@ -357,7 +368,7 @@ class TfComposeElement extends LitElement {
 | 
			
		||||
	remove_mention(id) {
 | 
			
		||||
		let draft = this.get_draft();
 | 
			
		||||
		delete draft.mentions[id];
 | 
			
		||||
		setTimeout(() => this.notify(), 0);
 | 
			
		||||
		setTimeout(() => this.notify(draft), 0);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	render_mention(mention) {
 | 
			
		||||
@@ -444,12 +455,15 @@ class TfComposeElement extends LitElement {
 | 
			
		||||
			self.apps = await tfrpc.rpc.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
 | 
			
		||||
			</button>`;
 | 
			
		||||
		} else {
 | 
			
		||||
			return html`<button
 | 
			
		||||
				class="w3-button w3-theme-d1"
 | 
			
		||||
				class="w3-button w3-bar-item w3-theme-d1"
 | 
			
		||||
				@click=${() => (this.apps = null)}
 | 
			
		||||
			>
 | 
			
		||||
				Discard App
 | 
			
		||||
@@ -470,18 +484,9 @@ class TfComposeElement extends LitElement {
 | 
			
		||||
		if (draft.content_warning !== undefined) {
 | 
			
		||||
			return html`
 | 
			
		||||
				<div class="w3-container w3-padding">
 | 
			
		||||
					<p>
 | 
			
		||||
						<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=${self.input} value=${draft.content_warning}></input>
 | 
			
		||||
				</div>
 | 
			
		||||
			`;
 | 
			
		||||
		} else {
 | 
			
		||||
			return html`
 | 
			
		||||
				<input type="checkbox" class="w3-check w3-theme-d1" id="cw" @change=${() => self.set_content_warning('')}></input>
 | 
			
		||||
				<label for="cw">CW</label>
 | 
			
		||||
			`;
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
@@ -500,7 +505,17 @@ class TfComposeElement extends LitElement {
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	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) {
 | 
			
		||||
@@ -522,7 +537,7 @@ class TfComposeElement extends LitElement {
 | 
			
		||||
		return html`
 | 
			
		||||
			<div style="display: flex; flex-direction: row; width: 100%">
 | 
			
		||||
				<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>
 | 
			
		||||
			</div>
 | 
			
		||||
			<ul>
 | 
			
		||||
@@ -544,6 +559,31 @@ class TfComposeElement extends LitElement {
 | 
			
		||||
		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() {
 | 
			
		||||
		let self = this;
 | 
			
		||||
		let draft = self.get_draft();
 | 
			
		||||
@@ -557,25 +597,38 @@ class TfComposeElement extends LitElement {
 | 
			
		||||
			draft.encrypt_to !== undefined
 | 
			
		||||
				? undefined
 | 
			
		||||
				: html`<button
 | 
			
		||||
						class="w3-button w3-theme-d1"
 | 
			
		||||
						class="w3-button w3-bar-item w3-theme-d1"
 | 
			
		||||
						@click=${() => this.set_encrypt([])}
 | 
			
		||||
					>
 | 
			
		||||
						🔐
 | 
			
		||||
						🔐 Encrypt
 | 
			
		||||
					</button>`;
 | 
			
		||||
		let result = html`
 | 
			
		||||
			<style>
 | 
			
		||||
				${generate_theme()}
 | 
			
		||||
			</style>
 | 
			
		||||
			<style>
 | 
			
		||||
				.w3-input:empty::before {
 | 
			
		||||
					content: attr(placeholder);
 | 
			
		||||
				}
 | 
			
		||||
				.w3-input:empty:focus::before {
 | 
			
		||||
					content: '';
 | 
			
		||||
				}
 | 
			
		||||
			</style>
 | 
			
		||||
			<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"
 | 
			
		||||
			>
 | 
			
		||||
				${this.channel !== undefined
 | 
			
		||||
					? html`<p>To #${this.channel}:</p>`
 | 
			
		||||
					: undefined}
 | 
			
		||||
				${this.render_encrypt()}
 | 
			
		||||
				<div class="w3-container w3-padding-small">
 | 
			
		||||
				<header class="w3-container">
 | 
			
		||||
					${this.channel !== undefined
 | 
			
		||||
						? html`<p>To #${this.channel}:</p>`
 | 
			
		||||
						: undefined}
 | 
			
		||||
					${this.render_encrypt()}
 | 
			
		||||
				</header>
 | 
			
		||||
				<div class="w3-container" style="padding: 0 0 16px 0">
 | 
			
		||||
					<div class="w3-half">
 | 
			
		||||
						<span
 | 
			
		||||
							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."
 | 
			
		||||
							id="edit"
 | 
			
		||||
							@input=${this.input}
 | 
			
		||||
@@ -584,26 +637,62 @@ class TfComposeElement extends LitElement {
 | 
			
		||||
							.innerText=${live(draft.text ?? '')}
 | 
			
		||||
						></span>
 | 
			
		||||
					</div>
 | 
			
		||||
					<div class="w3-half w3-padding">
 | 
			
		||||
					<div class="w3-half w3-container">
 | 
			
		||||
						${content_warning}
 | 
			
		||||
						<div id="preview"></div>
 | 
			
		||||
						<p id="preview"></p>
 | 
			
		||||
					</div>
 | 
			
		||||
				</div>
 | 
			
		||||
				${Object.values(draft.mentions || {}).map((x) =>
 | 
			
		||||
					self.render_mention(x)
 | 
			
		||||
				)}
 | 
			
		||||
				${this.render_attach_app()} ${this.render_content_warning()}
 | 
			
		||||
				${this.render_new_thread()}
 | 
			
		||||
				<button class="w3-button w3-theme-d1" id="submit" @click=${this.submit}>
 | 
			
		||||
					Submit
 | 
			
		||||
				</button>
 | 
			
		||||
				<button class="w3-button w3-theme-d1" @click=${this.attach}>
 | 
			
		||||
					Attach
 | 
			
		||||
				</button>
 | 
			
		||||
				${this.render_attach_app_button()} ${encrypt}
 | 
			
		||||
				<button class="w3-button w3-theme-d1" @click=${this.discard}>
 | 
			
		||||
					Discard
 | 
			
		||||
				</button>
 | 
			
		||||
				<footer>
 | 
			
		||||
					${this.render_attach_app()} ${this.render_content_warning()}
 | 
			
		||||
					${this.render_new_thread()}
 | 
			
		||||
					<button
 | 
			
		||||
						class="w3-button w3-theme-d1"
 | 
			
		||||
						id="submit"
 | 
			
		||||
						@click=${this.submit}
 | 
			
		||||
					>
 | 
			
		||||
						Submit
 | 
			
		||||
					</button>
 | 
			
		||||
					<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
 | 
			
		||||
							</button>
 | 
			
		||||
							${this.render_attach_app_button()} ${encrypt}
 | 
			
		||||
							<button
 | 
			
		||||
								class="w3-button w3-bar-item w3-theme-d1"
 | 
			
		||||
								@click=${this.discard}
 | 
			
		||||
							>
 | 
			
		||||
								Discard
 | 
			
		||||
							</button>
 | 
			
		||||
						</div>
 | 
			
		||||
					</div>
 | 
			
		||||
				</footer>
 | 
			
		||||
			</div>
 | 
			
		||||
		`;
 | 
			
		||||
		return result;
 | 
			
		||||
 
 | 
			
		||||
@@ -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 {styles} from './tf-styles.js';
 | 
			
		||||
import {styles, generate_theme} from './tf-styles.js';
 | 
			
		||||
 | 
			
		||||
class TfNewsElement extends LitElement {
 | 
			
		||||
	static get properties() {
 | 
			
		||||
@@ -13,6 +13,8 @@ class TfNewsElement extends LitElement {
 | 
			
		||||
			expanded: {type: Object},
 | 
			
		||||
			channel: {type: String},
 | 
			
		||||
			channel_unread: {type: Number},
 | 
			
		||||
			recent_reactions: {type: Array},
 | 
			
		||||
			hash: {type: String},
 | 
			
		||||
		};
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
@@ -28,6 +30,7 @@ class TfNewsElement extends LitElement {
 | 
			
		||||
		this.drafts = {};
 | 
			
		||||
		this.expanded = {};
 | 
			
		||||
		this.channel_unread = -1;
 | 
			
		||||
		this.recent_reactions = [];
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	process_messages(messages) {
 | 
			
		||||
@@ -157,48 +160,84 @@ class TfNewsElement extends LitElement {
 | 
			
		||||
		return recursive_sort(roots, true);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	group_following(messages) {
 | 
			
		||||
	group_messages(messages) {
 | 
			
		||||
		let result = [];
 | 
			
		||||
		let group = [];
 | 
			
		||||
		let type = undefined;
 | 
			
		||||
		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);
 | 
			
		||||
			} else {
 | 
			
		||||
				if (group.length > 0) {
 | 
			
		||||
				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: 'contact_group',
 | 
			
		||||
						type: `${type}_group`,
 | 
			
		||||
						messages: group,
 | 
			
		||||
					});
 | 
			
		||||
					group = [];
 | 
			
		||||
				}
 | 
			
		||||
				result.push(message);
 | 
			
		||||
				type = undefined;
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
		if (group.length > 0) {
 | 
			
		||||
		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: 'contact_group',
 | 
			
		||||
				type: `${type}_group`,
 | 
			
		||||
				messages: group,
 | 
			
		||||
			});
 | 
			
		||||
		}
 | 
			
		||||
		return result;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	unread_allowed() {
 | 
			
		||||
		return !this.hash?.startsWith('#%') && !this.hash?.startsWith('#@');
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	load_and_render(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)
 | 
			
		||||
		);
 | 
			
		||||
		let unread_rowid = -1;
 | 
			
		||||
		for (let message of final_messages) {
 | 
			
		||||
			if (message.rowid >= this.channel_unread) {
 | 
			
		||||
				unread_rowid = message.rowid;
 | 
			
		||||
		if (this.unread_allowed()) {
 | 
			
		||||
			for (let message of final_messages) {
 | 
			
		||||
				if (message.rowid >= this.channel_unread) {
 | 
			
		||||
					unread_rowid = message.rowid;
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
		return html`
 | 
			
		||||
			<style>
 | 
			
		||||
				${generate_theme()}
 | 
			
		||||
			</style>
 | 
			
		||||
			<div>
 | 
			
		||||
				${final_messages.map(
 | 
			
		||||
				${repeat(
 | 
			
		||||
					final_messages,
 | 
			
		||||
					(x) => x.id,
 | 
			
		||||
					(x) => html`
 | 
			
		||||
						<tf-message
 | 
			
		||||
							.message=${x}
 | 
			
		||||
@@ -209,13 +248,26 @@ class TfNewsElement extends LitElement {
 | 
			
		||||
							collapsed="true"
 | 
			
		||||
							channel=${this.channel}
 | 
			
		||||
							channel_unread=${this.channel_unread}
 | 
			
		||||
							.recent_reactions=${this.recent_reactions}
 | 
			
		||||
						></tf-message>
 | 
			
		||||
						${x.rowid == unread_rowid && x != final_messages[0]
 | 
			
		||||
						${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>
 | 
			
		||||
									<div style="color: #f00; padding: 8px">unread</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>
 | 
			
		||||
 
 | 
			
		||||
@@ -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 tfutils from './tf-utils.js';
 | 
			
		||||
import {styles} from './tf-styles.js';
 | 
			
		||||
import {styles, generate_theme} from './tf-styles.js';
 | 
			
		||||
 | 
			
		||||
class TfProfileElement extends LitElement {
 | 
			
		||||
	static get properties() {
 | 
			
		||||
@@ -11,9 +11,10 @@ class TfProfileElement extends LitElement {
 | 
			
		||||
			id: {type: String},
 | 
			
		||||
			users: {type: Object},
 | 
			
		||||
			size: {type: Number},
 | 
			
		||||
			server_follows_me: {type: Boolean},
 | 
			
		||||
			sequence: {type: Number},
 | 
			
		||||
			following: {type: Boolean},
 | 
			
		||||
			blocking: {type: Boolean},
 | 
			
		||||
			show_followed: {type: Boolean},
 | 
			
		||||
		};
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
@@ -27,7 +28,7 @@ class TfProfileElement extends LitElement {
 | 
			
		||||
		this.id = null;
 | 
			
		||||
		this.users = {};
 | 
			
		||||
		this.size = 0;
 | 
			
		||||
		this.server_follows_me = undefined;
 | 
			
		||||
		this.sequence = 0;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	async load() {
 | 
			
		||||
@@ -63,27 +64,8 @@ class TfProfileElement extends LitElement {
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	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) {
 | 
			
		||||
		let self = this;
 | 
			
		||||
		tfrpc.rpc
 | 
			
		||||
			.appendMessage(
 | 
			
		||||
				this.whoami,
 | 
			
		||||
@@ -95,6 +77,10 @@ class TfProfileElement extends LitElement {
 | 
			
		||||
					change
 | 
			
		||||
				)
 | 
			
		||||
			)
 | 
			
		||||
			.then(function () {
 | 
			
		||||
				self._follow_whoami = undefined;
 | 
			
		||||
				self.load();
 | 
			
		||||
			})
 | 
			
		||||
			.catch(function (error) {
 | 
			
		||||
				alert(error?.message);
 | 
			
		||||
			});
 | 
			
		||||
@@ -156,7 +142,8 @@ class TfProfileElement extends LitElement {
 | 
			
		||||
		let self = this;
 | 
			
		||||
		let input = document.createElement('input');
 | 
			
		||||
		input.type = 'file';
 | 
			
		||||
		input.onchange = function (event) {
 | 
			
		||||
		input.addEventListener('change', function (event) {
 | 
			
		||||
			input.parentNode.removeChild(input);
 | 
			
		||||
			let file = event.target.files[0];
 | 
			
		||||
			file
 | 
			
		||||
				.arrayBuffer()
 | 
			
		||||
@@ -171,67 +158,98 @@ class TfProfileElement extends LitElement {
 | 
			
		||||
				.catch(function (e) {
 | 
			
		||||
					alert(e.message);
 | 
			
		||||
				});
 | 
			
		||||
		};
 | 
			
		||||
		});
 | 
			
		||||
		document.body.appendChild(input);
 | 
			
		||||
		input.click();
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	async server_follow_me(follow) {
 | 
			
		||||
		try {
 | 
			
		||||
			await tfrpc.rpc.setServerFollowingMe(this.whoami, follow);
 | 
			
		||||
		} catch (e) {
 | 
			
		||||
			console.log(e);
 | 
			
		||||
		}
 | 
			
		||||
		try {
 | 
			
		||||
			await this.initial_load();
 | 
			
		||||
		} catch (e) {
 | 
			
		||||
			console.log(e);
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	copy_id() {
 | 
			
		||||
		navigator.clipboard.writeText(this.id);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	render() {
 | 
			
		||||
		if (
 | 
			
		||||
			this.id == this.whoami &&
 | 
			
		||||
			this.editing &&
 | 
			
		||||
			this.server_follows_me === undefined
 | 
			
		||||
		) {
 | 
			
		||||
			this.initial_load();
 | 
			
		||||
	show_image(link) {
 | 
			
		||||
		let div = document.createElement('div');
 | 
			
		||||
		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);
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	toggle_account_list(event) {
 | 
			
		||||
		let content = event.srcElement.nextElementSibling;
 | 
			
		||||
		this.show_followed = !this.show_followed;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	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();
 | 
			
		||||
		let self = this;
 | 
			
		||||
		let profile = this.users[this.id] || {};
 | 
			
		||||
		tfrpc.rpc
 | 
			
		||||
			.query(
 | 
			
		||||
				`SELECT SUM(LENGTH(content)) AS size FROM messages WHERE author = ?`,
 | 
			
		||||
				`SELECT SUM(LENGTH(content)) AS size, MAX(sequence) AS sequence FROM messages WHERE author = ?`,
 | 
			
		||||
				[this.id]
 | 
			
		||||
			)
 | 
			
		||||
			.then(function (result) {
 | 
			
		||||
				self.size = result[0].size;
 | 
			
		||||
				self.sequence = result[0].sequence;
 | 
			
		||||
			});
 | 
			
		||||
		let edit;
 | 
			
		||||
		let follow;
 | 
			
		||||
		let block;
 | 
			
		||||
		if (this.id === this.whoami) {
 | 
			
		||||
			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`
 | 
			
		||||
					<button
 | 
			
		||||
						id="save_profile"
 | 
			
		||||
@@ -243,7 +261,6 @@ class TfProfileElement extends LitElement {
 | 
			
		||||
					<button class="w3-button w3-theme-d1" @click=${this.discard_edits}>
 | 
			
		||||
						Discard
 | 
			
		||||
					</button>
 | 
			
		||||
					${server_follow}
 | 
			
		||||
				`;
 | 
			
		||||
			} else {
 | 
			
		||||
				edit = html`<button
 | 
			
		||||
@@ -276,49 +293,72 @@ class TfProfileElement extends LitElement {
 | 
			
		||||
		let edit_profile = this.editing
 | 
			
		||||
			? html`
 | 
			
		||||
			<div style="flex: 1 0 50%; display: flex; flex-direction: column; gap: 8px">
 | 
			
		||||
				<div class="w3-container">
 | 
			
		||||
					<div>
 | 
			
		||||
						<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>
 | 
			
		||||
					</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>
 | 
			
		||||
					<div>
 | 
			
		||||
						<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>
 | 
			
		||||
					</div>
 | 
			
		||||
					<div>
 | 
			
		||||
						<button class="w3-button w3-theme-d1" @click=${this.attach_image}>Attach Image</button>
 | 
			
		||||
					</div>
 | 
			
		||||
				<div>
 | 
			
		||||
					<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}))} placeholder="Choose a name"></input>
 | 
			
		||||
				</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}))} placeholder="Tell people a little bit about yourself here, if you like.">${this.editing.description}</textarea>
 | 
			
		||||
				<div>
 | 
			
		||||
					<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>
 | 
			
		||||
				</div>
 | 
			
		||||
				<div>
 | 
			
		||||
					<button class="w3-button w3-theme-d1" @click=${this.attach_image}>Attach Image</button>
 | 
			
		||||
				</div>
 | 
			
		||||
			</div>`
 | 
			
		||||
			: null;
 | 
			
		||||
		let image =
 | 
			
		||||
			typeof profile.image == 'string' ? profile.image : profile.image?.link;
 | 
			
		||||
		let image = profile.image;
 | 
			
		||||
		if (typeof image == 'string' && !image.startsWith('&')) {
 | 
			
		||||
			try {
 | 
			
		||||
				image = JSON.parse(image)?.link;
 | 
			
		||||
			} catch {}
 | 
			
		||||
		}
 | 
			
		||||
		image = this.editing?.image ?? image;
 | 
			
		||||
		let description = this.editing?.description ?? profile.description;
 | 
			
		||||
		return html`<div class="w3-container" style="box-sizing: border-box; border: 2px solid black; background-color: rgba(255, 255, 255, 0.2)">
 | 
			
		||||
			<p><tf-user id=${this.id} .users=${this.users}></tf-user> (${tfutils.human_readable_size(this.size)})
 | 
			
		||||
			<input type="text" class="w3-input w3-border w3-theme-d1" readonly value=${this.id}></input>
 | 
			
		||||
			<button class="w3-button w3-theme-d1 w3-ripple" @click=${this.copy_id}>Copy</button>
 | 
			
		||||
			<div style="display: flex; flex-direction: row; gap: 1em">
 | 
			
		||||
				${edit_profile}
 | 
			
		||||
				<div style="flex: 1 0 50%">
 | 
			
		||||
					<div><img src=${'/' + image + '/view'} style="width: 256px; height: auto"></img></div>
 | 
			
		||||
					<div>${unsafeHTML(tfutils.markdown(description))}</div>
 | 
			
		||||
		return html`
 | 
			
		||||
			<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">
 | 
			
		||||
					${edit_profile}
 | 
			
		||||
					<div style="flex: 1 0 50%; contain: layout; overflow: auto; word-wrap: normal; word-break: normal">
 | 
			
		||||
						${
 | 
			
		||||
							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>
 | 
			
		||||
				</div>
 | 
			
		||||
				<div>
 | 
			
		||||
					Following ${profile.following} identities.
 | 
			
		||||
					Followed by ${profile.followed} identities.
 | 
			
		||||
					Blocking ${profile.blocking} identities.
 | 
			
		||||
					Blocked by ${profile.blocked} identities.
 | 
			
		||||
				</div>
 | 
			
		||||
			</div>
 | 
			
		||||
			<div>
 | 
			
		||||
				Following ${profile.following} identities.
 | 
			
		||||
				Followed by ${profile.followed} identities.
 | 
			
		||||
				Blocking ${profile.blocking} identities.
 | 
			
		||||
				Blocked by ${profile.blocked} identities.
 | 
			
		||||
			</div>
 | 
			
		||||
			<p>
 | 
			
		||||
				${edit}
 | 
			
		||||
				${follow}
 | 
			
		||||
				${block}
 | 
			
		||||
			</p>
 | 
			
		||||
			${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}
 | 
			
		||||
					${follow}
 | 
			
		||||
					${block}
 | 
			
		||||
				</p>
 | 
			
		||||
			</footer>
 | 
			
		||||
		</div>`;
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,5 @@
 | 
			
		||||
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 {
 | 
			
		||||
	static get properties() {
 | 
			
		||||
@@ -24,47 +24,57 @@ class TfReactionsModalElement extends LitElement {
 | 
			
		||||
	render() {
 | 
			
		||||
		let self = this;
 | 
			
		||||
		return this.votes?.length
 | 
			
		||||
			? html` <div
 | 
			
		||||
					class="w3-modal w3-animate-opacity"
 | 
			
		||||
					style="display: block; box-sizing: border-box; z-index: 10"
 | 
			
		||||
					@click=${this.clear}
 | 
			
		||||
				>
 | 
			
		||||
			? html` <style>
 | 
			
		||||
						${generate_theme()}
 | 
			
		||||
					</style>
 | 
			
		||||
					<div
 | 
			
		||||
						class="w3-modal-content w3-card-4 w3-theme-d1"
 | 
			
		||||
						onclick="event.stopPropagation()"
 | 
			
		||||
						class="w3-modal w3-animate-opacity"
 | 
			
		||||
						style="display: block; box-sizing: border-box; z-index: 10"
 | 
			
		||||
						@click=${this.clear}
 | 
			
		||||
					>
 | 
			
		||||
						<div class="w3-container w3-padding">
 | 
			
		||||
							<header class="w3-container">
 | 
			
		||||
								<h2>Reactions</h2>
 | 
			
		||||
								<span class="w3-button w3-display-topright" @click=${this.clear}
 | 
			
		||||
									>×</span
 | 
			
		||||
								>
 | 
			
		||||
							</header>
 | 
			
		||||
							<ul class="w3-theme-dark w3-container w3-ul">
 | 
			
		||||
								${this.votes.map(
 | 
			
		||||
									(x) => html`
 | 
			
		||||
										<li class="w3-bar">
 | 
			
		||||
											<span class="w3-bar-item"
 | 
			
		||||
												>${x?.content?.vote?.expression}</span
 | 
			
		||||
											>
 | 
			
		||||
											<tf-user
 | 
			
		||||
												class="w3-bar-item"
 | 
			
		||||
												id=${x.author}
 | 
			
		||||
												.users=${this.users}
 | 
			
		||||
											></tf-user>
 | 
			
		||||
											<span class="w3-bar-item w3-right"
 | 
			
		||||
												>${new Date(x?.timestamp).toLocaleString()}</span
 | 
			
		||||
											>
 | 
			
		||||
										</li>
 | 
			
		||||
									`
 | 
			
		||||
								)}
 | 
			
		||||
							</ul>
 | 
			
		||||
							<footer class="w3-container w3-padding">
 | 
			
		||||
								<button class="w3-button" @click=${this.clear}>Close</button>
 | 
			
		||||
							</footer>
 | 
			
		||||
						<div
 | 
			
		||||
							class="w3-modal-content w3-card-4 w3-theme-d1"
 | 
			
		||||
							onclick="event.stopPropagation()"
 | 
			
		||||
						>
 | 
			
		||||
							<div class="w3-container w3-padding">
 | 
			
		||||
								<header class="w3-container">
 | 
			
		||||
									<h2>Reactions</h2>
 | 
			
		||||
									<span
 | 
			
		||||
										class="w3-button w3-display-topright"
 | 
			
		||||
										@click=${this.clear}
 | 
			
		||||
										>×</span
 | 
			
		||||
									>
 | 
			
		||||
								</header>
 | 
			
		||||
								<ul class="w3-theme-dark w3-container w3-ul">
 | 
			
		||||
									${this.votes
 | 
			
		||||
										.sort((x, y) => y.timestamp - x.timestamp)
 | 
			
		||||
										.map(
 | 
			
		||||
											(x) => html`
 | 
			
		||||
												<li
 | 
			
		||||
													style="display: flex; flex-direction: row; gap: 4px"
 | 
			
		||||
												>
 | 
			
		||||
													<span style="flex-basis: 3em"
 | 
			
		||||
														>${x?.content?.vote?.expression}</span
 | 
			
		||||
													>
 | 
			
		||||
													<tf-user
 | 
			
		||||
														style="flex: 1 1; overflow: hidden"
 | 
			
		||||
														id=${x.author}
 | 
			
		||||
														.users=${this.users}
 | 
			
		||||
													></tf-user>
 | 
			
		||||
													<span
 | 
			
		||||
														style="flex-shrink: 1; white-space: nowrap; overflow: hidden; text-overflow: ellipsis"
 | 
			
		||||
														>${new Date(x?.timestamp).toLocaleString()}</span
 | 
			
		||||
													>
 | 
			
		||||
												</li>
 | 
			
		||||
											`
 | 
			
		||||
										)}
 | 
			
		||||
								</ul>
 | 
			
		||||
								<footer class="w3-container w3-padding">
 | 
			
		||||
									<button class="w3-button" @click=${this.clear}>Close</button>
 | 
			
		||||
								</footer>
 | 
			
		||||
							</div>
 | 
			
		||||
						</div>
 | 
			
		||||
					</div>
 | 
			
		||||
				</div>`
 | 
			
		||||
					</div>`
 | 
			
		||||
			: undefined;
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -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`
 | 
			
		||||
	img {
 | 
			
		||||
@@ -43,12 +44,14 @@ const tf = css`
 | 
			
		||||
		border-left: 4px solid #fff;
 | 
			
		||||
		padding: 8px;
 | 
			
		||||
		padding-left: 12px;
 | 
			
		||||
		margin-left: 0;
 | 
			
		||||
		margin-right: 0;
 | 
			
		||||
	}
 | 
			
		||||
`;
 | 
			
		||||
 | 
			
		||||
// prettier-ignore
 | 
			
		||||
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}
 | 
			
		||||
/* 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}
 | 
			
		||||
@@ -88,7 +91,7 @@ hr{border:0;border-top:1px solid #eee;margin:20px 0}
 | 
			
		||||
.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-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%}
 | 
			
		||||
@@ -136,7 +139,7 @@ hr{border:0;border-top:1px solid #eee;margin:20px 0}
 | 
			
		||||
.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-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}}
 | 
			
		||||
@@ -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-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%}
 | 
			
		||||
@@ -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-hover-none:hover{box-shadow:none!important}
 | 
			
		||||
/* 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-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-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}
 | 
			
		||||
@@ -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-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-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-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-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-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-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}
 | 
			
		||||
@@ -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}
 | 
			
		||||
`;
 | 
			
		||||
 | 
			
		||||
// prettier-ignore
 | 
			
		||||
const w3_2016_snorkel_blue = css`
 | 
			
		||||
.w3-theme-l5 {color:#000 !important; background-color:#e9f5ff !important}
 | 
			
		||||
.w3-theme-l4 {color:#000 !important; background-color:#b5dffd !important}
 | 
			
		||||
.w3-theme-l3 {color:#000 !important; background-color:#6bc0fc !important}
 | 
			
		||||
.w3-theme-l2 {color:#fff !important; background-color:#21a0fa !important}
 | 
			
		||||
.w3-theme-l1 {color:#fff !important; background-color:#0479cc !important}
 | 
			
		||||
.w3-theme-d1 {color:#fff !important; background-color:#024575 !important}
 | 
			
		||||
.w3-theme-d2 {color:#fff !important; background-color:#023e68 !important}
 | 
			
		||||
.w3-theme-d3 {color:#fff !important; background-color:#02365b !important}
 | 
			
		||||
.w3-theme-d4 {color:#fff !important; background-color:#022e4e !important}
 | 
			
		||||
.w3-theme-d5 {color:#fff !important; background-color:#012641 !important}
 | 
			
		||||
function rgb_to_hsl(r, g, b) {
 | 
			
		||||
	let min,
 | 
			
		||||
		max,
 | 
			
		||||
		i,
 | 
			
		||||
		l,
 | 
			
		||||
		s,
 | 
			
		||||
		maxcolor,
 | 
			
		||||
		h,
 | 
			
		||||
		rgb = [];
 | 
			
		||||
	rgb[0] = r / 255;
 | 
			
		||||
	rgb[1] = g / 255;
 | 
			
		||||
	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:#e9f5ff !important}
 | 
			
		||||
.w3-theme-dark {color:#fff !important; background-color:#012641 !important}
 | 
			
		||||
.w3-theme-action {color:#fff !important; background-color:#012641 !important}
 | 
			
		||||
function hex_to_rgb(hex) {
 | 
			
		||||
	if (hex.charAt(0) == '#') {
 | 
			
		||||
		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:#034f84 !important}
 | 
			
		||||
.w3-text-theme {color:#034f84 !important}
 | 
			
		||||
.w3-border-theme {border-color:#034f84 !important}
 | 
			
		||||
function hsl_to_rgb(hue, sat, light) {
 | 
			
		||||
	let t2;
 | 
			
		||||
	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:#034f84 !important}
 | 
			
		||||
.w3-hover-text-theme:hover {color:#034f84 !important}
 | 
			
		||||
.w3-hover-border-theme:hover {border-color:#034f84 !important}
 | 
			
		||||
`;
 | 
			
		||||
function rgb_to_hex(rgb) {
 | 
			
		||||
	const hex_pair = (x) => Math.floor(x).toString(16).padStart(2, '0');
 | 
			
		||||
	return `#${hex_pair(rgb[0])}${hex_pair(rgb[1])}${hex_pair(rgb[2])}`;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export let styles = [tf, w3, w3_2016_snorkel_blue];
 | 
			
		||||
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);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
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 * as tfrpc from '/static/tfrpc.js';
 | 
			
		||||
import {styles} from './tf-styles.js';
 | 
			
		||||
import {styles, generate_theme} from './tf-styles.js';
 | 
			
		||||
 | 
			
		||||
class TfTabConnectionsElement extends LitElement {
 | 
			
		||||
	static get properties() {
 | 
			
		||||
@@ -15,6 +15,7 @@ class TfTabConnectionsElement extends LitElement {
 | 
			
		||||
			connect_attempt: {type: Object},
 | 
			
		||||
			connect_message: {type: String},
 | 
			
		||||
			connect_success: {type: Boolean},
 | 
			
		||||
			peer_exchange: {type: Boolean},
 | 
			
		||||
		};
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
@@ -47,6 +48,20 @@ class TfTabConnectionsElement extends LitElement {
 | 
			
		||||
		tfrpc.rpc.getServerIdentity().then(function (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) {
 | 
			
		||||
@@ -103,6 +118,23 @@ class TfTabConnectionsElement extends LitElement {
 | 
			
		||||
		</div>`;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	render_progress(name, value, max) {
 | 
			
		||||
		if (max && value != max) {
 | 
			
		||||
			return html`
 | 
			
		||||
				<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`
 | 
			
		||||
@@ -142,14 +174,28 @@ class TfTabConnectionsElement extends LitElement {
 | 
			
		||||
			}, {})
 | 
			
		||||
		);
 | 
			
		||||
		return html`
 | 
			
		||||
			<button
 | 
			
		||||
				class="w3-button w3-theme-d1"
 | 
			
		||||
				@click=${() => tfrpc.rpc.closeConnection(connection.id)}
 | 
			
		||||
			>
 | 
			
		||||
				Close
 | 
			
		||||
			</button>
 | 
			
		||||
			${connection.connected
 | 
			
		||||
				? html`
 | 
			
		||||
						<button
 | 
			
		||||
							class="w3-button w3-theme-d1"
 | 
			
		||||
							@click=${() => tfrpc.rpc.closeConnection(connection.id)}
 | 
			
		||||
						>
 | 
			
		||||
							Close
 | 
			
		||||
						</button>
 | 
			
		||||
					`
 | 
			
		||||
				: undefined}
 | 
			
		||||
			${connection.flags.one_shot ? '🔃' : undefined}
 | 
			
		||||
			<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
 | 
			
		||||
				? '🚇'
 | 
			
		||||
				: html`(${connection.host}:${connection.port})`}
 | 
			
		||||
@@ -175,6 +221,9 @@ class TfTabConnectionsElement extends LitElement {
 | 
			
		||||
					.map((x) => html`<li>${this.render_connection(x)}</li>`)}
 | 
			
		||||
				${this.render_room_peers(connection.id)}
 | 
			
		||||
			</ul>
 | 
			
		||||
			<div ?hidden=${!connection.destroy_reason} class="w3-panel w3-red">
 | 
			
		||||
				<p>${connection.destroy_reason}</p>
 | 
			
		||||
			</div>
 | 
			
		||||
		`;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
@@ -199,10 +248,44 @@ class TfTabConnectionsElement extends LitElement {
 | 
			
		||||
			});
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	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() {
 | 
			
		||||
		let self = this;
 | 
			
		||||
		return html`
 | 
			
		||||
			<style>
 | 
			
		||||
				${generate_theme()}
 | 
			
		||||
			</style>
 | 
			
		||||
			<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>
 | 
			
		||||
				<textarea class="w3-input w3-theme-d1" id="code"></textarea>
 | 
			
		||||
				${this.render_message(this.renderRoot.getElementById('code')?.value)}
 | 
			
		||||
@@ -213,27 +296,33 @@ class TfTabConnectionsElement extends LitElement {
 | 
			
		||||
				>
 | 
			
		||||
					Connect
 | 
			
		||||
				</button>
 | 
			
		||||
				<h2>Broadcasts</h2>
 | 
			
		||||
				<ul class="w3-ul w3-border">
 | 
			
		||||
					${this.broadcasts
 | 
			
		||||
						.filter((x) => x.address)
 | 
			
		||||
						.filter(
 | 
			
		||||
							(x) => self.connections.map((c) => c.id).indexOf(x.pubkey) == -1
 | 
			
		||||
						)
 | 
			
		||||
						.map((x) => self.render_broadcast(x))}
 | 
			
		||||
				<h2
 | 
			
		||||
					class="w3-button w3-block w3-theme-d1"
 | 
			
		||||
					@click=${() => self.toggle_accordian('connections')}
 | 
			
		||||
				>
 | 
			
		||||
					Connections (${this.valid_connections().length})
 | 
			
		||||
				</h2>
 | 
			
		||||
				<ul class="w3-ul w3-border" id="connections">
 | 
			
		||||
					${this.valid_connections().map(
 | 
			
		||||
						(x) => html` <li class="w3-bar">${this.render_connection(x)}</li> `
 | 
			
		||||
					)}
 | 
			
		||||
				</ul>
 | 
			
		||||
				<h2>Connections</h2>
 | 
			
		||||
				<ul class="w3-ul w3-border">
 | 
			
		||||
					${this.connections
 | 
			
		||||
						.filter((x) => x.tunnel === undefined)
 | 
			
		||||
						.map(
 | 
			
		||||
							(x) => html`
 | 
			
		||||
								<li class="w3-bar">${this.render_connection(x)}</li>
 | 
			
		||||
							`
 | 
			
		||||
						)}
 | 
			
		||||
				<h2
 | 
			
		||||
					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>Stored Connections</h2>
 | 
			
		||||
				<ul class="w3-ul w3-border">
 | 
			
		||||
				<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(
 | 
			
		||||
						(x) => html`
 | 
			
		||||
							<li>
 | 
			
		||||
@@ -253,6 +342,12 @@ class TfTabConnectionsElement extends LitElement {
 | 
			
		||||
									<div class="w3-bar-item">
 | 
			
		||||
										<tf-user id=${x.pubkey} .users=${self.users}></tf-user>
 | 
			
		||||
										<div><small>${x.address}:${x.port}</small></div>
 | 
			
		||||
										<div>
 | 
			
		||||
											<small
 | 
			
		||||
												>Last connection:
 | 
			
		||||
												${new Date(x.last_success * 1000)}</small
 | 
			
		||||
											>
 | 
			
		||||
										</div>
 | 
			
		||||
									</div>
 | 
			
		||||
								</div>
 | 
			
		||||
								${this.render_message(x)}
 | 
			
		||||
@@ -260,25 +355,33 @@ class TfTabConnectionsElement extends LitElement {
 | 
			
		||||
						`
 | 
			
		||||
					)}
 | 
			
		||||
				</ul>
 | 
			
		||||
				<h2>Local Accounts</h2>
 | 
			
		||||
				<ul class="w3-ul w3-border">
 | 
			
		||||
				<h2
 | 
			
		||||
					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(
 | 
			
		||||
						(x) =>
 | 
			
		||||
							html`<li class="w3-bar">
 | 
			
		||||
							html`<div
 | 
			
		||||
								class="w3-tag w3-round w3-theme-l3"
 | 
			
		||||
								style="padding: 4px; margin: 2px; max-width: 100%; text-wrap: nowrap; overflow: hidden"
 | 
			
		||||
							>
 | 
			
		||||
								${x == this.server_identity
 | 
			
		||||
									? html`<span class="w3-tag w3-medium w3-round w3-theme-l1"
 | 
			
		||||
											>🖥 local server</span
 | 
			
		||||
										>`
 | 
			
		||||
									? html`<div class="w3-tag w3-medium w3-round w3-theme-l1">
 | 
			
		||||
											🖥 local server
 | 
			
		||||
										</div>`
 | 
			
		||||
									: undefined}
 | 
			
		||||
								${this.my_identities.indexOf(x) != -1
 | 
			
		||||
									? 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-d1">
 | 
			
		||||
											😎 you
 | 
			
		||||
										</div>`
 | 
			
		||||
									: undefined}
 | 
			
		||||
								<tf-user id=${x} .users=${this.users}></tf-user>
 | 
			
		||||
							</li>`
 | 
			
		||||
							</div>`
 | 
			
		||||
					)}
 | 
			
		||||
				</ul>
 | 
			
		||||
				</div>
 | 
			
		||||
			</div>
 | 
			
		||||
		`;
 | 
			
		||||
	}
 | 
			
		||||
 
 | 
			
		||||
@@ -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 {styles} from './tf-styles.js';
 | 
			
		||||
import {styles, generate_theme} from './tf-styles.js';
 | 
			
		||||
 | 
			
		||||
class TfTabNewsFeedElement extends LitElement {
 | 
			
		||||
	static get properties() {
 | 
			
		||||
@@ -17,6 +17,9 @@ class TfTabNewsFeedElement extends LitElement {
 | 
			
		||||
			loading: {type: Number},
 | 
			
		||||
			time_range: {type: Array},
 | 
			
		||||
			time_loading: {type: Array},
 | 
			
		||||
			private_messages: {type: Array},
 | 
			
		||||
			grouped_private_messages: {type: Object},
 | 
			
		||||
			recent_reactions: {type: Array},
 | 
			
		||||
		};
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
@@ -36,6 +39,7 @@ class TfTabNewsFeedElement extends LitElement {
 | 
			
		||||
		this.start_time = new Date().valueOf();
 | 
			
		||||
		this.time_range = [0, 0];
 | 
			
		||||
		this.time_loading = undefined;
 | 
			
		||||
		this.recent_reactions = [];
 | 
			
		||||
		this.loading = 0;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
@@ -45,9 +49,73 @@ class TfTabNewsFeedElement extends LitElement {
 | 
			
		||||
			: this.hash.substring(1);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	async _fetch_related_messages(messages) {
 | 
			
		||||
		let refs = 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(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
 | 
			
		||||
				JOIN json_each(?2) refs ON messages.id = refs.value
 | 
			
		||||
				JOIN json_each(?1) AS following ON messages.author = following.value
 | 
			
		||||
			`,
 | 
			
		||||
			[JSON.stringify(this.following), JSON.stringify(refs.map((x) => x.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(
 | 
			
		||||
				`
 | 
			
		||||
@@ -57,53 +125,49 @@ class TfTabNewsFeedElement extends LitElement {
 | 
			
		||||
						JOIN json_each(?2) AS following ON messages.author = following.value
 | 
			
		||||
						WHERE
 | 
			
		||||
							messages.author != ?1 AND
 | 
			
		||||
							messages.timestamp >= ?3 AND
 | 
			
		||||
							messages.timestamp < ?4
 | 
			
		||||
						ORDER BY timestamp DESC limit 20)
 | 
			
		||||
					SELECT messages.rowid, messages.id, messages.previous, messages.author, messages.sequence, messages.timestamp, messages.hash, json(messages.content) AS content, messages.signature
 | 
			
		||||
							(?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
 | 
			
		||||
					UNION
 | 
			
		||||
					SELECT * FROM mentions
 | 
			
		||||
					SELECT TRUE AS is_primary, * FROM mentions
 | 
			
		||||
				`,
 | 
			
		||||
				[
 | 
			
		||||
					'"' + this.whoami.replace('"', '""') + '"',
 | 
			
		||||
					JSON.stringify(this.following),
 | 
			
		||||
					start_time,
 | 
			
		||||
					end_time,
 | 
			
		||||
					k_max_results,
 | 
			
		||||
				]
 | 
			
		||||
			);
 | 
			
		||||
		} else if (this.hash.startsWith('#@')) {
 | 
			
		||||
			result = await tfrpc.rpc.query(
 | 
			
		||||
				`
 | 
			
		||||
					WITH mine AS (SELECT rowid, id, previous, author, sequence, timestamp, hash, json(content) AS content, signature
 | 
			
		||||
						FROM messages
 | 
			
		||||
						WHERE messages.author = ?
 | 
			
		||||
						ORDER BY sequence DESC)
 | 
			
		||||
					SELECT messages.rowid, messages.id, messages.previous, messages.author, messages.sequence, messages.timestamp, messages.hash, json(messages.content) AS content, messages.signature
 | 
			
		||||
						FROM mine
 | 
			
		||||
						JOIN messages_refs ON mine.id = messages_refs.ref
 | 
			
		||||
					WITH
 | 
			
		||||
						selected AS (SELECT rowid, id, previous, author, sequence, timestamp, hash, json(content) AS content, signature
 | 
			
		||||
							FROM messages
 | 
			
		||||
							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
 | 
			
		||||
						WHERE
 | 
			
		||||
							mine.timestamp >= ?2 AND
 | 
			
		||||
							mine.timestamp < ?3
 | 
			
		||||
					UNION
 | 
			
		||||
					SELECT * FROM mine
 | 
			
		||||
					WHERE
 | 
			
		||||
						mine.timestamp >= ?2 AND
 | 
			
		||||
						mine.timestamp < ?3
 | 
			
		||||
					SELECT TRUE AS is_primary, * FROM selected
 | 
			
		||||
				`,
 | 
			
		||||
				[this.hash.substring(1), start_time, end_time]
 | 
			
		||||
				[this.hash.substring(1), start_time, end_time, k_max_results]
 | 
			
		||||
			);
 | 
			
		||||
		} else if (this.hash.startsWith('#%')) {
 | 
			
		||||
			result = await tfrpc.rpc.query(
 | 
			
		||||
				`
 | 
			
		||||
					SELECT id, previous, author, sequence, timestamp, hash, json(content) AS content, signature
 | 
			
		||||
					SELECT TRUE AS is_primary, id, previous, author, sequence, timestamp, hash, json(content) AS content, signature
 | 
			
		||||
					FROM messages
 | 
			
		||||
					WHERE id = ?1
 | 
			
		||||
					WHERE messages.id = ?1
 | 
			
		||||
					UNION
 | 
			
		||||
					SELECT id, previous, author, sequence, timestamp, hash, json(content) AS content, signature
 | 
			
		||||
					SELECT FALSE AS is_primary, id, previous, author, sequence, timestamp, hash, json(content) AS content, signature
 | 
			
		||||
					FROM messages JOIN messages_refs
 | 
			
		||||
					ON messages.id = messages_refs.message
 | 
			
		||||
					WHERE messages_refs.ref = ?1
 | 
			
		||||
@@ -111,134 +175,156 @@ class TfTabNewsFeedElement extends LitElement {
 | 
			
		||||
				[this.hash.substring(1)]
 | 
			
		||||
			);
 | 
			
		||||
		} else if (this.hash.startsWith('##')) {
 | 
			
		||||
			let promises = [];
 | 
			
		||||
			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.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(?) AS following ON messages.author = following.value
 | 
			
		||||
							WHERE
 | 
			
		||||
								messages.timestamp >= ? AND
 | 
			
		||||
								messages.timestamp < ? AND
 | 
			
		||||
								messages.content ->> 'channel' = ?
 | 
			
		||||
							ORDER BY messages.timestamp DESC)
 | 
			
		||||
						SELECT messages.rowid, 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
 | 
			
		||||
						SELECT messages.rowid, 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 messages.rowid, messages.id, messages.previous, messages.author, messages.sequence, messages.timestamp, messages.hash, json(messages.content) AS content, messages.signature
 | 
			
		||||
							FROM messages_fts(?5)
 | 
			
		||||
							JOIN messages ON messages.rowid = messages_fts.rowid
 | 
			
		||||
							JOIN json_each(?1) AS following ON messages.author = following.value
 | 
			
		||||
							JOIN json_tree(messages.content, '$.mentions') AS mention ON mention.value = '#' || ?4
 | 
			
		||||
							WHERE
 | 
			
		||||
								messages.timestamp >= ?2 AND
 | 
			
		||||
								messages.timestamp < ?3
 | 
			
		||||
						UNION
 | 
			
		||||
						SELECT news.* FROM news
 | 
			
		||||
					`,
 | 
			
		||||
						[
 | 
			
		||||
							JSON.stringify(this.following.slice(i, i + k_following_limit)),
 | 
			
		||||
							start_time,
 | 
			
		||||
							end_time,
 | 
			
		||||
							this.hash.substring(2),
 | 
			
		||||
							'"#' + this.hash.substring(2).replace('"', '""') + '"',
 | 
			
		||||
						]
 | 
			
		||||
					)
 | 
			
		||||
				);
 | 
			
		||||
			}
 | 
			
		||||
			result = [].concat(...(await Promise.all(promises)));
 | 
			
		||||
		} else if (this.hash == '#🔐') {
 | 
			
		||||
			let t0 = new Date();
 | 
			
		||||
			let initial_messages = await tfrpc.rpc.query(
 | 
			
		||||
				`
 | 
			
		||||
					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
 | 
			
		||||
								JOIN json_each(?) AS following ON messages.author = following.value
 | 
			
		||||
								WHERE messages.content ->> 'channel' = ?4 AND messages.content ->> 'type' != 'vote'
 | 
			
		||||
							UNION
 | 
			
		||||
							SELECT messages.rowid, messages.id, messages.previous, messages.author, messages.sequence, messages.timestamp, messages.hash, json(messages.content) AS content, messages.signature
 | 
			
		||||
								FROM messages_refs
 | 
			
		||||
								JOIN messages ON messages.id = messages_refs.message
 | 
			
		||||
								JOIN json_each(?1) AS following ON messages.author = following.value
 | 
			
		||||
								WHERE messages_refs.ref = '#' || ?4 AND messages.content ->> 'type' != 'vote'
 | 
			
		||||
							)
 | 
			
		||||
					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),
 | 
			
		||||
					start_time,
 | 
			
		||||
					end_time,
 | 
			
		||||
					this.hash.substring(2),
 | 
			
		||||
					k_max_results,
 | 
			
		||||
				]
 | 
			
		||||
			);
 | 
			
		||||
			let t1 = new Date();
 | 
			
		||||
			result = await this._fetch_related_messages(initial_messages);
 | 
			
		||||
			let t2 = new Date();
 | 
			
		||||
			console.log(
 | 
			
		||||
				`load of ${result.length} rows took ${(t2 - t0) / 1000} (${(t1 - t0) / 1000} to find ${initial_messages.length} initial messages, ${(t2 - t1) / 1000} to find ${result.length} total messages) following=${this.following.length} st=${start_time} et=${end_time}`
 | 
			
		||||
			);
 | 
			
		||||
		} else if (this.hash.startsWith('#🔐')) {
 | 
			
		||||
			let ids =
 | 
			
		||||
				this.hash == '#🔐' ? [] : this.hash.substring('#🔐'.length).split(',');
 | 
			
		||||
			result = await tfrpc.rpc.query(
 | 
			
		||||
				`
 | 
			
		||||
					SELECT messages.rowid, messages.id, previous, author, sequence, timestamp, hash, json(content) AS content, signature
 | 
			
		||||
					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.timestamp >= ?2 AND
 | 
			
		||||
							messages.timestamp < ?3 AND
 | 
			
		||||
							json(messages.content) LIKE '"%'
 | 
			
		||||
						ORDER BY sequence DESC
 | 
			
		||||
							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]
 | 
			
		||||
				[JSON.stringify(this.following), start_time, end_time, k_max_results]
 | 
			
		||||
			);
 | 
			
		||||
			result = (await this.decrypt(result)).filter((x) => x.decrypted);
 | 
			
		||||
		} else {
 | 
			
		||||
			let promises = [];
 | 
			
		||||
			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.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(?) AS following ON messages.author = following.value
 | 
			
		||||
						WHERE messages.timestamp >= ? AND messages.timestamp < ?
 | 
			
		||||
						ORDER BY messages.timestamp DESC)
 | 
			
		||||
						SELECT messages.rowid, 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
 | 
			
		||||
						SELECT messages.rowid, 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.slice(i, i + k_following_limit)),
 | 
			
		||||
							start_time,
 | 
			
		||||
							end_time,
 | 
			
		||||
						]
 | 
			
		||||
					)
 | 
			
		||||
				);
 | 
			
		||||
			}
 | 
			
		||||
			result = [].concat(...(await Promise.all(promises)));
 | 
			
		||||
			let t0 = new Date();
 | 
			
		||||
			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)),
 | 
			
		||||
				]
 | 
			
		||||
			);
 | 
			
		||||
			let t1 = new Date();
 | 
			
		||||
			result = await this._fetch_related_messages(initial_messages);
 | 
			
		||||
			let t2 = new Date();
 | 
			
		||||
			console.log(
 | 
			
		||||
				`load of ${result.length} rows took ${(t2 - t0) / 1000} (${(t1 - t0) / 1000} to find ${initial_messages.length} initial messages, ${(t2 - t1) / 1000} to find ${result.length} total messages) following=${this.following.length} st=${start_time} et=${end_time}`
 | 
			
		||||
			);
 | 
			
		||||
		}
 | 
			
		||||
		this.time_loading = undefined;
 | 
			
		||||
		return result;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	update_time_range_from_messages(messages) {
 | 
			
		||||
		let only_primary = messages.filter((x) => x.is_primary);
 | 
			
		||||
		this.time_range = [
 | 
			
		||||
			messages.reduce(
 | 
			
		||||
			only_primary.reduce(
 | 
			
		||||
				(accumulator, current) => Math.min(accumulator, current.timestamp),
 | 
			
		||||
				this.time_range[0]
 | 
			
		||||
			),
 | 
			
		||||
			messages.reduce(
 | 
			
		||||
			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() {
 | 
			
		||||
		this.loading++;
 | 
			
		||||
		this.loading_canceled = false;
 | 
			
		||||
		try {
 | 
			
		||||
			let more = [];
 | 
			
		||||
			while (!more.length && !this.loading_canceled) {
 | 
			
		||||
				let last_start_time = this.start_time;
 | 
			
		||||
				this.start_time = last_start_time - 7 * 24 * 60 * 60 * 1000;
 | 
			
		||||
				more = await this.fetch_messages(this.start_time, last_start_time);
 | 
			
		||||
				this.update_time_range_from_messages(
 | 
			
		||||
					more.filter(
 | 
			
		||||
						(x) =>
 | 
			
		||||
							x.timestamp >= this.start_time && x.timestamp < last_start_time
 | 
			
		||||
					)
 | 
			
		||||
				);
 | 
			
		||||
			let last_start_time = this.time_range[0];
 | 
			
		||||
			try {
 | 
			
		||||
				more = await this.fetch_messages(null, last_start_time);
 | 
			
		||||
			} catch (e) {
 | 
			
		||||
				console.log(e);
 | 
			
		||||
			}
 | 
			
		||||
			this.update_time_range_from_messages(
 | 
			
		||||
				more.filter((x) => x.timestamp < last_start_time)
 | 
			
		||||
			);
 | 
			
		||||
			this.messages = await this.decrypt([...more, ...this.messages]);
 | 
			
		||||
		} finally {
 | 
			
		||||
			this.loading--;
 | 
			
		||||
@@ -274,75 +360,78 @@ class TfTabNewsFeedElement extends LitElement {
 | 
			
		||||
		return result;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	merge_messages(old_messages, new_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[1] - 24 * 60 * 60 * 1000,
 | 
			
		||||
				end_time
 | 
			
		||||
			);
 | 
			
		||||
			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[1] && x.timestamp < end_time
 | 
			
		||||
					(x) => x.timestamp >= this.time_range[0] && x.timestamp < end_time
 | 
			
		||||
				)
 | 
			
		||||
			);
 | 
			
		||||
		} finally {
 | 
			
		||||
			this.loading--;
 | 
			
		||||
		}
 | 
			
		||||
		this.messages = Object.values(
 | 
			
		||||
			Object.fromEntries([...this.messages, ...messages].map((x) => [x.id, x]))
 | 
			
		||||
		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 {
 | 
			
		||||
			this.messages = [];
 | 
			
		||||
			this._messages_hash = this.hash;
 | 
			
		||||
			this._messages_following = this.following;
 | 
			
		||||
			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) +
 | 
			
		||||
				JSON.stringify(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 = [this.start_time, now + 24 * 60 * 60 * 1000];
 | 
			
		||||
			messages = await this.fetch_messages(
 | 
			
		||||
				this.time_range[0],
 | 
			
		||||
				this.time_range[1]
 | 
			
		||||
			);
 | 
			
		||||
			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[0] &&
 | 
			
		||||
						x.timestamp < this.time_range[1]
 | 
			
		||||
				)
 | 
			
		||||
				messages.filter((x) => x.timestamp < this.time_range[1])
 | 
			
		||||
			);
 | 
			
		||||
			messages = await this.decrypt(messages);
 | 
			
		||||
			if (!messages.length) {
 | 
			
		||||
				let more = [];
 | 
			
		||||
				while (!more.length && start_time >= 0) {
 | 
			
		||||
					let last_start_time = start_time;
 | 
			
		||||
					start_time = last_start_time - 7 * 24 * 60 * 60 * 1000;
 | 
			
		||||
					more = await this.fetch_messages(start_time, last_start_time);
 | 
			
		||||
					this.update_time_range_from_messages(
 | 
			
		||||
						more.filter(
 | 
			
		||||
							(x) => x.timestamp >= start_time && x.timestamp < last_start_time
 | 
			
		||||
						)
 | 
			
		||||
					);
 | 
			
		||||
				}
 | 
			
		||||
				messages = await this.decrypt([...more, ...this.messages]);
 | 
			
		||||
			}
 | 
			
		||||
		} finally {
 | 
			
		||||
			this.loading--;
 | 
			
		||||
		}
 | 
			
		||||
		this.messages = messages;
 | 
			
		||||
		if (this.hash == original_hash) {
 | 
			
		||||
			this.messages = this.merge_messages(this.messages, messages);
 | 
			
		||||
		}
 | 
			
		||||
		this.time_loading = undefined;
 | 
			
		||||
		console.log(`loading messages done for ${self.whoami}`);
 | 
			
		||||
		console.log(
 | 
			
		||||
			`loading ${messages.length} messages done for ${self.whoami} in ${(new Date() - start_time) / 1000}s`
 | 
			
		||||
		);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	mark_all_read() {
 | 
			
		||||
@@ -364,11 +453,44 @@ class TfTabNewsFeedElement extends LitElement {
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	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() {
 | 
			
		||||
		if (
 | 
			
		||||
			!this.messages ||
 | 
			
		||||
			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) +
 | 
			
		||||
					JSON.stringify(this.grouped_private_messages) ||
 | 
			
		||||
			this._channels_latest !==
 | 
			
		||||
				JSON.stringify(Object.keys(this.channels_latest))
 | 
			
		||||
		) {
 | 
			
		||||
			console.log(
 | 
			
		||||
				`loading messages for ${this.whoami} (following ${this.following.length})`
 | 
			
		||||
@@ -379,9 +501,16 @@ class TfTabNewsFeedElement extends LitElement {
 | 
			
		||||
		if (!this.hash.startsWith('#%')) {
 | 
			
		||||
			more = html`
 | 
			
		||||
				<p>
 | 
			
		||||
					<button class="w3-button w3-theme-d1" @click=${this.mark_all_read}>
 | 
			
		||||
						Mark All Read
 | 
			
		||||
					</button>
 | 
			
		||||
					${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"
 | 
			
		||||
@@ -412,10 +541,19 @@ class TfTabNewsFeedElement extends LitElement {
 | 
			
		||||
				</p>
 | 
			
		||||
			`;
 | 
			
		||||
		}
 | 
			
		||||
		return html`
 | 
			
		||||
			<button class="w3-button w3-theme-d1" @click=${this.mark_all_read}>
 | 
			
		||||
				Mark All Read
 | 
			
		||||
			</button>
 | 
			
		||||
		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
 | 
			
		||||
				id="news"
 | 
			
		||||
				whoami=${this.whoami}
 | 
			
		||||
@@ -424,11 +562,14 @@ class TfTabNewsFeedElement extends LitElement {
 | 
			
		||||
				.following=${this.following}
 | 
			
		||||
				.drafts=${this.drafts}
 | 
			
		||||
				.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>
 | 
			
		||||
			${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 {styles} from './tf-styles.js';
 | 
			
		||||
import {styles, generate_theme} from './tf-styles.js';
 | 
			
		||||
 | 
			
		||||
class TfTabNewsElement extends LitElement {
 | 
			
		||||
	static get properties() {
 | 
			
		||||
@@ -8,7 +15,6 @@ class TfTabNewsElement extends LitElement {
 | 
			
		||||
			whoami: {type: String},
 | 
			
		||||
			users: {type: Object},
 | 
			
		||||
			hash: {type: String},
 | 
			
		||||
			unread: {type: Array},
 | 
			
		||||
			following: {type: Array},
 | 
			
		||||
			drafts: {type: Object},
 | 
			
		||||
			expanded: {type: Object},
 | 
			
		||||
@@ -16,6 +22,14 @@ class TfTabNewsElement extends LitElement {
 | 
			
		||||
			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},
 | 
			
		||||
		};
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
@@ -27,7 +41,6 @@ class TfTabNewsElement extends LitElement {
 | 
			
		||||
		this.whoami = null;
 | 
			
		||||
		this.users = {};
 | 
			
		||||
		this.hash = '#';
 | 
			
		||||
		this.unread = [];
 | 
			
		||||
		this.following = [];
 | 
			
		||||
		this.cache = {};
 | 
			
		||||
		this.drafts = {};
 | 
			
		||||
@@ -35,9 +48,12 @@ class TfTabNewsElement extends LitElement {
 | 
			
		||||
		this.channels_unread = {};
 | 
			
		||||
		this.channels_latest = {};
 | 
			
		||||
		this.channels = [];
 | 
			
		||||
		this.connections = [];
 | 
			
		||||
		this.recent_reactions = [];
 | 
			
		||||
		tfrpc.rpc.localStorageGet('drafts').then(function (d) {
 | 
			
		||||
			self.drafts = JSON.parse(d || '{}');
 | 
			
		||||
		});
 | 
			
		||||
		this.check_peer_exchange();
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	connectedCallback() {
 | 
			
		||||
@@ -50,34 +66,19 @@ class TfTabNewsElement extends LitElement {
 | 
			
		||||
		document.body.removeEventListener('keypress', this.on_keypress.bind(this));
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	show_more() {
 | 
			
		||||
		let unread = this.unread;
 | 
			
		||||
		let news = this.shadowRoot?.getElementById('news');
 | 
			
		||||
		if (news) {
 | 
			
		||||
			news.load_latest();
 | 
			
		||||
			this.dispatchEvent(new CustomEvent('refresh'));
 | 
			
		||||
	async check_peer_exchange() {
 | 
			
		||||
		if (await tfrpc.rpc.isAdministrator()) {
 | 
			
		||||
			this.peer_exchange = await tfrpc.rpc.globalSettingsGet('peer_exchange');
 | 
			
		||||
		} else {
 | 
			
		||||
			this.peer_exchange = undefined;
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	new_messages_text() {
 | 
			
		||||
		if (!this.unread?.length) {
 | 
			
		||||
			return 'No new messages.';
 | 
			
		||||
	load_latest() {
 | 
			
		||||
		let news = this.shadowRoot?.getElementById('news');
 | 
			
		||||
		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) {
 | 
			
		||||
@@ -110,12 +111,32 @@ class TfTabNewsElement extends LitElement {
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	unread_status(channel) {
 | 
			
		||||
		if (
 | 
			
		||||
		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 '🔵';
 | 
			
		||||
			return '✉️ ';
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
@@ -150,33 +171,39 @@ class TfTabNewsElement extends LitElement {
 | 
			
		||||
		return this.hash.startsWith('##') ? this.hash.substring(2) : undefined;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	render() {
 | 
			
		||||
		let profile =
 | 
			
		||||
			this.hash.startsWith('#@') && this.hash != '#@'
 | 
			
		||||
				? html`<tf-profile
 | 
			
		||||
						class="tf-profile"
 | 
			
		||||
						id=${this.hash.substring(1)}
 | 
			
		||||
						whoami=${this.whoami}
 | 
			
		||||
						.users=${this.users}
 | 
			
		||||
					></tf-profile>`
 | 
			
		||||
				: undefined;
 | 
			
		||||
		let edit_profile;
 | 
			
		||||
		if (
 | 
			
		||||
			!this.loading &&
 | 
			
		||||
			this.users[this.whoami]?.name === undefined &&
 | 
			
		||||
			this.hash.substring(1) != this.whoami
 | 
			
		||||
		) {
 | 
			
		||||
			edit_profile = html` <div
 | 
			
		||||
				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>`;
 | 
			
		||||
		}
 | 
			
		||||
	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"
 | 
			
		||||
				style="width: 2in; left: 0; z-index: 5; box-sizing: border-box; top: 0"
 | 
			
		||||
				id="sidebar"
 | 
			
		||||
			>
 | 
			
		||||
				<div
 | 
			
		||||
@@ -193,29 +220,52 @@ class TfTabNewsElement extends LitElement {
 | 
			
		||||
								href="#"
 | 
			
		||||
								class="w3-bar-item w3-button"
 | 
			
		||||
								style="font-weight: bold"
 | 
			
		||||
								>${this.hash.substring(2)}</a
 | 
			
		||||
								>${this.hash.substring(1)}</a
 | 
			
		||||
							>
 | 
			
		||||
						`
 | 
			
		||||
					: undefined}
 | 
			
		||||
				<div class="w3-bar-item w3-theme-d2">Channels</div>
 | 
			
		||||
				<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}
 | 
			
		||||
					>general ${this.unread_status('')}</a
 | 
			
		||||
					>${this.unread_status('')}general</a
 | 
			
		||||
				>
 | 
			
		||||
				<a
 | 
			
		||||
					href="#@"
 | 
			
		||||
					class="w3-bar-item w3-button"
 | 
			
		||||
					style=${this.hash == '#@' ? 'font-weight: bold' : undefined}
 | 
			
		||||
					>@mentions ${this.unread_status('@')}</a
 | 
			
		||||
					>${this.unread_status('@')}@mentions</a
 | 
			
		||||
				>
 | 
			
		||||
				<a
 | 
			
		||||
					href="#🔐"
 | 
			
		||||
					href="#👍"
 | 
			
		||||
					class="w3-bar-item w3-button"
 | 
			
		||||
					style=${this.hash == '#🔐' ? 'font-weight: bold' : undefined}
 | 
			
		||||
					>🔐private ${this.unread_status('🔐')}</a
 | 
			
		||||
					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(
 | 
			
		||||
@@ -234,71 +284,186 @@ class TfTabNewsElement extends LitElement {
 | 
			
		||||
							href=${'#' + encodeURIComponent('#' + x)}
 | 
			
		||||
							class="w3-bar-item w3-button"
 | 
			
		||||
							style=${this.hash == '##' + x ? 'font-weight: bold' : undefined}
 | 
			
		||||
							>#${x} ${this.unread_status(x)}</a
 | 
			
		||||
							>${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>
 | 
			
		||||
			<div style="margin-left: 2in; padding: 8px" id="main" class="w3-main">
 | 
			
		||||
				<div
 | 
			
		||||
					id="show_sidebar"
 | 
			
		||||
					class="w3-left w3-button w3-hide-large"
 | 
			
		||||
					@click=${this.show_sidebar}
 | 
			
		||||
				>
 | 
			
		||||
					☰
 | 
			
		||||
				</div>
 | 
			
		||||
				<p>
 | 
			
		||||
					<button class="w3-button w3-theme-d1" @click=${this.show_more}>
 | 
			
		||||
						${this.new_messages_text()}
 | 
			
		||||
					</button>
 | 
			
		||||
					${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>
 | 
			
		||||
							`
 | 
			
		||||
						: undefined}
 | 
			
		||||
				</p>
 | 
			
		||||
				<div class="w3-bar">
 | 
			
		||||
					Welcome, <tf-user id=${this.whoami} .users=${this.users}></tf-user>!
 | 
			
		||||
					${edit_profile}
 | 
			
		||||
				</div>
 | 
			
		||||
				<div>
 | 
			
		||||
					<tf-compose
 | 
			
		||||
						id="tf-compose"
 | 
			
		||||
		`;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	render() {
 | 
			
		||||
		let profile =
 | 
			
		||||
			this.hash.startsWith('#@') && this.hash != '#@'
 | 
			
		||||
				? keyed(
 | 
			
		||||
						this.hash.substring(1),
 | 
			
		||||
						html`<tf-profile
 | 
			
		||||
							class="tf-profile"
 | 
			
		||||
							id=${this.hash.substring(1)}
 | 
			
		||||
							whoami=${this.whoami}
 | 
			
		||||
							.users=${this.users}
 | 
			
		||||
						></tf-profile>`
 | 
			
		||||
					)
 | 
			
		||||
				: undefined;
 | 
			
		||||
		let edit_profile;
 | 
			
		||||
		if (
 | 
			
		||||
			!this.loading &&
 | 
			
		||||
			this.users[this.whoami]?.name === undefined &&
 | 
			
		||||
			this.hash.substring(1) != this.whoami
 | 
			
		||||
		) {
 | 
			
		||||
			edit_profile = html` <div
 | 
			
		||||
				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>`;
 | 
			
		||||
		}
 | 
			
		||||
		return cache(html`
 | 
			
		||||
			<style>
 | 
			
		||||
				${generate_theme()}
 | 
			
		||||
			</style>
 | 
			
		||||
			${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"
 | 
			
		||||
			>
 | 
			
		||||
				<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>
 | 
			
		||||
								`
 | 
			
		||||
							: undefined}
 | 
			
		||||
					</p>
 | 
			
		||||
					<div>
 | 
			
		||||
						<div
 | 
			
		||||
							id="show_sidebar"
 | 
			
		||||
							class="w3-button w3-hide-large"
 | 
			
		||||
							@click=${this.show_sidebar}
 | 
			
		||||
						>
 | 
			
		||||
							${this.unread_status()}☰
 | 
			
		||||
						</div>
 | 
			
		||||
						Welcome, <tf-user id=${this.whoami} .users=${this.users}></tf-user>!
 | 
			
		||||
						${edit_profile}
 | 
			
		||||
					</div>
 | 
			
		||||
					<div>
 | 
			
		||||
						<tf-compose
 | 
			
		||||
							id="tf-compose"
 | 
			
		||||
							whoami=${this.whoami}
 | 
			
		||||
							.users=${this.users}
 | 
			
		||||
							.drafts=${this.drafts}
 | 
			
		||||
							@tf-draft=${this.draft}
 | 
			
		||||
							.channel=${this.channel()}
 | 
			
		||||
							.recipients=${this.hash.startsWith('#🔐')
 | 
			
		||||
								? this.hash.substring('#🔐'.length).split(',')
 | 
			
		||||
								: undefined}
 | 
			
		||||
						></tf-compose>
 | 
			
		||||
					</div>
 | 
			
		||||
					${profile}
 | 
			
		||||
					<tf-tab-news-feed
 | 
			
		||||
						id="news"
 | 
			
		||||
						whoami=${this.whoami}
 | 
			
		||||
						.users=${this.users}
 | 
			
		||||
						.following=${this.following}
 | 
			
		||||
						hash=${this.hash}
 | 
			
		||||
						.drafts=${this.drafts}
 | 
			
		||||
						.expanded=${this.expanded}
 | 
			
		||||
						@tf-draft=${this.draft}
 | 
			
		||||
						.channel=${this.channel()}
 | 
			
		||||
					></tf-compose>
 | 
			
		||||
						@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>
 | 
			
		||||
				</div>
 | 
			
		||||
				${profile}
 | 
			
		||||
				<tf-tab-news-feed
 | 
			
		||||
					id="news"
 | 
			
		||||
					whoami=${this.whoami}
 | 
			
		||||
					.users=${this.users}
 | 
			
		||||
					.following=${this.following}
 | 
			
		||||
					hash=${this.hash}
 | 
			
		||||
					.drafts=${this.drafts}
 | 
			
		||||
					.expanded=${this.expanded}
 | 
			
		||||
					@tf-draft=${this.draft}
 | 
			
		||||
					@tf-expand=${this.on_expand}
 | 
			
		||||
					.channels_unread=${this.channels_unread}
 | 
			
		||||
					.channels_latest=${this.channels_latest}
 | 
			
		||||
				></tf-tab-news-feed>
 | 
			
		||||
			</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,6 +1,6 @@
 | 
			
		||||
import {LitElement, html, unsafeHTML} from './lit-all.min.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 {
 | 
			
		||||
	static get properties() {
 | 
			
		||||
@@ -11,6 +11,9 @@ class TfTabSearchElement extends LitElement {
 | 
			
		||||
			following: {type: Array},
 | 
			
		||||
			query: {type: String},
 | 
			
		||||
			expanded: {type: Object},
 | 
			
		||||
			messages: {type: Array},
 | 
			
		||||
			results: {type: Array},
 | 
			
		||||
			error: {type: Object},
 | 
			
		||||
		};
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
@@ -38,24 +41,40 @@ class TfTabSearchElement extends LitElement {
 | 
			
		||||
			search.select();
 | 
			
		||||
		}
 | 
			
		||||
		await tfrpc.rpc.setHash('#q=' + encodeURIComponent(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
 | 
			
		||||
				FROM messages_fts(?)
 | 
			
		||||
				JOIN messages ON messages.rowid = messages_fts.rowid
 | 
			
		||||
				JOIN json_each(?) AS following ON messages.author = following.value
 | 
			
		||||
				ORDER BY timestamp DESC limit 100
 | 
			
		||||
			`,
 | 
			
		||||
			['"' + query.replace('"', '""') + '"', JSON.stringify(this.following)]
 | 
			
		||||
		);
 | 
			
		||||
		console.log('Done.');
 | 
			
		||||
		search = this.renderRoot.getElementById('search');
 | 
			
		||||
		if (search) {
 | 
			
		||||
			search.value = query;
 | 
			
		||||
			search.focus();
 | 
			
		||||
			search.select();
 | 
			
		||||
		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(
 | 
			
		||||
				`
 | 
			
		||||
					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
 | 
			
		||||
					ORDER BY timestamp DESC limit 100
 | 
			
		||||
				`,
 | 
			
		||||
				['"' + query.replace('"', '""') + '"', JSON.stringify(this.following)]
 | 
			
		||||
			);
 | 
			
		||||
			console.log('Done.');
 | 
			
		||||
			search = this.renderRoot.getElementById('search');
 | 
			
		||||
			if (search) {
 | 
			
		||||
				search.value = query;
 | 
			
		||||
				search.focus();
 | 
			
		||||
				search.select();
 | 
			
		||||
			}
 | 
			
		||||
			this.messages = results;
 | 
			
		||||
		}
 | 
			
		||||
		this.renderRoot.getElementById('news').messages = results;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	search_keydown(event) {
 | 
			
		||||
@@ -87,6 +106,39 @@ class TfTabSearchElement extends LitElement {
 | 
			
		||||
		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() {
 | 
			
		||||
		if (this.query !== this.last_query) {
 | 
			
		||||
			this.last_query = this.query;
 | 
			
		||||
@@ -94,11 +146,14 @@ class TfTabSearchElement extends LitElement {
 | 
			
		||||
		}
 | 
			
		||||
		let self = this;
 | 
			
		||||
		return html`
 | 
			
		||||
			<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>
 | 
			
		||||
				<button class="w3-button w3-theme-d1" @click=${(event) => self.search(self.renderRoot.getElementById('search').value)}>Search</button>
 | 
			
		||||
			<style>${generate_theme()}</style>
 | 
			
		||||
			<div class="w3-padding">
 | 
			
		||||
				<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>
 | 
			
		||||
					<button class="w3-button w3-theme-d1" @click=${(event) => self.search(self.renderRoot.getElementById('search').value)}>Search</button>
 | 
			
		||||
				</div>
 | 
			
		||||
				${this.render_results()}
 | 
			
		||||
			</div>
 | 
			
		||||
			<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>
 | 
			
		||||
		`;
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,5 @@
 | 
			
		||||
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 {
 | 
			
		||||
	static get properties() {
 | 
			
		||||
@@ -17,11 +17,15 @@ class TfTagElement extends LitElement {
 | 
			
		||||
 | 
			
		||||
	render() {
 | 
			
		||||
		let number = this.count ? html` (${this.count})` : undefined;
 | 
			
		||||
		return html`<a
 | 
			
		||||
			href=${'#' + encodeURIComponent(this.tag)}
 | 
			
		||||
			style="display: inline-block; margin: 3px; border: 1px solid black; background-color: #444; padding: 4px; border-radius: 3px"
 | 
			
		||||
			>${this.tag}${number}</a
 | 
			
		||||
		>`;
 | 
			
		||||
		return html`
 | 
			
		||||
			<style>
 | 
			
		||||
				${generate_theme()}</style
 | 
			
		||||
			><a
 | 
			
		||||
				href=${'#' + encodeURIComponent(this.tag)}
 | 
			
		||||
				class="w3-tag w3-theme-d1 w3-round-4 w3-button"
 | 
			
		||||
				>${this.tag}${number}</a
 | 
			
		||||
			>
 | 
			
		||||
		`;
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,12 +1,15 @@
 | 
			
		||||
import {LitElement, html} from './lit-all.min.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 {
 | 
			
		||||
	static get properties() {
 | 
			
		||||
		return {
 | 
			
		||||
			id: {type: String},
 | 
			
		||||
			fallback_name: {type: String},
 | 
			
		||||
			icon_only: {type: Boolean},
 | 
			
		||||
			users: {type: Object},
 | 
			
		||||
			nolink: {type: Boolean},
 | 
			
		||||
		};
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
@@ -15,36 +18,55 @@ class TfUserElement extends LitElement {
 | 
			
		||||
	constructor() {
 | 
			
		||||
		super();
 | 
			
		||||
		this.id = null;
 | 
			
		||||
		this.fallback_name = null;
 | 
			
		||||
		this.icon_only = false;
 | 
			
		||||
		this.users = {};
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	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-light w3-circle"
 | 
			
		||||
			class=${'w3-theme-l4 ' + shape}
 | 
			
		||||
			style="display: inline-block; width: 2em; height: 2em; text-align: center; line-height: 2em"
 | 
			
		||||
			>?</span
 | 
			
		||||
			>😎</span
 | 
			
		||||
		>`;
 | 
			
		||||
		let name = this.users?.[this.id]?.name;
 | 
			
		||||
		name =
 | 
			
		||||
			name !== undefined
 | 
			
		||||
				? html`<a target="_top" href=${'#' + this.id}>${name}</a>`
 | 
			
		||||
				: html`<a target="_top" href=${'#' + this.id}>${this.id}</a>`;
 | 
			
		||||
		let name_string = name ?? this.fallback_name ?? this.id;
 | 
			
		||||
		name = this.icon_only
 | 
			
		||||
			? undefined
 | 
			
		||||
			: !this.nolink
 | 
			
		||||
				? html`<a target="_top" href=${'#' + this.id}>${name_string}</a>`
 | 
			
		||||
				: html`<span>${name_string}</span>`;
 | 
			
		||||
 | 
			
		||||
		if (this.users[this.id]) {
 | 
			
		||||
			let image_link = this.users[this.id].image;
 | 
			
		||||
			image_link =
 | 
			
		||||
				typeof image_link == 'string' ? image_link : image_link?.link;
 | 
			
		||||
		if (user) {
 | 
			
		||||
			let image_link = user.image;
 | 
			
		||||
			if (typeof image_link == 'string' && !image_link.startsWith('&')) {
 | 
			
		||||
				try {
 | 
			
		||||
					image_link = JSON.parse(image_link)?.link;
 | 
			
		||||
				} catch {}
 | 
			
		||||
			}
 | 
			
		||||
			if (image_link !== undefined) {
 | 
			
		||||
				image = html`<img
 | 
			
		||||
					class="w3-circle"
 | 
			
		||||
					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` <div style="display: inline-block; font-weight: bold">
 | 
			
		||||
			${image} ${name}
 | 
			
		||||
		</div>`;
 | 
			
		||||
		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>`;
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -50,9 +50,9 @@ function image(node, entering) {
 | 
			
		||||
						'</div>'
 | 
			
		||||
				);
 | 
			
		||||
				if (this.options.safe && potentiallyUnsafe(node.destination)) {
 | 
			
		||||
					this.lit('<img src="" alt="');
 | 
			
		||||
					this.lit('<img src="" title="');
 | 
			
		||||
				} else {
 | 
			
		||||
					this.lit('<img src="' + this.esc(node.destination) + '" alt="');
 | 
			
		||||
					this.lit('<img src="' + this.esc(node.destination) + '" title="');
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
			this.disableTags += 1;
 | 
			
		||||
@@ -104,12 +104,12 @@ export function markdown(md) {
 | 
			
		||||
					node.destination.startsWith('@') &&
 | 
			
		||||
					node.destination.endsWith('.ed25519')
 | 
			
		||||
				) {
 | 
			
		||||
					node.destination = '#' + node.destination;
 | 
			
		||||
					node.destination = '#' + encodeURIComponent(node.destination);
 | 
			
		||||
				} else if (
 | 
			
		||||
					node.destination.startsWith('%') &&
 | 
			
		||||
					node.destination.endsWith('.sha256')
 | 
			
		||||
				) {
 | 
			
		||||
					node.destination = '#' + node.destination;
 | 
			
		||||
					node.destination = '#' + encodeURIComponent(node.destination);
 | 
			
		||||
				} else if (
 | 
			
		||||
					node.destination.startsWith('&') &&
 | 
			
		||||
					node.destination.endsWith('.sha256')
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,5 @@
 | 
			
		||||
{
 | 
			
		||||
	"type": "tildefriends-app",
 | 
			
		||||
	"emoji": "💾",
 | 
			
		||||
	"previous": "&mvGTlWKFR5QM/3nb4fJ2WQq0n/gNKvBmhGDkAvb8ki8=.sha256"
 | 
			
		||||
	"previous": "&tzZFIe7Y54O4sx1QtAPdemkXh+p5qHXSG/dlS7NP6OQ=.sha256"
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -8,7 +8,7 @@ async function query(sql, args) {
 | 
			
		||||
 | 
			
		||||
async function get_biggest() {
 | 
			
		||||
	return query(`
 | 
			
		||||
		select author, sum(length(content)) as size from messages group by author order by size desc limit 10;
 | 
			
		||||
		select author, size from messages_stats group by author order by size desc limit 10;
 | 
			
		||||
	`);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -62,15 +62,14 @@ function nice_size(bytes) {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function main() {
 | 
			
		||||
	await app.setDocument(
 | 
			
		||||
		'<p style="color: #fff">Finding the top 10 largest feeds...</p>'
 | 
			
		||||
	);
 | 
			
		||||
	let most_follows = await get_most_follows();
 | 
			
		||||
	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),
 | 
			
		||||
@@ -94,7 +93,7 @@ async function main() {
 | 
			
		||||
	}
 | 
			
		||||
	let html = `<body style="color: #000; background-color: #ddd">\n
 | 
			
		||||
		<h1>Storage Summary</h1>
 | 
			
		||||
		<h2>Top 10 Accounts by Size</h2>
 | 
			
		||||
		<h2>Top Accounts by Size</h2>
 | 
			
		||||
		<ol>`;
 | 
			
		||||
	for (let item of biggest) {
 | 
			
		||||
		html += `<li>
 | 
			
		||||
@@ -105,7 +104,7 @@ async function main() {
 | 
			
		||||
	}
 | 
			
		||||
	html += `
 | 
			
		||||
		</ol>
 | 
			
		||||
		<h2>Top 10 Accounts by Follows</h2>
 | 
			
		||||
		<h2>Top Accounts by Follows</h2>
 | 
			
		||||
		<ol>`;
 | 
			
		||||
	for (let item of most_follows) {
 | 
			
		||||
		html += `<li>
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										5
									
								
								apps/web.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,5 @@
 | 
			
		||||
{
 | 
			
		||||
	"type": "tildefriends-app",
 | 
			
		||||
	"emoji": "🕸",
 | 
			
		||||
	"previous": "&n7hu5b8/TsfiG6FDlCRG5nPCrIdCr96+xpIJ/aQT/uM=.sha256"
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										100
									
								
								apps/web/app.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,100 @@
 | 
			
		||||
let g_hash;
 | 
			
		||||
 | 
			
		||||
async function query(sql, params) {
 | 
			
		||||
	let results = [];
 | 
			
		||||
	await ssb.sqlAsync(sql, params, function (row) {
 | 
			
		||||
		results.push(row);
 | 
			
		||||
	});
 | 
			
		||||
	return results;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function resolve(id) {
 | 
			
		||||
	try {
 | 
			
		||||
		let blob = await ssb.blobGet(id);
 | 
			
		||||
		if (blob) {
 | 
			
		||||
			let json;
 | 
			
		||||
			try {
 | 
			
		||||
				json = JSON.parse(utf8Decode(blob));
 | 
			
		||||
			} catch {
 | 
			
		||||
				return {id: utf8Decode(blob)};
 | 
			
		||||
			}
 | 
			
		||||
			if (json?.links) {
 | 
			
		||||
				for (let [key, value] of Object.entries(json.links)) {
 | 
			
		||||
					json.links[key] = await resolve(value);
 | 
			
		||||
				}
 | 
			
		||||
				return json;
 | 
			
		||||
			} else {
 | 
			
		||||
				return 'huh?' + json;
 | 
			
		||||
			}
 | 
			
		||||
		} else {
 | 
			
		||||
			return `missing<${id}>`;
 | 
			
		||||
		}
 | 
			
		||||
	} catch (e) {
 | 
			
		||||
		return id + ': ' + e.message;
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function get_names(identities) {
 | 
			
		||||
	return Object.fromEntries(
 | 
			
		||||
		(
 | 
			
		||||
			await 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)]
 | 
			
		||||
			)
 | 
			
		||||
		).map((x) => [x.author, x.name])
 | 
			
		||||
	);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function render(hash) {
 | 
			
		||||
	g_hash = hash;
 | 
			
		||||
	if (!hash) {
 | 
			
		||||
		let sites = await query(
 | 
			
		||||
			`
 | 
			
		||||
			SELECT site.author, site.id
 | 
			
		||||
			FROM messages site
 | 
			
		||||
			WHERE site.content ->> 'type' = 'web-init'
 | 
			
		||||
		`,
 | 
			
		||||
			[]
 | 
			
		||||
		);
 | 
			
		||||
		let names = await get_names(sites.map((x) => x.author));
 | 
			
		||||
		if (hash === g_hash) {
 | 
			
		||||
			await app.setDocument(
 | 
			
		||||
				`<ul style="background-color: #ddd">${sites.map((x) => `<li><a target="_top" href="#${encodeURIComponent(x.id)}">${names[x.author] ?? x.author} - ${x.id}</a></li>`).join('\n')}</ul>`
 | 
			
		||||
			);
 | 
			
		||||
		}
 | 
			
		||||
	} else {
 | 
			
		||||
		let site_id =
 | 
			
		||||
			hash.charAt(0) == '#'
 | 
			
		||||
				? decodeURIComponent(hash.substring(1))
 | 
			
		||||
				: decodeURIComponent(hash);
 | 
			
		||||
		await app.setDocument(`<html style="margin: 0; padding: 0; width: 100vw; height: 100vh; margin: 0; padding: 0">
 | 
			
		||||
			<body style="display: flex; flex-direction: column; width: 100vw; height: 100vh">
 | 
			
		||||
				<iframe src="${encodeURIComponent(site_id)}/index.html" style="flex: 1 1; border: 0; background-color: #fff"></iframe>
 | 
			
		||||
			</body>
 | 
			
		||||
		</html>`);
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
core.register('message', async function message_handler(message) {
 | 
			
		||||
	if (message.event == 'hashChange') {
 | 
			
		||||
		await render(message.hash);
 | 
			
		||||
	}
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
async function main() {
 | 
			
		||||
	render(null);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
main();
 | 
			
		||||
							
								
								
									
										63
									
								
								apps/web/handler.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,63 @@
 | 
			
		||||
async function query(sql, params) {
 | 
			
		||||
	let results = [];
 | 
			
		||||
	await ssb.sqlAsync(sql, params, function (row) {
 | 
			
		||||
		results.push(row);
 | 
			
		||||
	});
 | 
			
		||||
	return results;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function guess_content_type(name) {
 | 
			
		||||
	if (name.endsWith('.html')) {
 | 
			
		||||
		return 'text/html; charset=UTF-8';
 | 
			
		||||
	} else if (name.endsWith('.js') || name.endsWith('.mjs')) {
 | 
			
		||||
		return 'text/javascript; charset=UTF-8';
 | 
			
		||||
	} else if (name.endsWith('.css')) {
 | 
			
		||||
		return 'text/stylesheet; charset=UTF-8';
 | 
			
		||||
	} else {
 | 
			
		||||
		return 'application/binary';
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function main() {
 | 
			
		||||
	let path = request.path.replaceAll(/(%[0-9a-fA-F]{2})/g, (x) =>
 | 
			
		||||
		String.fromCharCode(parseInt(x.substring(1), 16))
 | 
			
		||||
	);
 | 
			
		||||
	let match = path.match(/^(%.{44}\.sha256)(?:\/)?(.*)$/);
 | 
			
		||||
 | 
			
		||||
	let content_type = guess_content_type(request.path);
 | 
			
		||||
	let root = await query(
 | 
			
		||||
		`
 | 
			
		||||
		SELECT root.content ->> 'root' AS root
 | 
			
		||||
		FROM messages site
 | 
			
		||||
		JOIN messages root
 | 
			
		||||
		ON site.id = ? AND root.author = site.author AND root.content ->> 'site' = site.id
 | 
			
		||||
		ORDER BY root.sequence DESC LIMIT 1
 | 
			
		||||
	`,
 | 
			
		||||
		[match[1]]
 | 
			
		||||
	);
 | 
			
		||||
	let root_id = root[0]['root'];
 | 
			
		||||
	let last_id = root_id;
 | 
			
		||||
	let blob = await ssb.blobGet(root_id);
 | 
			
		||||
	try {
 | 
			
		||||
		for (let part of match[2]?.split('/')) {
 | 
			
		||||
			let dir = JSON.parse(utf8Decode(blob));
 | 
			
		||||
			last_id = dir?.links[part];
 | 
			
		||||
			blob = await ssb.blobGet(dir?.links[part]);
 | 
			
		||||
			content_type = guess_content_type(part);
 | 
			
		||||
		}
 | 
			
		||||
	} catch {}
 | 
			
		||||
 | 
			
		||||
	respond({
 | 
			
		||||
		status_code: 200,
 | 
			
		||||
		data: blob ? utf8Decode(blob) : `${last_id} not found`,
 | 
			
		||||
		content_type: content_type,
 | 
			
		||||
	});
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
main().catch(function (e) {
 | 
			
		||||
	respond({
 | 
			
		||||
		status_code: 200,
 | 
			
		||||
		data: `${e.message}\n${e.stack}`,
 | 
			
		||||
		content_type: 'text/plain',
 | 
			
		||||
	});
 | 
			
		||||
});
 | 
			
		||||
@@ -1,5 +1,5 @@
 | 
			
		||||
{
 | 
			
		||||
	"type": "tildefriends-app",
 | 
			
		||||
	"emoji": "👋",
 | 
			
		||||
	"previous": "&7gFmLW5zSMhmxWWY1+jeRcHdullgujSqGJg94lVgr1k=.sha256"
 | 
			
		||||
	"previous": "&n1QkPkB5JoduFSx8UKOY3IlZqS2GwLiTUZv4ZrEOthQ=.sha256"
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +0,0 @@
 | 
			
		||||
async function main() {
 | 
			
		||||
	await app.setDocument(utf8Decode(getFile('index.html')));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
main();
 | 
			
		||||
							
								
								
									
										1
									
								
								apps/welcome/gitea.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1 @@
 | 
			
		||||
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" viewBox="0 0 640 640" width="32" height="32"><path d="m395.9 484.2-126.9-61c-12.5-6-17.9-21.2-11.8-33.8l61-126.9c6-12.5 21.2-17.9 33.8-11.8 17.2 8.3 27.1 13 27.1 13l-.1-109.2 16.7-.1.1 117.1s57.4 24.2 83.1 40.1c3.7 2.3 10.2 6.8 12.9 14.4 2.1 6.1 2 13.1-1 19.3l-61 126.9c-6.2 12.7-21.4 18.1-33.9 12" style="fill:#fff"/><path d="M622.7 149.8c-4.1-4.1-9.6-4-9.6-4s-117.2 6.6-177.9 8c-13.3.3-26.5.6-39.6.7v117.2c-5.5-2.6-11.1-5.3-16.6-7.9 0-36.4-.1-109.2-.1-109.2-29 .4-89.2-2.2-89.2-2.2s-141.4-7.1-156.8-8.5c-9.8-.6-22.5-2.1-39 1.5-8.7 1.8-33.5 7.4-53.8 26.9C-4.9 212.4 6.6 276.2 8 285.8c1.7 11.7 6.9 44.2 31.7 72.5 45.8 56.1 144.4 54.8 144.4 54.8s12.1 28.9 30.6 55.5c25 33.1 50.7 58.9 75.7 62 63 0 188.9-.1 188.9-.1s12 .1 28.3-10.3c14-8.5 26.5-23.4 26.5-23.4S547 483 565 451.5c5.5-9.7 10.1-19.1 14.1-28 0 0 55.2-117.1 55.2-231.1-1.1-34.5-9.6-40.6-11.6-42.6M125.6 353.9c-25.9-8.5-36.9-18.7-36.9-18.7S69.6 321.8 60 295.4c-16.5-44.2-1.4-71.2-1.4-71.2s8.4-22.5 38.5-30c13.8-3.7 31-3.1 31-3.1s7.1 59.4 15.7 94.2c7.2 29.2 24.8 77.7 24.8 77.7s-26.1-3.1-43-9.1m300.3 107.6s-6.1 14.5-19.6 15.4c-5.8.4-10.3-1.2-10.3-1.2s-.3-.1-5.3-2.1l-112.9-55s-10.9-5.7-12.8-15.6c-2.2-8.1 2.7-18.1 2.7-18.1L322 273s4.8-9.7 12.2-13c.6-.3 2.3-1 4.5-1.5 8.1-2.1 18 2.8 18 2.8L467.4 315s12.6 5.7 15.3 16.2c1.9 7.4-.5 14-1.8 17.2-6.3 15.4-55 113.1-55 113.1" style="fill:#609926"/><path d="M326.8 380.1c-8.2.1-15.4 5.8-17.3 13.8s2 16.3 9.1 20c7.7 4 17.5 1.8 22.7-5.4 5.1-7.1 4.3-16.9-1.8-23.1l24-49.1c1.5.1 3.7.2 6.2-.5 4.1-.9 7.1-3.6 7.1-3.6 4.2 1.8 8.6 3.8 13.2 6.1 4.8 2.4 9.3 4.9 13.4 7.3.9.5 1.8 1.1 2.8 1.9 1.6 1.3 3.4 3.1 4.7 5.5 1.9 5.5-1.9 14.9-1.9 14.9-2.3 7.6-18.4 40.6-18.4 40.6-8.1-.2-15.3 5-17.7 12.5-2.6 8.1 1.1 17.3 8.9 21.3s17.4 1.7 22.5-5.3c5-6.8 4.6-16.3-1.1-22.6 1.9-3.7 3.7-7.4 5.6-11.3 5-10.4 13.5-30.4 13.5-30.4.9-1.7 5.7-10.3 2.7-21.3-2.5-11.4-12.6-16.7-12.6-16.7-12.2-7.9-29.2-15.2-29.2-15.2s0-4.1-1.1-7.1c-1.1-3.1-2.8-5.1-3.9-6.3 4.7-9.7 9.4-19.3 14.1-29-4.1-2-8.1-4-12.2-6.1-4.8 9.8-9.7 19.7-14.5 29.5-6.7-.1-12.9 3.5-16.1 9.4-3.4 6.3-2.7 14.1 1.9 19.8z" style="fill:#609926"/></svg>
 | 
			
		||||
| 
		 After Width: | Height: | Size: 2.1 KiB  | 
							
								
								
									
										1521
									
								
								apps/welcome/hermietildefriends.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 86 KiB  | 
@@ -10,17 +10,6 @@
 | 
			
		||||
		<link rel="stylesheet" href="brands.min.css" />
 | 
			
		||||
 | 
			
		||||
		<style>
 | 
			
		||||
			body,
 | 
			
		||||
			h1,
 | 
			
		||||
			h2,
 | 
			
		||||
			h3,
 | 
			
		||||
			h4,
 | 
			
		||||
			h5 {
 | 
			
		||||
				font-family: 'Poppins', sans-serif;
 | 
			
		||||
			}
 | 
			
		||||
			body {
 | 
			
		||||
				font-size: 16px;
 | 
			
		||||
			}
 | 
			
		||||
			img {
 | 
			
		||||
				margin-bottom: -8px;
 | 
			
		||||
			}
 | 
			
		||||
@@ -39,57 +28,39 @@
 | 
			
		||||
						<b>😎 Tilde Friends</b>
 | 
			
		||||
					</h1>
 | 
			
		||||
					<h1 class="w3-xxlarge w3-text-green">
 | 
			
		||||
						<b>Make apps and friends from the comfort of your web browser.</b>
 | 
			
		||||
						<b>a Secure Scuttlebutt decentralized social network client</b>
 | 
			
		||||
					</h1>
 | 
			
		||||
					<p>
 | 
			
		||||
						Tilde Friends is a platform for building, running, and sharing web
 | 
			
		||||
						applications.
 | 
			
		||||
					</p>
 | 
			
		||||
					<p>
 | 
			
		||||
						Available for lots of devices:
 | 
			
		||||
						<i class="fa-brands fa-linux w3-xlarge"></i>
 | 
			
		||||
						<i class="fa-brands fa-android w3-xlarge"></i>
 | 
			
		||||
						<i class="fa-brands fa-apple w3-xlarge"></i>
 | 
			
		||||
						<i class="fa fa-mobile-screen w3-xlarge"></i>
 | 
			
		||||
						<i class="fa-brands fa-windows w3-xlarge"></i>
 | 
			
		||||
						In addition to participating in Secure Scuttlebutt, Tilde Friends is
 | 
			
		||||
						a platform for building, running, and sharing applications.
 | 
			
		||||
					</p>
 | 
			
		||||
					<a
 | 
			
		||||
						class="w3-button w3-blue w3-padding-large"
 | 
			
		||||
						href="https://www.tildefriends.net/~core/ssb/"
 | 
			
		||||
						>🦀 Try It</a
 | 
			
		||||
					>
 | 
			
		||||
					<a
 | 
			
		||||
						class="w3-button w3-black w3-padding-large"
 | 
			
		||||
						href="https://dev.tildefriends.net/cory/tildefriends/releases"
 | 
			
		||||
						href="https://dev.tildefriends.net/cory/tildefriends/releases/latest"
 | 
			
		||||
						><i class="fa fa-download"></i> Download</a
 | 
			
		||||
					>
 | 
			
		||||
					<a
 | 
			
		||||
						class="w3-button w3-black w3-padding-large"
 | 
			
		||||
						href="https://www.tildefriends.net/~cory/apps/"
 | 
			
		||||
						><i class="fa fa-link"></i> Try It</a
 | 
			
		||||
						href="https://dev.tildefriends.net/cory/tildefriends"
 | 
			
		||||
					>
 | 
			
		||||
						<img src="gitea.svg" style="height: 1em; margin: 0" />
 | 
			
		||||
						Development
 | 
			
		||||
					</a>
 | 
			
		||||
					<a
 | 
			
		||||
						class="w3-button w3-black w3-padding-large"
 | 
			
		||||
						href="https://docs.tildefriends.net/"
 | 
			
		||||
						><i class="fa fa-book"></i> Documentation</a
 | 
			
		||||
					>
 | 
			
		||||
					<a
 | 
			
		||||
						class="w3-button w3-black w3-padding-large"
 | 
			
		||||
						href="https://dev.tildefriends.net/"
 | 
			
		||||
						><i class="fa fa-mug-hot"></i> Development</a
 | 
			
		||||
						href="https://www.tildefriends.net/~cory/tildeblog/"
 | 
			
		||||
						><i class="fa fa-solid fa-square-rss"></i> Blog</a
 | 
			
		||||
					>
 | 
			
		||||
					<p>
 | 
			
		||||
						<a
 | 
			
		||||
							class="w3-button w3-round-large w3-padding w3-blue-gray w3-margin-top"
 | 
			
		||||
							href="https://f-droid.org/en/packages/com.unprompted.tildefriends.fdroid/"
 | 
			
		||||
							><img src="f-droid.svg" style="height: 2em; margin: 0" /> Get it
 | 
			
		||||
							on F-Droid</a
 | 
			
		||||
						>
 | 
			
		||||
						<a
 | 
			
		||||
							class="w3-button w3-round-large w3-padding w3-blue-gray w3-margin-top"
 | 
			
		||||
							href="https://dev.tildefriends.net/releases/tildefriends-x86_64.AppImage"
 | 
			
		||||
						>
 | 
			
		||||
							<img src="appimage.svg" style="height: 2em; margin: 0" />
 | 
			
		||||
							Get Linux 64-bit AppImage
 | 
			
		||||
						</a>
 | 
			
		||||
						<a
 | 
			
		||||
							class="w3-button w3-round-large w3-padding w3-blue-gray w3-margin-top"
 | 
			
		||||
							href="https://play.google.com/store/apps/details?id=com.unprompted.tildefriends"
 | 
			
		||||
						>
 | 
			
		||||
							<img src="googleplay.svg" style="height: 2em; margin: 0" />
 | 
			
		||||
							Get it on Google Play (Open Testing)
 | 
			
		||||
						</a>
 | 
			
		||||
					</p>
 | 
			
		||||
				</div>
 | 
			
		||||
				<div class="w3-col l4 m6">
 | 
			
		||||
					<img src="tildefriends.png" class="w3-image w3-right w3-hide-small" />
 | 
			
		||||
@@ -107,15 +78,119 @@
 | 
			
		||||
					<h2>First-time user checklist:</h2>
 | 
			
		||||
					<ol type="1" style="text-align: left">
 | 
			
		||||
						<li>
 | 
			
		||||
							<a href="https://dev.tildefriends.net/cory/tildefriends/releases"
 | 
			
		||||
							<a
 | 
			
		||||
								href="https://dev.tildefriends.net/cory/tildefriends/releases/latest"
 | 
			
		||||
								>Download</a
 | 
			
		||||
							>
 | 
			
		||||
							Tilde Friends or use
 | 
			
		||||
							<a href="https://www.tildefriends.net/"
 | 
			
		||||
								>https://www.tildefriends.net/</a
 | 
			
		||||
							>.
 | 
			
		||||
							<div class="w3-cell-row">
 | 
			
		||||
								<div class="w3-container w3-cell w3-mobile">
 | 
			
		||||
									<h3>Mobile</h3>
 | 
			
		||||
									<p>
 | 
			
		||||
										<a
 | 
			
		||||
											class="w3-button w3-round-large w3-padding w3-blue-gray w3-margin-top"
 | 
			
		||||
											href="https://f-droid.org/en/packages/com.unprompted.tildefriends.fdroid/"
 | 
			
		||||
											><img src="f-droid.svg" style="height: 2em; margin: 0" />
 | 
			
		||||
											Get it on F-Droid</a
 | 
			
		||||
										>
 | 
			
		||||
										<a
 | 
			
		||||
											class="w3-button w3-round-large w3-padding w3-blue-gray w3-margin-top"
 | 
			
		||||
											href="https://play.google.com/store/apps/details?id=com.unprompted.tildefriends"
 | 
			
		||||
										>
 | 
			
		||||
											<img
 | 
			
		||||
												src="googleplay.svg"
 | 
			
		||||
												style="height: 2em; margin: 0"
 | 
			
		||||
											/>
 | 
			
		||||
											Get it on Google Play
 | 
			
		||||
										</a>
 | 
			
		||||
										<a
 | 
			
		||||
											class="w3-button w3-round-large w3-padding w3-blue-gray w3-margin-top"
 | 
			
		||||
											href="https://testflight.apple.com/join/tXxgtSpE"
 | 
			
		||||
										>
 | 
			
		||||
											<img src="ios.svg" style="height: 2em; margin: 0" />
 | 
			
		||||
											Get it on iOS (TestFlight)
 | 
			
		||||
										</a>
 | 
			
		||||
									</p>
 | 
			
		||||
									<p>Just launch the app.</p>
 | 
			
		||||
								</div>
 | 
			
		||||
								<div class="w3-container w3-cell w3-mobile">
 | 
			
		||||
									<h3>Web</h3>
 | 
			
		||||
									<p>
 | 
			
		||||
										<a
 | 
			
		||||
											class="w3-button w3-round-large w3-blue w3-padding-large"
 | 
			
		||||
											href="https://www.tildefriends.net/~core/ssb/"
 | 
			
		||||
											>🦀 Try It</a
 | 
			
		||||
										>
 | 
			
		||||
									</p>
 | 
			
		||||
									<p>
 | 
			
		||||
										<a href="/login?return=/~core/intro"
 | 
			
		||||
											>Register an account with tildefriends.net</a
 | 
			
		||||
										>
 | 
			
		||||
										to take it for a spin right away.
 | 
			
		||||
									</p>
 | 
			
		||||
									<h3>PeachCloud</h3>
 | 
			
		||||
									<p>
 | 
			
		||||
										Tilde Friends is also a part of 🍑☁️<a
 | 
			
		||||
											href="https://peach-docs.commoninternet.net/"
 | 
			
		||||
											>PeachCloud</a
 | 
			
		||||
										>, which is available on
 | 
			
		||||
										<a href="https://apps.yunohost.org/app/peachpub"
 | 
			
		||||
											>YunoHost</a
 | 
			
		||||
										>
 | 
			
		||||
										for accessible self-hosting.
 | 
			
		||||
									</p>
 | 
			
		||||
								</div>
 | 
			
		||||
								<div class="w3-container w3-cell w3-mobile">
 | 
			
		||||
									<h3>Desktop</h3>
 | 
			
		||||
									<p>
 | 
			
		||||
										<a
 | 
			
		||||
											class="w3-button w3-round-large w3-black w3-padding-large"
 | 
			
		||||
											href="https://dev.tildefriends.net/cory/tildefriends/releases"
 | 
			
		||||
											><i class="fa fa-download"></i> Download</a
 | 
			
		||||
										>
 | 
			
		||||
										<a
 | 
			
		||||
											class="w3-button w3-round-large w3-padding w3-blue-gray"
 | 
			
		||||
											href="https://dev.tildefriends.net/releases/tildefriends-x86_64.AppImage"
 | 
			
		||||
										>
 | 
			
		||||
											<img src="appimage.svg" style="height: 2em; margin: 0" />
 | 
			
		||||
											Get Linux 64-bit AppImage
 | 
			
		||||
										</a>
 | 
			
		||||
									</p>
 | 
			
		||||
									<p>
 | 
			
		||||
										Tilde Friends is distributed as a single executable file (or
 | 
			
		||||
										source that you can
 | 
			
		||||
										<a href="http://dev.tildefriends.net">build yourself</a>)
 | 
			
		||||
										and stores all of its data in a single
 | 
			
		||||
										file(<code>db.sqlite</code>). You can generally download the
 | 
			
		||||
										latest executable from
 | 
			
		||||
										<a
 | 
			
		||||
											href="https://dev.tildefriends.net/cory/tildefriends/releases"
 | 
			
		||||
											>releases</a
 | 
			
		||||
										>
 | 
			
		||||
										for your platform, mark it as executable (<code
 | 
			
		||||
											>chmod +x tildefriends*</code
 | 
			
		||||
										>
 | 
			
		||||
										on macOS and Linux), and run. Run with <code>-h</code> to
 | 
			
		||||
										learn more.
 | 
			
		||||
									</p>
 | 
			
		||||
									<p>
 | 
			
		||||
										Tilde Friends will run in the console and provide a web
 | 
			
		||||
										interface at
 | 
			
		||||
										<a href="http://localhost:12345/">http://localhost:12345/</a
 | 
			
		||||
										>. You will have to register a username and password to sign
 | 
			
		||||
										into your instance.
 | 
			
		||||
									</p>
 | 
			
		||||
								</div>
 | 
			
		||||
							</div>
 | 
			
		||||
							<p>
 | 
			
		||||
								After a <a href="/~core/intro">brief introduction</a>, Tilde
 | 
			
		||||
								Friends will take you to the Secure Scuttlebutt social network
 | 
			
		||||
								app.
 | 
			
		||||
							</p>
 | 
			
		||||
						</li>
 | 
			
		||||
						<li>Create an account to identify yourself with that instance.</li>
 | 
			
		||||
						<li>
 | 
			
		||||
							Describe yourself in your profile in the <b>ssb</b> app. Give
 | 
			
		||||
							yourself a name and an avatar if you like.
 | 
			
		||||
@@ -149,11 +224,11 @@
 | 
			
		||||
		<!-- SSB Section -->
 | 
			
		||||
		<div class="w3-light-grey">
 | 
			
		||||
			<div class="w3-row-padding w3-padding-64">
 | 
			
		||||
				<div class="w3-col l4 m6 s4">
 | 
			
		||||
				<div class="w3-col l4 m6 s4 w3-center">
 | 
			
		||||
					<a href="https://scuttlebutt.nz/"
 | 
			
		||||
						><img
 | 
			
		||||
							class="w3-image w3-round-large"
 | 
			
		||||
							src="ssb.png"
 | 
			
		||||
							class="w3-image"
 | 
			
		||||
							src="hermietildefriends.svg"
 | 
			
		||||
							alt="Secure Scuttlebutt"
 | 
			
		||||
					/></a>
 | 
			
		||||
				</div>
 | 
			
		||||
@@ -223,16 +298,15 @@
 | 
			
		||||
 | 
			
		||||
		<!-- Technlology Section -->
 | 
			
		||||
		<div class="w3-container w3-padding-64 w3-light-grey w3-center">
 | 
			
		||||
			<h1 class="w3-jumbo"><b>Boring Technology</b></h1>
 | 
			
		||||
			<h1 class="w3-jumbo"><b>Built to Last</b></h1>
 | 
			
		||||
			<p>
 | 
			
		||||
				Tilde Friends is built using boring, trusted tech. Unless a better
 | 
			
		||||
				reason presents itself, it strives to use only simple and widely adopted
 | 
			
		||||
				dependencies in order to keep it easy to build for all sorts of
 | 
			
		||||
				platforms and maintainable for a very long time.
 | 
			
		||||
				Tilde Friends strives to use only simple and widely adopted dependencies
 | 
			
		||||
				in order to keep it easy to build for all sorts of platforms and
 | 
			
		||||
				maintainable for a very long time.
 | 
			
		||||
			</p>
 | 
			
		||||
			<p>
 | 
			
		||||
				Though of course for building Tilde Friends apps, you are free to use
 | 
			
		||||
				whatever fits.
 | 
			
		||||
				whatever fits on top.
 | 
			
		||||
			</p>
 | 
			
		||||
 | 
			
		||||
			<div class="w3-row" style="margin-top: 64px">
 | 
			
		||||
@@ -266,10 +340,6 @@
 | 
			
		||||
					<i class="fa fa-lock w3-text-purple w3-jumbo"></i>
 | 
			
		||||
					<p>libsodium</p>
 | 
			
		||||
				</a>
 | 
			
		||||
				<a href="https://github.com/openssl/openssl/releases" class="w3-col s3">
 | 
			
		||||
					<i class="fa fa-shield-halved w3-text-green w3-jumbo"></i>
 | 
			
		||||
					<p>OpenSSL</p>
 | 
			
		||||
				</a>
 | 
			
		||||
				<a
 | 
			
		||||
					href="https://github.com/ianlancetaylor/libbacktrace"
 | 
			
		||||
					class="w3-col s3"
 | 
			
		||||
@@ -277,13 +347,13 @@
 | 
			
		||||
					<i class="fa fa-burst w3-text-pink w3-jumbo"></i>
 | 
			
		||||
					<p>libbacktrace</p>
 | 
			
		||||
				</a>
 | 
			
		||||
			</div>
 | 
			
		||||
 | 
			
		||||
			<div class="w3-row" style="margin-top: 64px">
 | 
			
		||||
				<a href="https://codemirror.net/docs/changelog/" class="w3-col s3">
 | 
			
		||||
					<i class="fa fa-keyboard w3-text-indigo w3-jumbo"></i>
 | 
			
		||||
					<p>CodeMirror</p>
 | 
			
		||||
				</a>
 | 
			
		||||
			</div>
 | 
			
		||||
 | 
			
		||||
			<div class="w3-row" style="margin-top: 64px">
 | 
			
		||||
				<a href="https://github.com/jlfwong/speedscope/" class="w3-col s3">
 | 
			
		||||
					<i class="fa fa-microscope w3-text-orange w3-jumbo"></i>
 | 
			
		||||
					<p>Speedscope</p>
 | 
			
		||||
@@ -296,9 +366,6 @@
 | 
			
		||||
					<i class="fa fa-book-atlas w3-text-purple w3-jumbo"></i>
 | 
			
		||||
					<p>c-ares</p>
 | 
			
		||||
				</a>
 | 
			
		||||
			</div>
 | 
			
		||||
 | 
			
		||||
			<div class="w3-row" style="margin-top: 64px">
 | 
			
		||||
				<a href="https://www.gnu.org/software/make/" class="w3-col s3">
 | 
			
		||||
					<i class="fa fa-hammer w3-text-teal w3-jumbo"></i>
 | 
			
		||||
					<p>GNU Make</p>
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										3
									
								
								apps/welcome/ios.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,3 @@
 | 
			
		||||
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" width="814" height="1000">
 | 
			
		||||
  <path d="M788.1 340.9c-5.8 4.5-108.2 62.2-108.2 190.5 0 148.4 130.3 200.9 134.2 202.2-.6 3.2-20.7 71.9-68.7 141.9-42.8 61.6-87.5 123.1-155.5 123.1s-85.5-39.5-164-39.5c-76.5 0-103.7 40.8-165.9 40.8s-105.6-57-155.5-127C46.7 790.7 0 663 0 541.8c0-194.4 126.4-297.5 250.8-297.5 66.1 0 121.2 43.4 162.7 43.4 39.5 0 101.1-46 176.3-46 28.5 0 130.9 2.6 198.3 99.2zm-234-181.5c31.1-36.9 53.1-88.1 53.1-139.3 0-7.1-.6-14.3-1.9-20.1-50.6 1.9-110.8 33.7-147.1 75.8-28.5 32.4-55.1 83.6-55.1 135.5 0 7.8 1.3 15.6 1.9 18.1 3.2.6 8.4 1.3 13.6 1.3 45.4 0 102.5-30.4 135.5-71.3z"/>
 | 
			
		||||
</svg>
 | 
			
		||||
| 
		 After Width: | Height: | Size: 660 B  | 
| 
		 Before Width: | Height: | Size: 50 KiB  | 
| 
		 Before Width: | Height: | Size: 141 KiB After Width: | Height: | Size: 141 KiB  | 
@@ -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}
 | 
			
		||||
/* 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}
 | 
			
		||||
@@ -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-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%}
 | 
			
		||||
@@ -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-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}
 | 
			
		||||
@@ -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-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}
 | 
			
		||||
@@ -232,4 +248,4 @@ hr{border:0;border-top:1px solid #eee;margin:20px 0}
 | 
			
		||||
.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}
 | 
			
		||||
.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}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										42
									
								
								apps/wiki/lit-all.min.js
									
									
									
									
										vendored
									
									
								
							
							
						
						
							
								
								
									
										132
									
								
								core/app.js
									
									
									
									
									
								
							
							
						
						@@ -1,53 +1,48 @@
 | 
			
		||||
/**
 | 
			
		||||
 * \file
 | 
			
		||||
 * \defgroup tfapp Tilde Friends App JS
 | 
			
		||||
 * Tilde Friends server-side app wrapper.
 | 
			
		||||
 * @{
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
/** \cond */
 | 
			
		||||
import * as core from './core.js';
 | 
			
		||||
 | 
			
		||||
let g_next_id = 1;
 | 
			
		||||
let g_calls = {};
 | 
			
		||||
export {App};
 | 
			
		||||
/** \endcond */
 | 
			
		||||
 | 
			
		||||
let gSessionIndex = 0;
 | 
			
		||||
/** A sequence number of apps. */
 | 
			
		||||
let g_session_index = 0;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * TODOC
 | 
			
		||||
 * @returns
 | 
			
		||||
 */
 | 
			
		||||
function makeSessionId() {
 | 
			
		||||
	return 'session_' + (gSessionIndex++).toString();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * TODOC
 | 
			
		||||
 * @returns
 | 
			
		||||
 ** App constructor.
 | 
			
		||||
 ** @return An app instance.
 | 
			
		||||
 */
 | 
			
		||||
function App() {
 | 
			
		||||
	this._on_output = null;
 | 
			
		||||
	this._send_queue = [];
 | 
			
		||||
	this.calls = {};
 | 
			
		||||
	this._next_call_id = 1;
 | 
			
		||||
	return this;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * TODOC
 | 
			
		||||
 * @param {*} callback
 | 
			
		||||
 */
 | 
			
		||||
App.prototype.readOutput = function (callback) {
 | 
			
		||||
	this._on_output = callback;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * TODOC
 | 
			
		||||
 * @param {*} api
 | 
			
		||||
 * @returns
 | 
			
		||||
 ** Create a function wrapper that when called invokes a function on the app
 | 
			
		||||
 ** itself.
 | 
			
		||||
 ** @param api The function and argument names.
 | 
			
		||||
 ** @return A function.
 | 
			
		||||
 */
 | 
			
		||||
App.prototype.makeFunction = function (api) {
 | 
			
		||||
	let self = this;
 | 
			
		||||
	let result = function () {
 | 
			
		||||
		let id = g_next_id++;
 | 
			
		||||
		while (!id || g_calls[id]) {
 | 
			
		||||
			id = g_next_id++;
 | 
			
		||||
		let id = self._next_call_id++;
 | 
			
		||||
		while (!id || self.calls[id]) {
 | 
			
		||||
			id = self._next_call_id++;
 | 
			
		||||
		}
 | 
			
		||||
		let promise = new Promise(function (resolve, reject) {
 | 
			
		||||
			g_calls[id] = {resolve: resolve, reject: reject};
 | 
			
		||||
			self.calls[id] = {resolve: resolve, reject: reject};
 | 
			
		||||
		});
 | 
			
		||||
		let message = {
 | 
			
		||||
			message: 'tfrpc',
 | 
			
		||||
			action: 'tfrpc',
 | 
			
		||||
			method: api[0],
 | 
			
		||||
			params: [...arguments],
 | 
			
		||||
			id: id,
 | 
			
		||||
@@ -60,8 +55,8 @@ App.prototype.makeFunction = function (api) {
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * TODOC
 | 
			
		||||
 * @param {*} message
 | 
			
		||||
 ** Send a message to the app.
 | 
			
		||||
 ** @param message The message to send.
 | 
			
		||||
 */
 | 
			
		||||
App.prototype.send = function (message) {
 | 
			
		||||
	if (this._send_queue) {
 | 
			
		||||
@@ -78,12 +73,11 @@ App.prototype.send = function (message) {
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * TODOC
 | 
			
		||||
 * @param {*} request
 | 
			
		||||
 * @param {*} response
 | 
			
		||||
 * @param {*} client
 | 
			
		||||
 ** App socket handler.
 | 
			
		||||
 ** @param request The HTTP request of the WebSocket connection.
 | 
			
		||||
 ** @param response The HTTP response.
 | 
			
		||||
 */
 | 
			
		||||
async function socket(request, response, client) {
 | 
			
		||||
exports.app_socket = async function socket(request, response) {
 | 
			
		||||
	let process;
 | 
			
		||||
	let options = {};
 | 
			
		||||
	let credentials = await httpd.auth_query(request.headers);
 | 
			
		||||
@@ -103,10 +97,16 @@ async function socket(request, response, client) {
 | 
			
		||||
			try {
 | 
			
		||||
				message = JSON.parse(event.data);
 | 
			
		||||
			} catch (error) {
 | 
			
		||||
				print('ERROR', error, event.data, event.data.length, event.opCode);
 | 
			
		||||
				print(
 | 
			
		||||
					'WebSocket error:',
 | 
			
		||||
					error,
 | 
			
		||||
					event.data,
 | 
			
		||||
					event.data.length,
 | 
			
		||||
					event.opCode
 | 
			
		||||
				);
 | 
			
		||||
				return;
 | 
			
		||||
			}
 | 
			
		||||
			if (message.action == 'hello') {
 | 
			
		||||
			if (!process && message.action == 'hello') {
 | 
			
		||||
				let packageOwner;
 | 
			
		||||
				let packageName;
 | 
			
		||||
				let blobId;
 | 
			
		||||
@@ -123,7 +123,7 @@ async function socket(request, response, client) {
 | 
			
		||||
					if (!blobId) {
 | 
			
		||||
						response.send(
 | 
			
		||||
							JSON.stringify({
 | 
			
		||||
								message: 'tfrpc',
 | 
			
		||||
								action: 'tfrpc',
 | 
			
		||||
								method: 'error',
 | 
			
		||||
								params: [message.path + ' not found'],
 | 
			
		||||
								id: -1,
 | 
			
		||||
@@ -149,7 +149,7 @@ async function socket(request, response, client) {
 | 
			
		||||
								parentApp: parentApp,
 | 
			
		||||
								id: blobId,
 | 
			
		||||
							},
 | 
			
		||||
							await ssb.getIdentityInfo(
 | 
			
		||||
							await ssb_internal.getIdentityInfo(
 | 
			
		||||
								credentials?.session?.name,
 | 
			
		||||
								packageOwner,
 | 
			
		||||
								packageName
 | 
			
		||||
@@ -164,7 +164,7 @@ async function socket(request, response, client) {
 | 
			
		||||
				options.packageOwner = packageOwner;
 | 
			
		||||
				options.packageName = packageName;
 | 
			
		||||
				options.url = message.url;
 | 
			
		||||
				let sessionId = makeSessionId();
 | 
			
		||||
				let sessionId = 'session_' + (g_session_index++).toString();
 | 
			
		||||
				if (blobId) {
 | 
			
		||||
					if (message.edit_only) {
 | 
			
		||||
						response.send(
 | 
			
		||||
@@ -176,9 +176,24 @@ async function socket(request, response, client) {
 | 
			
		||||
					}
 | 
			
		||||
				}
 | 
			
		||||
				if (process) {
 | 
			
		||||
					process.app.readOutput(function (message) {
 | 
			
		||||
					process.client_api.tfrpc = function (message) {
 | 
			
		||||
						if (message.id) {
 | 
			
		||||
							let calls = process?.app?.calls;
 | 
			
		||||
							if (calls) {
 | 
			
		||||
								let call = calls[message.id];
 | 
			
		||||
								if (call) {
 | 
			
		||||
									if (message.error !== undefined) {
 | 
			
		||||
										call.reject(message.error);
 | 
			
		||||
									} else {
 | 
			
		||||
										call.resolve(message.result);
 | 
			
		||||
									}
 | 
			
		||||
									delete calls[message.id];
 | 
			
		||||
								}
 | 
			
		||||
							}
 | 
			
		||||
						}
 | 
			
		||||
					};
 | 
			
		||||
					process.app._on_output = (message) =>
 | 
			
		||||
						response.send(JSON.stringify(message), 0x1);
 | 
			
		||||
					});
 | 
			
		||||
					process.app.send();
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
@@ -207,26 +222,13 @@ async function socket(request, response, client) {
 | 
			
		||||
				if (process && process.timeout > 0) {
 | 
			
		||||
					setTimeout(ping, process.timeout);
 | 
			
		||||
				}
 | 
			
		||||
			} else if (message.action == 'resetPermission') {
 | 
			
		||||
				if (process) {
 | 
			
		||||
					process.resetPermission(message.permission);
 | 
			
		||||
				}
 | 
			
		||||
			} else if (message.action == 'setActiveIdentity') {
 | 
			
		||||
				process.setActiveIdentity(message.identity);
 | 
			
		||||
			} else if (message.action == 'createIdentity') {
 | 
			
		||||
				await process.createIdentity();
 | 
			
		||||
			} else if (message.message == 'tfrpc') {
 | 
			
		||||
				if (message.id && g_calls[message.id]) {
 | 
			
		||||
					if (message.error !== undefined) {
 | 
			
		||||
						g_calls[message.id].reject(message.error);
 | 
			
		||||
					} else {
 | 
			
		||||
						g_calls[message.id].resolve(message.result);
 | 
			
		||||
					}
 | 
			
		||||
					delete g_calls[message.id];
 | 
			
		||||
				}
 | 
			
		||||
			} else {
 | 
			
		||||
				if (process && process.eventHandlers['message']) {
 | 
			
		||||
					await core.invoke(process.eventHandlers['message'], [message]);
 | 
			
		||||
				if (process) {
 | 
			
		||||
					if (process.client_api[message.action]) {
 | 
			
		||||
						process.client_api[message.action](message);
 | 
			
		||||
					} else if (process.eventHandlers['message']) {
 | 
			
		||||
						await core.invoke(process.eventHandlers['message'], [message]);
 | 
			
		||||
					}
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		} else if (event.opCode == 0x8) {
 | 
			
		||||
@@ -245,6 +247,6 @@ async function socket(request, response, client) {
 | 
			
		||||
	};
 | 
			
		||||
 | 
			
		||||
	response.upgrade(100, {});
 | 
			
		||||
}
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export {socket, App};
 | 
			
		||||
/** @} */
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										697
									
								
								core/client.js
									
									
									
									
									
								
							
							
						
						
							
								
								
									
										376
									
								
								core/core.js
									
									
									
									
									
								
							
							
						
						@@ -1,33 +1,46 @@
 | 
			
		||||
import * as app from './app.js';
 | 
			
		||||
import * as form from './form.js';
 | 
			
		||||
import * as http from './http.js';
 | 
			
		||||
/**
 | 
			
		||||
 * \file
 | 
			
		||||
 * \defgroup tfcore Tilde Friends Core JS
 | 
			
		||||
 * Tilde Friends process management, in JavaScript.
 | 
			
		||||
 * @{
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
/** \cond */
 | 
			
		||||
import * as app from './app.js';
 | 
			
		||||
 | 
			
		||||
export {invoke, getProcessBlob};
 | 
			
		||||
/** \endcond */
 | 
			
		||||
 | 
			
		||||
/** All running processes. */
 | 
			
		||||
let gProcesses = {};
 | 
			
		||||
/** Whether stats are currently being sent. */
 | 
			
		||||
let gStatsTimer = false;
 | 
			
		||||
let kPingInterval = 60 * 1000;
 | 
			
		||||
/** Effectively a process ID. */
 | 
			
		||||
let g_handler_index = 0;
 | 
			
		||||
/** Time between pings, in milliseconds. */
 | 
			
		||||
const k_ping_interval = 60 * 1000;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * TODOC
 | 
			
		||||
 * @param {*} out
 | 
			
		||||
 * @param {*} error
 | 
			
		||||
 * Print an error.
 | 
			
		||||
 * @param error The error.
 | 
			
		||||
 */
 | 
			
		||||
function printError(out, error) {
 | 
			
		||||
function printError(error) {
 | 
			
		||||
	if (error.stackTrace) {
 | 
			
		||||
		out.print(error.fileName + ':' + error.lineNumber + ': ' + error.message);
 | 
			
		||||
		out.print(error.stackTrace);
 | 
			
		||||
		print(error.fileName + ':' + error.lineNumber + ': ' + error.message);
 | 
			
		||||
		print(error.stackTrace);
 | 
			
		||||
	} else {
 | 
			
		||||
		for (let [k, v] of Object.entries(error)) {
 | 
			
		||||
			out.print(k, v);
 | 
			
		||||
			print(k, v);
 | 
			
		||||
		}
 | 
			
		||||
		out.print(error.toString());
 | 
			
		||||
		print(error.toString());
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * TODOC
 | 
			
		||||
 * @param {*} handlers
 | 
			
		||||
 * @param {*} argv
 | 
			
		||||
 * @returns
 | 
			
		||||
 * Invoke a handler.
 | 
			
		||||
 * @param handlers The handlers on which to invoke the callback.
 | 
			
		||||
 * @param argv Arguments to pass to the handlers.
 | 
			
		||||
 * @return A promise.
 | 
			
		||||
 */
 | 
			
		||||
function invoke(handlers, argv) {
 | 
			
		||||
	let promises = [];
 | 
			
		||||
@@ -50,10 +63,10 @@ function invoke(handlers, argv) {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * TODOC
 | 
			
		||||
 * @param {*} eventName
 | 
			
		||||
 * @param {*} argv
 | 
			
		||||
 * @returns
 | 
			
		||||
 * Broadcast a named event to all registered apps.
 | 
			
		||||
 * @param eventName the name of the event.
 | 
			
		||||
 * @param argv Arguments to pass to the handlers.
 | 
			
		||||
 * @return A promise.
 | 
			
		||||
 */
 | 
			
		||||
function broadcastEvent(eventName, argv) {
 | 
			
		||||
	let promises = [];
 | 
			
		||||
@@ -66,9 +79,9 @@ function broadcastEvent(eventName, argv) {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * TODOC
 | 
			
		||||
 * @param {*} message
 | 
			
		||||
 * @returns
 | 
			
		||||
 * Send a message to all other instances of the same app.
 | 
			
		||||
 * @param message The message.
 | 
			
		||||
 * @return A promise.
 | 
			
		||||
 */
 | 
			
		||||
function broadcast(message) {
 | 
			
		||||
	let sender = this;
 | 
			
		||||
@@ -87,10 +100,13 @@ function broadcast(message) {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * TODOC
 | 
			
		||||
 * @param {String} eventName
 | 
			
		||||
 * @param {*} argv
 | 
			
		||||
 * @returns
 | 
			
		||||
 * Send a message to all instances of the same app running as the same user.
 | 
			
		||||
 * @param user The user.
 | 
			
		||||
 * @param packageOwner The owner of the app.
 | 
			
		||||
 * @param packageName The name of the app.
 | 
			
		||||
 * @param eventName The name of the event.
 | 
			
		||||
 * @param argv The arguments to pass.
 | 
			
		||||
 * @return A promise.
 | 
			
		||||
 */
 | 
			
		||||
function broadcastAppEventToUser(
 | 
			
		||||
	user,
 | 
			
		||||
@@ -115,10 +131,9 @@ function broadcastAppEventToUser(
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * TODOC
 | 
			
		||||
 * @param {*} caller
 | 
			
		||||
 * @param {*} process
 | 
			
		||||
 * @returns
 | 
			
		||||
 * Get user context information for a call.
 | 
			
		||||
 * @param caller The calling process.
 | 
			
		||||
 * @param process The receiving process.
 | 
			
		||||
 */
 | 
			
		||||
function getUser(caller, process) {
 | 
			
		||||
	return {
 | 
			
		||||
@@ -131,43 +146,11 @@ function getUser(caller, process) {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * TODOC
 | 
			
		||||
 * @param {*} user
 | 
			
		||||
 * @param {*} process
 | 
			
		||||
 * @returns
 | 
			
		||||
 */
 | 
			
		||||
async function getApps(user, process) {
 | 
			
		||||
	if (
 | 
			
		||||
		process.credentials &&
 | 
			
		||||
		process.credentials.session &&
 | 
			
		||||
		process.credentials.session.name
 | 
			
		||||
	) {
 | 
			
		||||
		if (user && user !== process.credentials.session.name && user !== 'core') {
 | 
			
		||||
			return {};
 | 
			
		||||
		} else if (!user) {
 | 
			
		||||
			user = process.credentials.session.name;
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	if (user) {
 | 
			
		||||
		let db = new Database(user);
 | 
			
		||||
		try {
 | 
			
		||||
			let names = JSON.parse(await db.get('apps'));
 | 
			
		||||
			let result = {};
 | 
			
		||||
			for (let name of names) {
 | 
			
		||||
				result[name] = await db.get('path:' + name);
 | 
			
		||||
			}
 | 
			
		||||
			return result;
 | 
			
		||||
		} catch {}
 | 
			
		||||
	}
 | 
			
		||||
	return {};
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * TODOC
 | 
			
		||||
 * @param {*} from
 | 
			
		||||
 * @param {*} to
 | 
			
		||||
 * @param {*} message
 | 
			
		||||
 * @returns
 | 
			
		||||
 * Send a message.
 | 
			
		||||
 * @param from The calling process.
 | 
			
		||||
 * @param to The receiving process.
 | 
			
		||||
 * @param message The message.
 | 
			
		||||
 * @return A promise.
 | 
			
		||||
 */
 | 
			
		||||
function postMessageInternal(from, to, message) {
 | 
			
		||||
	if (to.eventHandlers['message']) {
 | 
			
		||||
@@ -176,14 +159,13 @@ function postMessageInternal(from, to, message) {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * TODOC
 | 
			
		||||
 * @param {*} blobId
 | 
			
		||||
 * @param {*} key
 | 
			
		||||
 * @param {*} options
 | 
			
		||||
 * @returns
 | 
			
		||||
 * Get or create a process for an app blob.
 | 
			
		||||
 * @param blobId The blob identifier.
 | 
			
		||||
 * @param key A unique key for the invocation.
 | 
			
		||||
 * @param options Other options.
 | 
			
		||||
 * @return The process.
 | 
			
		||||
 */
 | 
			
		||||
async function getProcessBlob(blobId, key, options) {
 | 
			
		||||
	// TODO(tasiaiso): break this down ?
 | 
			
		||||
	let process = gProcesses[key];
 | 
			
		||||
	if (!process && !(options && 'create' in options && !options.create)) {
 | 
			
		||||
		let resolveReady;
 | 
			
		||||
@@ -196,67 +178,27 @@ async function getProcessBlob(blobId, key, options) {
 | 
			
		||||
			process.task = new Task();
 | 
			
		||||
			process.packageOwner = options.packageOwner;
 | 
			
		||||
			process.packageName = options.packageName;
 | 
			
		||||
			process.url = options?.url;
 | 
			
		||||
			process.eventHandlers = {};
 | 
			
		||||
			if (!options?.script || options?.script === 'app.js') {
 | 
			
		||||
				process.app = new app.App();
 | 
			
		||||
			}
 | 
			
		||||
			process.lastActive = Date.now();
 | 
			
		||||
			process.lastPing = null;
 | 
			
		||||
			process.timeout = kPingInterval;
 | 
			
		||||
			process.timeout = k_ping_interval;
 | 
			
		||||
			process.ready = new Promise(function (resolve, reject) {
 | 
			
		||||
				resolveReady = resolve;
 | 
			
		||||
				rejectReady = reject;
 | 
			
		||||
			});
 | 
			
		||||
			gProcesses[key] = process;
 | 
			
		||||
			process.task.onExit = function (exitCode, terminationSignal) {
 | 
			
		||||
				broadcastEvent('onSessionEnd', [getUser(process, process)]);
 | 
			
		||||
				process.task = null;
 | 
			
		||||
				delete gProcesses[key];
 | 
			
		||||
			};
 | 
			
		||||
			let imports = {
 | 
			
		||||
				core: {
 | 
			
		||||
					broadcast: broadcast.bind(process),
 | 
			
		||||
					register: function (eventName, handler) {
 | 
			
		||||
						if (!process.eventHandlers[eventName]) {
 | 
			
		||||
							process.eventHandlers[eventName] = [];
 | 
			
		||||
						}
 | 
			
		||||
						process.eventHandlers[eventName].push(handler);
 | 
			
		||||
					},
 | 
			
		||||
					unregister: function (eventName, handler) {
 | 
			
		||||
						if (process.eventHandlers[eventName]) {
 | 
			
		||||
							let index = process.eventHandlers[eventName].indexOf(handler);
 | 
			
		||||
							if (index != -1) {
 | 
			
		||||
								process.eventHandlers[eventName].splice(index, 1);
 | 
			
		||||
							}
 | 
			
		||||
							if (process.eventHandlers[eventName].length == 0) {
 | 
			
		||||
								delete process.eventHandlers[eventName];
 | 
			
		||||
							}
 | 
			
		||||
						}
 | 
			
		||||
					},
 | 
			
		||||
					user: getUser(process, process),
 | 
			
		||||
					users: async function () {
 | 
			
		||||
						try {
 | 
			
		||||
							return JSON.parse(await new Database('auth').get('users'));
 | 
			
		||||
						} catch {
 | 
			
		||||
							return [];
 | 
			
		||||
						}
 | 
			
		||||
					},
 | 
			
		||||
					permissionsGranted: async function () {
 | 
			
		||||
						let user = process?.credentials?.session?.name;
 | 
			
		||||
						let settings = await loadSettings();
 | 
			
		||||
						if (
 | 
			
		||||
							user &&
 | 
			
		||||
							options?.packageOwner &&
 | 
			
		||||
							options?.packageName &&
 | 
			
		||||
							settings.userPermissions &&
 | 
			
		||||
							settings.userPermissions[user] &&
 | 
			
		||||
							settings.userPermissions[user][options.packageOwner]
 | 
			
		||||
						) {
 | 
			
		||||
							return settings.userPermissions[user][options.packageOwner][
 | 
			
		||||
								options.packageName
 | 
			
		||||
							];
 | 
			
		||||
						}
 | 
			
		||||
					},
 | 
			
		||||
					allPermissionsGranted: async function () {
 | 
			
		||||
						let user = process?.credentials?.session?.name;
 | 
			
		||||
						let settings = await loadSettings();
 | 
			
		||||
@@ -270,12 +212,6 @@ async function getProcessBlob(blobId, key, options) {
 | 
			
		||||
							return settings.userPermissions[user];
 | 
			
		||||
						}
 | 
			
		||||
					},
 | 
			
		||||
					permissionsForUser: async function (user) {
 | 
			
		||||
						let settings = await loadSettings();
 | 
			
		||||
						return settings?.permissions?.[user] ?? [];
 | 
			
		||||
					},
 | 
			
		||||
					apps: (user) => getApps(user, process),
 | 
			
		||||
					getSockets: getSockets,
 | 
			
		||||
					permissionTest: async function (permission) {
 | 
			
		||||
						let user = process?.credentials?.session?.name;
 | 
			
		||||
						let settings = await loadSettings();
 | 
			
		||||
@@ -336,11 +272,6 @@ async function getProcessBlob(blobId, key, options) {
 | 
			
		||||
							throw Error(`Permission denied: ${permission}.`);
 | 
			
		||||
						}
 | 
			
		||||
					},
 | 
			
		||||
					app: {
 | 
			
		||||
						owner: options?.packageOwner,
 | 
			
		||||
						name: options?.packageName,
 | 
			
		||||
					},
 | 
			
		||||
					url: options?.url,
 | 
			
		||||
				},
 | 
			
		||||
			};
 | 
			
		||||
			process.sendIdentities = async function () {
 | 
			
		||||
@@ -349,7 +280,7 @@ async function getProcessBlob(blobId, key, options) {
 | 
			
		||||
						{
 | 
			
		||||
							action: 'identities',
 | 
			
		||||
						},
 | 
			
		||||
						await ssb.getIdentityInfo(
 | 
			
		||||
						await ssb_internal.getIdentityInfo(
 | 
			
		||||
							process?.credentials?.session?.name,
 | 
			
		||||
							options?.packageOwner,
 | 
			
		||||
							options?.packageName
 | 
			
		||||
@@ -392,7 +323,7 @@ async function getProcessBlob(blobId, key, options) {
 | 
			
		||||
						options.packageName,
 | 
			
		||||
						'setActiveIdentity',
 | 
			
		||||
						[
 | 
			
		||||
							await ssb.getActiveIdentity(
 | 
			
		||||
							await imports.ssb.getActiveIdentity(
 | 
			
		||||
								process.credentials?.session?.name,
 | 
			
		||||
								options.packageOwner,
 | 
			
		||||
								options.packageName
 | 
			
		||||
@@ -405,19 +336,6 @@ async function getProcessBlob(blobId, key, options) {
 | 
			
		||||
				}
 | 
			
		||||
			};
 | 
			
		||||
			if (process.credentials?.permissions?.administration) {
 | 
			
		||||
				imports.core.globalSettingsDescriptions = async function () {
 | 
			
		||||
					let settings = Object.assign({}, defaultGlobalSettings());
 | 
			
		||||
					for (let [key, value] of Object.entries(await loadSettings())) {
 | 
			
		||||
						if (settings[key]) {
 | 
			
		||||
							settings[key].value = value;
 | 
			
		||||
						}
 | 
			
		||||
					}
 | 
			
		||||
					return settings;
 | 
			
		||||
				};
 | 
			
		||||
				imports.core.globalSettingsGet = async function (key) {
 | 
			
		||||
					let settings = await loadSettings();
 | 
			
		||||
					return settings?.[key];
 | 
			
		||||
				};
 | 
			
		||||
				imports.core.globalSettingsSet = async function (key, value) {
 | 
			
		||||
					await imports.core.permissionTest('set_global_setting');
 | 
			
		||||
					print('Setting', key, value);
 | 
			
		||||
@@ -462,16 +380,15 @@ async function getProcessBlob(blobId, key, options) {
 | 
			
		||||
					if (process.app) {
 | 
			
		||||
						process.app.makeFunction(['error'])(error);
 | 
			
		||||
					} else {
 | 
			
		||||
						printError({print: print}, error);
 | 
			
		||||
						printError(error);
 | 
			
		||||
					}
 | 
			
		||||
				} catch (e) {
 | 
			
		||||
					printError({print: print}, error);
 | 
			
		||||
					printError(error);
 | 
			
		||||
				}
 | 
			
		||||
			};
 | 
			
		||||
			imports.ssb = Object.fromEntries(
 | 
			
		||||
				Object.keys(ssb).map((key) => [key, ssb[key].bind(ssb)])
 | 
			
		||||
			);
 | 
			
		||||
			imports.ssb.port = tildefriends.ssb_port;
 | 
			
		||||
			imports.ssb.createIdentity = () => process.createIdentity();
 | 
			
		||||
			imports.ssb.addIdentity = function (id) {
 | 
			
		||||
				if (
 | 
			
		||||
@@ -500,26 +417,6 @@ async function getProcessBlob(blobId, key, options) {
 | 
			
		||||
				}
 | 
			
		||||
			};
 | 
			
		||||
			imports.ssb.setActiveIdentity = (id) => process.setActiveIdentity(id);
 | 
			
		||||
			imports.ssb.getActiveIdentity = () =>
 | 
			
		||||
				ssb.getActiveIdentity(
 | 
			
		||||
					process.credentials?.session?.name,
 | 
			
		||||
					options.packageOwner,
 | 
			
		||||
					options.packageName
 | 
			
		||||
				);
 | 
			
		||||
			imports.ssb.getOwnerIdentities = function () {
 | 
			
		||||
				if (options.packageOwner) {
 | 
			
		||||
					return ssb.getIdentities(options.packageOwner);
 | 
			
		||||
				}
 | 
			
		||||
			};
 | 
			
		||||
			imports.ssb.getIdentities = function () {
 | 
			
		||||
				if (
 | 
			
		||||
					process.credentials &&
 | 
			
		||||
					process.credentials.session &&
 | 
			
		||||
					process.credentials.session.name
 | 
			
		||||
				) {
 | 
			
		||||
					return ssb.getIdentities(process.credentials.session.name);
 | 
			
		||||
				}
 | 
			
		||||
			};
 | 
			
		||||
			imports.ssb.getPrivateKey = function (id) {
 | 
			
		||||
				if (
 | 
			
		||||
					process.credentials &&
 | 
			
		||||
@@ -577,19 +474,6 @@ async function getProcessBlob(blobId, key, options) {
 | 
			
		||||
					);
 | 
			
		||||
				}
 | 
			
		||||
			};
 | 
			
		||||
			imports.ssb.setServerFollowingMe = function (id, following) {
 | 
			
		||||
				if (
 | 
			
		||||
					process.credentials &&
 | 
			
		||||
					process.credentials.session &&
 | 
			
		||||
					process.credentials.session.name
 | 
			
		||||
				) {
 | 
			
		||||
					return ssb.setServerFollowingMe(
 | 
			
		||||
						process.credentials.session.name,
 | 
			
		||||
						id,
 | 
			
		||||
						following
 | 
			
		||||
					);
 | 
			
		||||
				}
 | 
			
		||||
			};
 | 
			
		||||
			imports.ssb.swapWithServerIdentity = function (id) {
 | 
			
		||||
				if (
 | 
			
		||||
					process.credentials &&
 | 
			
		||||
@@ -602,13 +486,6 @@ async function getProcessBlob(blobId, key, options) {
 | 
			
		||||
					);
 | 
			
		||||
				}
 | 
			
		||||
			};
 | 
			
		||||
			imports.ssb.addEventListener = undefined;
 | 
			
		||||
			imports.ssb.removeEventListener = undefined;
 | 
			
		||||
			imports.ssb.getIdentityInfo = undefined;
 | 
			
		||||
			imports.fetch = async function (url, options) {
 | 
			
		||||
				let settings = await loadSettings();
 | 
			
		||||
				return http.fetch(url, options, settings?.fetch_hosts);
 | 
			
		||||
			};
 | 
			
		||||
 | 
			
		||||
			if (
 | 
			
		||||
				process.credentials &&
 | 
			
		||||
@@ -664,17 +541,26 @@ async function getProcessBlob(blobId, key, options) {
 | 
			
		||||
					permissions: await imports.core.permissionsGranted(),
 | 
			
		||||
				});
 | 
			
		||||
			};
 | 
			
		||||
			process.resetPermission = async function resetPermission(permission) {
 | 
			
		||||
				let user = process?.credentials?.session?.name;
 | 
			
		||||
				await ssb.setUserPermission(
 | 
			
		||||
					user,
 | 
			
		||||
					options?.packageOwner,
 | 
			
		||||
					options?.packageName,
 | 
			
		||||
					permission,
 | 
			
		||||
					undefined
 | 
			
		||||
				);
 | 
			
		||||
				return process.sendPermissions();
 | 
			
		||||
			process.client_api = {
 | 
			
		||||
				createIdentity: function () {
 | 
			
		||||
					return process.createIdentity();
 | 
			
		||||
				},
 | 
			
		||||
				resetPermission: async function resetPermission(message) {
 | 
			
		||||
					let user = process?.credentials?.session?.name;
 | 
			
		||||
					await ssb.setUserPermission(
 | 
			
		||||
						user,
 | 
			
		||||
						options?.packageOwner,
 | 
			
		||||
						options?.packageName,
 | 
			
		||||
						message.permission,
 | 
			
		||||
						undefined
 | 
			
		||||
					);
 | 
			
		||||
					return process.sendPermissions();
 | 
			
		||||
				},
 | 
			
		||||
				setActiveIdentity: function setActiveIdentity(message) {
 | 
			
		||||
					return process.setActiveIdentity(message.identity);
 | 
			
		||||
				},
 | 
			
		||||
			};
 | 
			
		||||
			ssb.registerImports(imports, process);
 | 
			
		||||
			process.task.setImports(imports);
 | 
			
		||||
			process.task.activate();
 | 
			
		||||
			let source = await ssb.blobGet(blobId);
 | 
			
		||||
@@ -701,9 +587,8 @@ async function getProcessBlob(blobId, key, options) {
 | 
			
		||||
					);
 | 
			
		||||
				}
 | 
			
		||||
			} catch (e) {
 | 
			
		||||
				printError({print: print}, e);
 | 
			
		||||
				printError(e);
 | 
			
		||||
			}
 | 
			
		||||
			broadcastEvent('onSessionBegin', [getUser(process, process)]);
 | 
			
		||||
			if (process.app) {
 | 
			
		||||
				process.app.send({action: 'ready', version: version()});
 | 
			
		||||
				await process.sendPermissions();
 | 
			
		||||
@@ -715,14 +600,10 @@ async function getProcessBlob(blobId, key, options) {
 | 
			
		||||
				sendStats();
 | 
			
		||||
			}
 | 
			
		||||
		} catch (error) {
 | 
			
		||||
			if (process.app) {
 | 
			
		||||
				if (process?.task?.onError) {
 | 
			
		||||
					process.task.onError(error);
 | 
			
		||||
				} else {
 | 
			
		||||
					printError({print: print}, error);
 | 
			
		||||
				}
 | 
			
		||||
			if (process?.app && process?.task?.onError) {
 | 
			
		||||
				process.task.onError(error);
 | 
			
		||||
			} else {
 | 
			
		||||
				printError({print: print}, error);
 | 
			
		||||
				printError(error);
 | 
			
		||||
			}
 | 
			
		||||
			rejectReady(error);
 | 
			
		||||
		}
 | 
			
		||||
@@ -730,20 +611,28 @@ async function getProcessBlob(blobId, key, options) {
 | 
			
		||||
	return process;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
ssb.addEventListener('message', function () {
 | 
			
		||||
/**
 | 
			
		||||
 * SSB message added callback.
 | 
			
		||||
 */
 | 
			
		||||
ssb_internal.addEventListener('message', function () {
 | 
			
		||||
	broadcastEvent('onMessage', [...arguments]);
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
ssb.addEventListener('broadcasts', function () {
 | 
			
		||||
ssb_internal.addEventListener('blob', function () {
 | 
			
		||||
	broadcastEvent('onBlob', [...arguments]);
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
ssb_internal.addEventListener('broadcasts', function () {
 | 
			
		||||
	broadcastEvent('onBroadcastsChanged', []);
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
ssb.addEventListener('connections', function () {
 | 
			
		||||
ssb_internal.addEventListener('connections', function () {
 | 
			
		||||
	broadcastEvent('onConnectionsChanged', []);
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * TODOC
 | 
			
		||||
 * Load settings from the database.
 | 
			
		||||
 * @return The settings as a key value pairs object.
 | 
			
		||||
 */
 | 
			
		||||
async function loadSettings() {
 | 
			
		||||
	let data = {};
 | 
			
		||||
@@ -764,7 +653,7 @@ async function loadSettings() {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * TODOC
 | 
			
		||||
 * Send periodic stats to all clients.
 | 
			
		||||
 */
 | 
			
		||||
function sendStats() {
 | 
			
		||||
	let apps = Object.values(gProcesses)
 | 
			
		||||
@@ -781,8 +670,16 @@ function sendStats() {
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
let g_handler_index = 0;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Invoke an app's handler.js.
 | 
			
		||||
 * @param response The response object.
 | 
			
		||||
 * @param app_blob_id The app's blob identifier.
 | 
			
		||||
 * @param path The request path.
 | 
			
		||||
 * @param query The request query string.
 | 
			
		||||
 * @param headers The request headers.
 | 
			
		||||
 * @param package_owner The app's owner.
 | 
			
		||||
 * @param package_name The app's name.
 | 
			
		||||
 */
 | 
			
		||||
exports.callAppHandler = async function callAppHandler(
 | 
			
		||||
	response,
 | 
			
		||||
	app_blob_id,
 | 
			
		||||
@@ -848,59 +745,4 @@ exports.callAppHandler = async function callAppHandler(
 | 
			
		||||
	response.end(answer?.data);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * TODOC
 | 
			
		||||
 */
 | 
			
		||||
loadSettings()
 | 
			
		||||
	.then(function (settings) {
 | 
			
		||||
		if (tildefriends.https_port && settings.http_redirect) {
 | 
			
		||||
			httpd.set_http_redirect(settings.http_redirect);
 | 
			
		||||
		}
 | 
			
		||||
		httpd.all('/app/socket', app.socket);
 | 
			
		||||
		let port = httpd.start(tildefriends.http_port);
 | 
			
		||||
		if (tildefriends.args.out_http_port_file) {
 | 
			
		||||
			print('Writing the port file.');
 | 
			
		||||
			File.writeFile(
 | 
			
		||||
				tildefriends.args.out_http_port_file,
 | 
			
		||||
				port.toString() + '\n'
 | 
			
		||||
			)
 | 
			
		||||
				.then(function (r) {
 | 
			
		||||
					print(
 | 
			
		||||
						'Wrote the port file:',
 | 
			
		||||
						tildefriends.args.out_http_port_file,
 | 
			
		||||
						r
 | 
			
		||||
					);
 | 
			
		||||
				})
 | 
			
		||||
				.catch(function () {
 | 
			
		||||
					print('Failed to write the port file.');
 | 
			
		||||
				});
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if (tildefriends.https_port) {
 | 
			
		||||
			async function start_tls() {
 | 
			
		||||
				const kCertificatePath = 'data/httpd/certificate.pem';
 | 
			
		||||
				const kPrivateKeyPath = 'data/httpd/privatekey.pem';
 | 
			
		||||
				let privateKey;
 | 
			
		||||
				let certificate;
 | 
			
		||||
				try {
 | 
			
		||||
					privateKey = utf8Decode(await File.readFile(kPrivateKeyPath));
 | 
			
		||||
					certificate = utf8Decode(await File.readFile(kCertificatePath));
 | 
			
		||||
				} catch (e) {
 | 
			
		||||
					print(`TLS disabled (${e.message}).`);
 | 
			
		||||
					return;
 | 
			
		||||
				}
 | 
			
		||||
				let context = new TlsContext();
 | 
			
		||||
				context.setPrivateKey(privateKey);
 | 
			
		||||
				context.setCertificate(certificate);
 | 
			
		||||
				httpd.start(tildefriends.https_port, context);
 | 
			
		||||
			}
 | 
			
		||||
			start_tls();
 | 
			
		||||
		}
 | 
			
		||||
	})
 | 
			
		||||
	.catch(function (error) {
 | 
			
		||||
		print('Failed to load settings.');
 | 
			
		||||
		printError({print: print}, error);
 | 
			
		||||
		exit(1);
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
export {invoke, getProcessBlob};
 | 
			
		||||
/** @} */
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										44
									
								
								core/form.js
									
									
									
									
									
								
							
							
						
						@@ -1,44 +0,0 @@
 | 
			
		||||
/**
 | 
			
		||||
 * TODOC
 | 
			
		||||
 * @param {*} encoded
 | 
			
		||||
 * @returns
 | 
			
		||||
 */
 | 
			
		||||
function decode(encoded) {
 | 
			
		||||
	let result = '';
 | 
			
		||||
	for (let i = 0; i < encoded.length; i++) {
 | 
			
		||||
		let c = encoded[i];
 | 
			
		||||
		if (c == '+') {
 | 
			
		||||
			result += ' ';
 | 
			
		||||
		} else if (c == '%') {
 | 
			
		||||
			result += String.fromCharCode(parseInt(encoded.slice(i + 1, i + 3), 16));
 | 
			
		||||
			i += 2;
 | 
			
		||||
		} else {
 | 
			
		||||
			result += c;
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return result;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * TODOC
 | 
			
		||||
 * @param {*} encoded
 | 
			
		||||
 * @param {*} initial
 | 
			
		||||
 * @returns
 | 
			
		||||
 */
 | 
			
		||||
function decodeForm(encoded, initial) {
 | 
			
		||||
	let result = initial || {};
 | 
			
		||||
	if (encoded) {
 | 
			
		||||
		encoded = encoded.trim();
 | 
			
		||||
		let items = encoded.split('&');
 | 
			
		||||
		for (let i = 0; i < items.length; i++) {
 | 
			
		||||
			let item = items[i];
 | 
			
		||||
			let equals = item.indexOf('=');
 | 
			
		||||
			let key = decode(item.slice(0, equals));
 | 
			
		||||
			let value = decode(item.slice(equals + 1));
 | 
			
		||||
			result[key] = value;
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return result;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export {decodeForm};
 | 
			
		||||
							
								
								
									
										113
									
								
								core/http.js
									
									
									
									
									
								
							
							
						
						@@ -1,113 +0,0 @@
 | 
			
		||||
/**
 | 
			
		||||
 * TODOC
 | 
			
		||||
 * TODO: document so we can improve this
 | 
			
		||||
 * @param {*} url
 | 
			
		||||
 * @returns
 | 
			
		||||
 */
 | 
			
		||||
function parseUrl(url) {
 | 
			
		||||
	// XXX: Hack.
 | 
			
		||||
	let match = url.match(new RegExp('(\\w+)://([^/:]+)(?::(\\d+))?(.*)'));
 | 
			
		||||
	return {
 | 
			
		||||
		protocol: match[1],
 | 
			
		||||
		host: match[2],
 | 
			
		||||
		path: match[4],
 | 
			
		||||
		port: match[3] ? parseInt(match[3]) : match[1] == 'http' ? 80 : 443,
 | 
			
		||||
	};
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * TODOC
 | 
			
		||||
 * @param {*} data
 | 
			
		||||
 * @returns
 | 
			
		||||
 */
 | 
			
		||||
function parseResponse(data) {
 | 
			
		||||
	let firstLine;
 | 
			
		||||
	let headers = {};
 | 
			
		||||
	while (true) {
 | 
			
		||||
		let endLine = data.indexOf('\r\n');
 | 
			
		||||
		let line = data.substring(0, endLine);
 | 
			
		||||
		data = data.substring(endLine + 2);
 | 
			
		||||
		if (!line.length) {
 | 
			
		||||
			break;
 | 
			
		||||
		} else if (!firstLine) {
 | 
			
		||||
			firstLine = line;
 | 
			
		||||
		} else {
 | 
			
		||||
			let colon = line.indexOf(':');
 | 
			
		||||
			headers[line.substring(colon)] = line.substring(colon + 1);
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return {body: data};
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * TODOC
 | 
			
		||||
 * @param {*} url
 | 
			
		||||
 * @param {*} options
 | 
			
		||||
 * @param {*} allowed_hosts
 | 
			
		||||
 * @returns
 | 
			
		||||
 */
 | 
			
		||||
export function fetch(url, options, allowed_hosts) {
 | 
			
		||||
	let parsed = parseUrl(url);
 | 
			
		||||
	return new Promise(function (resolve, reject) {
 | 
			
		||||
		if ((allowed_hosts ?? []).indexOf(parsed.host) == -1) {
 | 
			
		||||
			throw new Error(`fetch() request to host ${parsed.host} is not allowed.`);
 | 
			
		||||
		}
 | 
			
		||||
		let socket = new Socket();
 | 
			
		||||
		let buffer = new Uint8Array(0);
 | 
			
		||||
 | 
			
		||||
		return socket
 | 
			
		||||
			.connect(parsed.host, parsed.port)
 | 
			
		||||
			.then(function () {
 | 
			
		||||
				socket.read(function (data) {
 | 
			
		||||
					if (data && data.length) {
 | 
			
		||||
						let newBuffer = new Uint8Array(buffer.length + data.length);
 | 
			
		||||
						newBuffer.set(buffer, 0);
 | 
			
		||||
						newBuffer.set(data, buffer.length);
 | 
			
		||||
						buffer = newBuffer;
 | 
			
		||||
					} else {
 | 
			
		||||
						let result = parseHttpResponse(buffer);
 | 
			
		||||
						if (!result) {
 | 
			
		||||
							reject(new Exception('Parse failed.'));
 | 
			
		||||
						}
 | 
			
		||||
						if (typeof result == 'number') {
 | 
			
		||||
							if (result == -2) {
 | 
			
		||||
								reject('Incomplete request.');
 | 
			
		||||
							} else {
 | 
			
		||||
								reject('Bad request.');
 | 
			
		||||
							}
 | 
			
		||||
						} else if (typeof result == 'object') {
 | 
			
		||||
							resolve({
 | 
			
		||||
								body: buffer.slice(result.bytes_parsed),
 | 
			
		||||
								status: result.status,
 | 
			
		||||
								message: result.message,
 | 
			
		||||
								headers: result.headers,
 | 
			
		||||
							});
 | 
			
		||||
						} else {
 | 
			
		||||
							reject(new Exception('Unexpected parse result.'));
 | 
			
		||||
						}
 | 
			
		||||
						resolve(parseResponse(utf8Decode(buffer)));
 | 
			
		||||
					}
 | 
			
		||||
				});
 | 
			
		||||
 | 
			
		||||
				if (parsed.port == 443) {
 | 
			
		||||
					return socket.startTls();
 | 
			
		||||
				}
 | 
			
		||||
			})
 | 
			
		||||
			.then(function () {
 | 
			
		||||
				let body =
 | 
			
		||||
					typeof options?.body == 'string'
 | 
			
		||||
						? utf8Encode(options.body)
 | 
			
		||||
						: options.body || new Uint8Array(0);
 | 
			
		||||
				let headers = utf8Encode(
 | 
			
		||||
					`${options?.method ?? 'GET'} ${parsed.path} HTTP/1.0\r\nHost: ${parsed.host}\r\nConnection: close\r\nContent-Length: ${body.length}\r\n\r\n`
 | 
			
		||||
				);
 | 
			
		||||
				let fullRequest = new Uint8Array(headers.length + body.length);
 | 
			
		||||
				fullRequest.set(headers, 0);
 | 
			
		||||
				fullRequest.set(body, headers.length);
 | 
			
		||||
				socket.write(fullRequest);
 | 
			
		||||
			})
 | 
			
		||||
			.catch(function (error) {
 | 
			
		||||
				reject(error);
 | 
			
		||||
			});
 | 
			
		||||
	});
 | 
			
		||||
}
 | 
			
		||||
@@ -38,8 +38,8 @@
 | 
			
		||||
		style="
 | 
			
		||||
			display: flex;
 | 
			
		||||
			flex-flow: column;
 | 
			
		||||
			width: 100vw;
 | 
			
		||||
			height: 100vh;
 | 
			
		||||
			width: 100%;
 | 
			
		||||
			height: 100%;
 | 
			
		||||
			position: absolute;
 | 
			
		||||
			max-width: 100%;
 | 
			
		||||
			max-height: 100%;
 | 
			
		||||
 
 | 
			
		||||
@@ -7,8 +7,8 @@ html {
 | 
			
		||||
 | 
			
		||||
body {
 | 
			
		||||
	font-family: monospace;
 | 
			
		||||
	background-color: #002b36;
 | 
			
		||||
	color: #eee8d5;
 | 
			
		||||
	background-color: #444;
 | 
			
		||||
	color: #fff;
 | 
			
		||||
	width: 100%;
 | 
			
		||||
	height: 100%;
 | 
			
		||||
	padding: 0;
 | 
			
		||||
 
 | 
			
		||||
@@ -1,11 +1,22 @@
 | 
			
		||||
/**
 | 
			
		||||
 * \file
 | 
			
		||||
 * \defgroup tfrpc Tilde Friends RPC.
 | 
			
		||||
 * Tilde Friends RPC.
 | 
			
		||||
 * @{
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
/** Whether this module is being run in a web browser. */
 | 
			
		||||
const k_is_browser = get_is_browser();
 | 
			
		||||
/** Registered methods. */
 | 
			
		||||
let g_api = {};
 | 
			
		||||
/** The next method identifier. */
 | 
			
		||||
let g_next_id = 1;
 | 
			
		||||
/** Identifiers of pending calls. */
 | 
			
		||||
let g_calls = {};
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * TODOC
 | 
			
		||||
 * @returns
 | 
			
		||||
 * Check if being called from a browser vs. server-side.
 | 
			
		||||
 * @return true if called from a browser.
 | 
			
		||||
 */
 | 
			
		||||
function get_is_browser() {
 | 
			
		||||
	try {
 | 
			
		||||
@@ -15,16 +26,30 @@ function get_is_browser() {
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/** \cond */
 | 
			
		||||
if (k_is_browser) {
 | 
			
		||||
	print = console.log;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
if (k_is_browser) {
 | 
			
		||||
	window.addEventListener('message', function (event) {
 | 
			
		||||
		call_rpc(event.data);
 | 
			
		||||
	});
 | 
			
		||||
} else {
 | 
			
		||||
	core.register('message', function (message) {
 | 
			
		||||
		call_rpc(message?.message);
 | 
			
		||||
	});
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export let rpc = new Proxy({}, {get: make_rpc});
 | 
			
		||||
/** \endcond */
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * TODOC
 | 
			
		||||
 * @param {*} target
 | 
			
		||||
 * @param {*} prop
 | 
			
		||||
 * @param {*} receiver
 | 
			
		||||
 * @returns
 | 
			
		||||
 * Make a function to invoke a remote procedure.
 | 
			
		||||
 * @param target The target.
 | 
			
		||||
 * @param prop The name of the function.
 | 
			
		||||
 * @param receiver The receiver.
 | 
			
		||||
 * @return A function.
 | 
			
		||||
 */
 | 
			
		||||
function make_rpc(target, prop, receiver) {
 | 
			
		||||
	return function () {
 | 
			
		||||
@@ -55,8 +80,8 @@ function make_rpc(target, prop, receiver) {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * TODOC
 | 
			
		||||
 * @param {*} response
 | 
			
		||||
 * Send a response.
 | 
			
		||||
 * @param response The response.
 | 
			
		||||
 */
 | 
			
		||||
function send(response) {
 | 
			
		||||
	if (k_is_browser) {
 | 
			
		||||
@@ -67,8 +92,8 @@ function send(response) {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * TODOC
 | 
			
		||||
 * @param {*} message
 | 
			
		||||
 * Invoke a remote procedure.
 | 
			
		||||
 * @param message An object describing the call.
 | 
			
		||||
 */
 | 
			
		||||
function call_rpc(message) {
 | 
			
		||||
	if (message && message.message === 'tfrpc') {
 | 
			
		||||
@@ -112,22 +137,12 @@ function call_rpc(message) {
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
if (k_is_browser) {
 | 
			
		||||
	window.addEventListener('message', function (event) {
 | 
			
		||||
		call_rpc(event.data);
 | 
			
		||||
	});
 | 
			
		||||
} else {
 | 
			
		||||
	core.register('message', function (message) {
 | 
			
		||||
		call_rpc(message?.message);
 | 
			
		||||
	});
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export let rpc = new Proxy({}, {get: make_rpc});
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * TODOC
 | 
			
		||||
 * @param {*} method
 | 
			
		||||
 * Register a function that to be called remotely.
 | 
			
		||||
 * @param method The method.
 | 
			
		||||
 */
 | 
			
		||||
export function register(method) {
 | 
			
		||||
	g_api[method.name] = method;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/** @} */
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										20
									
								
								core/w3.css
									
									
									
									
									
								
							
							
						
						@@ -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}
 | 
			
		||||
/* 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}
 | 
			
		||||
@@ -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-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%}
 | 
			
		||||
@@ -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-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}
 | 
			
		||||
@@ -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-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}
 | 
			
		||||
@@ -232,4 +248,4 @@ hr{border:0;border-top:1px solid #eee;margin:20px 0}
 | 
			
		||||
.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}
 | 
			
		||||
.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}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										10
									
								
								default.nix
									
									
									
									
									
								
							
							
						
						@@ -1,4 +1,8 @@
 | 
			
		||||
# How to upgrade to a newer version
 | 
			
		||||
# - On the june and december release, you'll have to update nixpkgs to the current branch
 | 
			
		||||
# Change `nixpkgs.url = "github:NixOS/nixpkgs/nixos-24.11";`
 | 
			
		||||
# to the latest release (see https://nixos.org/)
 | 
			
		||||
# - Run `$ nix flake update`
 | 
			
		||||
# - Comment `src.hash`
 | 
			
		||||
# - Change `version`
 | 
			
		||||
# - Run `$ nix build`
 | 
			
		||||
@@ -21,14 +25,14 @@
 | 
			
		||||
}:
 | 
			
		||||
pkgs.stdenv.mkDerivation rec {
 | 
			
		||||
  pname = "tildefriends";
 | 
			
		||||
  version = "0.0.26";
 | 
			
		||||
  version = "0.2025.9";
 | 
			
		||||
 | 
			
		||||
  src = pkgs.fetchFromGitea {
 | 
			
		||||
    domain = "dev.tildefriends.net";
 | 
			
		||||
    owner = "cory";
 | 
			
		||||
    repo = "tildefriends";
 | 
			
		||||
    rev = "v${version}";
 | 
			
		||||
    hash = "sha256-XJ7M++risfsRn9GkS1zjTQpqqV5S09uyimeVzU9hGGg=";
 | 
			
		||||
    hash = "sha256-1nhsfhdOO5HIiiTMb+uROB8nDPL/UpOYm52hZ/OpPyk=";
 | 
			
		||||
    fetchSubmodules = true;
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
@@ -46,7 +50,7 @@ pkgs.stdenv.mkDerivation rec {
 | 
			
		||||
  ];
 | 
			
		||||
 | 
			
		||||
  buildPhase = ''
 | 
			
		||||
    make -j $NIX_BUILD_CORES release
 | 
			
		||||
    make -j $NIX_BUILD_CORES release USE_SYSTEM_SSL=1
 | 
			
		||||
  '';
 | 
			
		||||
 | 
			
		||||
  installPhase = ''
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										2
									
								
								deps/c-ares
									
									
									
									
										vendored
									
									
								
							
							
								
								
								
								
								
							
						
						
							
								
								
									
										2
									
								
								deps/codemirror/cm6.js
									
									
									
									
										vendored
									
									
								
							
							
						
						
							
								
								
									
										407
									
								
								deps/codemirror_src/package-lock.json
									
									
									
										generated
									
									
										vendored
									
									
								
							
							
						
						@@ -19,9 +19,9 @@
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/@codemirror/autocomplete": {
 | 
			
		||||
      "version": "6.18.4",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@codemirror/autocomplete/-/autocomplete-6.18.4.tgz",
 | 
			
		||||
      "integrity": "sha512-sFAphGQIqyQZfP2ZBsSHV7xQvo9Py0rV0dW7W3IMRdS+zDuNb2l3no78CvUaWKGfzFjI4FTrLdUSj86IGb2hRA==",
 | 
			
		||||
      "version": "6.19.1",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@codemirror/autocomplete/-/autocomplete-6.19.1.tgz",
 | 
			
		||||
      "integrity": "sha512-q6NenYkEy2fn9+JyjIxMWcNjzTL/IhwqfzOut1/G3PrIFkrbl4AL7Wkse5tLrQUUyqGoAKU5+Pi5jnnXxH5HGw==",
 | 
			
		||||
      "license": "MIT",
 | 
			
		||||
      "dependencies": {
 | 
			
		||||
        "@codemirror/language": "^6.0.0",
 | 
			
		||||
@@ -31,9 +31,9 @@
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/@codemirror/commands": {
 | 
			
		||||
      "version": "6.7.1",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@codemirror/commands/-/commands-6.7.1.tgz",
 | 
			
		||||
      "integrity": "sha512-llTrboQYw5H4THfhN4U3qCnSZ1SOJ60ohhz+SzU0ADGtwlc533DtklQP0vSFaQuCPDn3BPpOd1GbbnUtwNjsrw==",
 | 
			
		||||
      "version": "6.10.0",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@codemirror/commands/-/commands-6.10.0.tgz",
 | 
			
		||||
      "integrity": "sha512-2xUIc5mHXQzT16JnyOFkh8PvfeXuIut3pslWGfsGOhxP/lpgRm9HOl/mpzLErgt5mXDovqA0d11P21gofRLb9w==",
 | 
			
		||||
      "license": "MIT",
 | 
			
		||||
      "dependencies": {
 | 
			
		||||
        "@codemirror/language": "^6.0.0",
 | 
			
		||||
@@ -56,9 +56,9 @@
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/@codemirror/lang-html": {
 | 
			
		||||
      "version": "6.4.9",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@codemirror/lang-html/-/lang-html-6.4.9.tgz",
 | 
			
		||||
      "integrity": "sha512-aQv37pIMSlueybId/2PVSP6NPnmurFDVmZwzc7jszd2KAF8qd4VBbvNYPXWQq90WIARjsdVkPbw29pszmHws3Q==",
 | 
			
		||||
      "version": "6.4.11",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@codemirror/lang-html/-/lang-html-6.4.11.tgz",
 | 
			
		||||
      "integrity": "sha512-9NsXp7Nwp891pQchI7gPdTwBuSuT3K65NGTHWHNJ55HjYcHLllr0rbIZNdOzas9ztc1EUVBlHou85FFZS4BNnw==",
 | 
			
		||||
      "license": "MIT",
 | 
			
		||||
      "dependencies": {
 | 
			
		||||
        "@codemirror/autocomplete": "^6.0.0",
 | 
			
		||||
@@ -69,13 +69,13 @@
 | 
			
		||||
        "@codemirror/view": "^6.17.0",
 | 
			
		||||
        "@lezer/common": "^1.0.0",
 | 
			
		||||
        "@lezer/css": "^1.1.0",
 | 
			
		||||
        "@lezer/html": "^1.3.0"
 | 
			
		||||
        "@lezer/html": "^1.3.12"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/@codemirror/lang-javascript": {
 | 
			
		||||
      "version": "6.2.2",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@codemirror/lang-javascript/-/lang-javascript-6.2.2.tgz",
 | 
			
		||||
      "integrity": "sha512-VGQfY+FCc285AhWuwjYxQyUQcYurWlxdKYT4bqwr3Twnd5wP5WSeu52t4tvvuWmljT4EmgEgZCqSieokhtY8hg==",
 | 
			
		||||
      "version": "6.2.4",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@codemirror/lang-javascript/-/lang-javascript-6.2.4.tgz",
 | 
			
		||||
      "integrity": "sha512-0WVmhp1QOqZ4Rt6GlVGwKJN3KW7Xh4H2q8ZZNGZaP6lRdxXJzmjm4FqvmOojVj6khWJHIb9sp7U/72W7xQgqAA==",
 | 
			
		||||
      "license": "MIT",
 | 
			
		||||
      "dependencies": {
 | 
			
		||||
        "@codemirror/autocomplete": "^6.0.0",
 | 
			
		||||
@@ -88,9 +88,9 @@
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/@codemirror/lang-json": {
 | 
			
		||||
      "version": "6.0.1",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@codemirror/lang-json/-/lang-json-6.0.1.tgz",
 | 
			
		||||
      "integrity": "sha512-+T1flHdgpqDDlJZ2Lkil/rLiRy684WMLc74xUnjJH48GQdfJo/pudlTRreZmKwzP8/tGdKf83wlbAdOCzlJOGQ==",
 | 
			
		||||
      "version": "6.0.2",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@codemirror/lang-json/-/lang-json-6.0.2.tgz",
 | 
			
		||||
      "integrity": "sha512-x2OtO+AvwEHrEwR0FyyPtfDUiloG3rnVTSZV1W8UteaLL8/MajQd8DpvUb2YVzC+/T18aSDv0H9mu+xw0EStoQ==",
 | 
			
		||||
      "license": "MIT",
 | 
			
		||||
      "dependencies": {
 | 
			
		||||
        "@codemirror/language": "^6.0.0",
 | 
			
		||||
@@ -98,9 +98,9 @@
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/@codemirror/language": {
 | 
			
		||||
      "version": "6.10.7",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.10.7.tgz",
 | 
			
		||||
      "integrity": "sha512-aOswhVOLYhMNeqykt4P7+ukQSpGL0ynZYaEyFDVHE7fl2xgluU3yuE9MdgYNfw6EmaNidoFMIQ2iTh1ADrnT6A==",
 | 
			
		||||
      "version": "6.11.3",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.11.3.tgz",
 | 
			
		||||
      "integrity": "sha512-9HBM2XnwDj7fnu0551HkGdrUrrqmYq/WC5iv6nbY2WdicXdGbhR/gfbZOH73Aqj4351alY1+aoG9rCNfiwS1RA==",
 | 
			
		||||
      "license": "MIT",
 | 
			
		||||
      "dependencies": {
 | 
			
		||||
        "@codemirror/state": "^6.0.0",
 | 
			
		||||
@@ -112,9 +112,9 @@
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/@codemirror/lint": {
 | 
			
		||||
      "version": "6.8.4",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@codemirror/lint/-/lint-6.8.4.tgz",
 | 
			
		||||
      "integrity": "sha512-u4q7PnZlJUojeRe8FJa/njJcMctISGgPQ4PnWsd9268R4ZTtU+tfFYmwkBvgcrK2+QQ8tYFVALVb5fVJykKc5A==",
 | 
			
		||||
      "version": "6.9.1",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@codemirror/lint/-/lint-6.9.1.tgz",
 | 
			
		||||
      "integrity": "sha512-te7To1EQHePBQQzasDKWmK2xKINIXpk+xAiSYr9ZN+VB4KaT+/Hi2PEkeErTk5BV3PTz1TLyQL4MtJfPkKZ9sw==",
 | 
			
		||||
      "license": "MIT",
 | 
			
		||||
      "dependencies": {
 | 
			
		||||
        "@codemirror/state": "^6.0.0",
 | 
			
		||||
@@ -123,9 +123,9 @@
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/@codemirror/search": {
 | 
			
		||||
      "version": "6.5.8",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@codemirror/search/-/search-6.5.8.tgz",
 | 
			
		||||
      "integrity": "sha512-PoWtZvo7c1XFeZWmmyaOp2G0XVbOnm+fJzvghqGAktBW3cufwJUWvSCcNG0ppXiBEM05mZu6RhMtXPv2hpllig==",
 | 
			
		||||
      "version": "6.5.11",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@codemirror/search/-/search-6.5.11.tgz",
 | 
			
		||||
      "integrity": "sha512-KmWepDE6jUdL6n8cAAqIpRmLPBZ5ZKnicE8oGU/s3QrAVID+0VhLFrzUucVKHG5035/BSykhExDL/Xm7dHthiA==",
 | 
			
		||||
      "license": "MIT",
 | 
			
		||||
      "dependencies": {
 | 
			
		||||
        "@codemirror/state": "^6.0.0",
 | 
			
		||||
@@ -134,18 +134,18 @@
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/@codemirror/state": {
 | 
			
		||||
      "version": "6.5.0",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.5.0.tgz",
 | 
			
		||||
      "integrity": "sha512-MwBHVK60IiIHDcoMet78lxt6iw5gJOGSbNbOIVBHWVXIH4/Nq1+GQgLLGgI1KlnN86WDXsPudVaqYHKBIx7Eyw==",
 | 
			
		||||
      "version": "6.5.2",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.5.2.tgz",
 | 
			
		||||
      "integrity": "sha512-FVqsPqtPWKVVL3dPSxy8wEF/ymIEuVzF1PK3VbUgrxXpJUSHQWWZz4JMToquRxnkw+36LTamCZG2iua2Ptq0fA==",
 | 
			
		||||
      "license": "MIT",
 | 
			
		||||
      "dependencies": {
 | 
			
		||||
        "@marijn/find-cluster-break": "^1.0.0"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/@codemirror/theme-one-dark": {
 | 
			
		||||
      "version": "6.1.2",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@codemirror/theme-one-dark/-/theme-one-dark-6.1.2.tgz",
 | 
			
		||||
      "integrity": "sha512-F+sH0X16j/qFLMAfbciKTxVOwkdAS336b7AXTKOZhy8BR3eH/RelsnLgLFINrpST63mmN2OuwUt0W2ndUgYwUA==",
 | 
			
		||||
      "version": "6.1.3",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@codemirror/theme-one-dark/-/theme-one-dark-6.1.3.tgz",
 | 
			
		||||
      "integrity": "sha512-NzBdIvEJmx6fjeremiGp3t/okrLPYT0d9orIc7AFun8oZcRk58aejkqhv6spnz4MLAevrKNPMQYXEWMg4s+sKA==",
 | 
			
		||||
      "license": "MIT",
 | 
			
		||||
      "dependencies": {
 | 
			
		||||
        "@codemirror/language": "^6.0.0",
 | 
			
		||||
@@ -155,29 +155,26 @@
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/@codemirror/view": {
 | 
			
		||||
      "version": "6.36.1",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.36.1.tgz",
 | 
			
		||||
      "integrity": "sha512-miD1nyT4m4uopZaDdO2uXU/LLHliKNYL9kB1C1wJHrunHLm/rpkb5QVSokqgw9hFqEZakrdlb/VGWX8aYZTslQ==",
 | 
			
		||||
      "version": "6.38.6",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.38.6.tgz",
 | 
			
		||||
      "integrity": "sha512-qiS0z1bKs5WOvHIAC0Cybmv4AJSkAXgX5aD6Mqd2epSLlVJsQl8NG23jCVouIgkh4All/mrbdsf2UOLFnJw0tw==",
 | 
			
		||||
      "license": "MIT",
 | 
			
		||||
      "dependencies": {
 | 
			
		||||
        "@codemirror/state": "^6.5.0",
 | 
			
		||||
        "crelt": "^1.0.6",
 | 
			
		||||
        "style-mod": "^4.1.0",
 | 
			
		||||
        "w3c-keyname": "^2.2.4"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/@jridgewell/gen-mapping": {
 | 
			
		||||
      "version": "0.3.8",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz",
 | 
			
		||||
      "integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==",
 | 
			
		||||
      "version": "0.3.13",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
 | 
			
		||||
      "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
 | 
			
		||||
      "dev": true,
 | 
			
		||||
      "license": "MIT",
 | 
			
		||||
      "dependencies": {
 | 
			
		||||
        "@jridgewell/set-array": "^1.2.1",
 | 
			
		||||
        "@jridgewell/sourcemap-codec": "^1.4.10",
 | 
			
		||||
        "@jridgewell/sourcemap-codec": "^1.5.0",
 | 
			
		||||
        "@jridgewell/trace-mapping": "^0.3.24"
 | 
			
		||||
      },
 | 
			
		||||
      "engines": {
 | 
			
		||||
        "node": ">=6.0.0"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/@jridgewell/resolve-uri": {
 | 
			
		||||
@@ -190,20 +187,10 @@
 | 
			
		||||
        "node": ">=6.0.0"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/@jridgewell/set-array": {
 | 
			
		||||
      "version": "1.2.1",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz",
 | 
			
		||||
      "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==",
 | 
			
		||||
      "dev": true,
 | 
			
		||||
      "license": "MIT",
 | 
			
		||||
      "engines": {
 | 
			
		||||
        "node": ">=6.0.0"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/@jridgewell/source-map": {
 | 
			
		||||
      "version": "0.3.6",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.6.tgz",
 | 
			
		||||
      "integrity": "sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ==",
 | 
			
		||||
      "version": "0.3.11",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.11.tgz",
 | 
			
		||||
      "integrity": "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==",
 | 
			
		||||
      "dev": true,
 | 
			
		||||
      "license": "MIT",
 | 
			
		||||
      "dependencies": {
 | 
			
		||||
@@ -212,16 +199,16 @@
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/@jridgewell/sourcemap-codec": {
 | 
			
		||||
      "version": "1.5.0",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz",
 | 
			
		||||
      "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==",
 | 
			
		||||
      "version": "1.5.5",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
 | 
			
		||||
      "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
 | 
			
		||||
      "dev": true,
 | 
			
		||||
      "license": "MIT"
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/@jridgewell/trace-mapping": {
 | 
			
		||||
      "version": "0.3.25",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz",
 | 
			
		||||
      "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==",
 | 
			
		||||
      "version": "0.3.31",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
 | 
			
		||||
      "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
 | 
			
		||||
      "dev": true,
 | 
			
		||||
      "license": "MIT",
 | 
			
		||||
      "dependencies": {
 | 
			
		||||
@@ -230,35 +217,35 @@
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/@lezer/common": {
 | 
			
		||||
      "version": "1.2.3",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.2.3.tgz",
 | 
			
		||||
      "integrity": "sha512-w7ojc8ejBqr2REPsWxJjrMFsA/ysDCFICn8zEOR9mrqzOu2amhITYuLD8ag6XZf0CFXDrhKqw7+tW8cX66NaDA==",
 | 
			
		||||
      "version": "1.3.0",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.3.0.tgz",
 | 
			
		||||
      "integrity": "sha512-L9X8uHCYU310o99L3/MpJKYxPzXPOS7S0NmBaM7UO/x2Kb2WbmMLSkfvdr1KxRIFYOpbY0Jhn7CfLSUDzL8arQ==",
 | 
			
		||||
      "license": "MIT"
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/@lezer/css": {
 | 
			
		||||
      "version": "1.1.9",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@lezer/css/-/css-1.1.9.tgz",
 | 
			
		||||
      "integrity": "sha512-TYwgljcDv+YrV0MZFFvYFQHCfGgbPMR6nuqLabBdmZoFH3EP1gvw8t0vae326Ne3PszQkbXfVBjCnf3ZVCr0bA==",
 | 
			
		||||
      "version": "1.3.0",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@lezer/css/-/css-1.3.0.tgz",
 | 
			
		||||
      "integrity": "sha512-pBL7hup88KbI7hXnZV3PQsn43DHy6TWyzuyk2AO9UyoXcDltvIdqWKE1dLL/45JVZ+YZkHe1WVHqO6wugZZWcw==",
 | 
			
		||||
      "license": "MIT",
 | 
			
		||||
      "dependencies": {
 | 
			
		||||
        "@lezer/common": "^1.2.0",
 | 
			
		||||
        "@lezer/highlight": "^1.0.0",
 | 
			
		||||
        "@lezer/lr": "^1.0.0"
 | 
			
		||||
        "@lezer/lr": "^1.3.0"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/@lezer/highlight": {
 | 
			
		||||
      "version": "1.2.1",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-1.2.1.tgz",
 | 
			
		||||
      "integrity": "sha512-Z5duk4RN/3zuVO7Jq0pGLJ3qynpxUVsh7IbUbGj88+uV2ApSAn6kWg2au3iJb+0Zi7kKtqffIESgNcRXWZWmSA==",
 | 
			
		||||
      "version": "1.2.2",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-1.2.2.tgz",
 | 
			
		||||
      "integrity": "sha512-z8TQwaBXXQIvG6i2g3e9cgMwUUXu9Ib7jo2qRRggdhwKpM56Dw3PM3wmexn+EGaaOZ7az0K7sjc3/gcGW7sz7A==",
 | 
			
		||||
      "license": "MIT",
 | 
			
		||||
      "dependencies": {
 | 
			
		||||
        "@lezer/common": "^1.0.0"
 | 
			
		||||
        "@lezer/common": "^1.3.0"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/@lezer/html": {
 | 
			
		||||
      "version": "1.3.10",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@lezer/html/-/html-1.3.10.tgz",
 | 
			
		||||
      "integrity": "sha512-dqpT8nISx/p9Do3AchvYGV3qYc4/rKr3IBZxlHmpIKam56P47RSHkSF5f13Vu9hebS1jM0HmtJIwLbWz1VIY6w==",
 | 
			
		||||
      "version": "1.3.12",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@lezer/html/-/html-1.3.12.tgz",
 | 
			
		||||
      "integrity": "sha512-RJ7eRWdaJe3bsiiLLHjCFT1JMk8m1YP9kaUbvu2rMLEoOnke9mcTVDyfOslsln0LtujdWespjJ39w6zo+RsQYw==",
 | 
			
		||||
      "license": "MIT",
 | 
			
		||||
      "dependencies": {
 | 
			
		||||
        "@lezer/common": "^1.2.0",
 | 
			
		||||
@@ -267,9 +254,9 @@
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/@lezer/javascript": {
 | 
			
		||||
      "version": "1.4.21",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@lezer/javascript/-/javascript-1.4.21.tgz",
 | 
			
		||||
      "integrity": "sha512-lL+1fcuxWYPURMM/oFZLEDm0XuLN128QPV+VuGtKpeaOGdcl9F2LYC3nh1S9LkPqx9M0mndZFdXCipNAZpzIkQ==",
 | 
			
		||||
      "version": "1.5.4",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@lezer/javascript/-/javascript-1.5.4.tgz",
 | 
			
		||||
      "integrity": "sha512-vvYx3MhWqeZtGPwDStM2dwgljd5smolYD2lR2UyFcHfxbBQebqx8yjmFmxtJ/E6nN6u1D9srOiVWm3Rb4tmcUA==",
 | 
			
		||||
      "license": "MIT",
 | 
			
		||||
      "dependencies": {
 | 
			
		||||
        "@lezer/common": "^1.2.0",
 | 
			
		||||
@@ -278,9 +265,9 @@
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/@lezer/json": {
 | 
			
		||||
      "version": "1.0.2",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@lezer/json/-/json-1.0.2.tgz",
 | 
			
		||||
      "integrity": "sha512-xHT2P4S5eeCYECyKNPhr4cbEL9tc8w83SPwRC373o9uEdrvGKTZoJVAGxpOsZckMlEh9W23Pc72ew918RWQOBQ==",
 | 
			
		||||
      "version": "1.0.3",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@lezer/json/-/json-1.0.3.tgz",
 | 
			
		||||
      "integrity": "sha512-BP9KzdF9Y35PDpv04r0VeSTKDeox5vVr3efE7eBbx3r4s3oNLfunchejZhjArmeieBH+nVOpgIiBJpEAv8ilqQ==",
 | 
			
		||||
      "license": "MIT",
 | 
			
		||||
      "dependencies": {
 | 
			
		||||
        "@lezer/common": "^1.2.0",
 | 
			
		||||
@@ -351,9 +338,9 @@
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/@rollup/pluginutils": {
 | 
			
		||||
      "version": "5.1.4",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.1.4.tgz",
 | 
			
		||||
      "integrity": "sha512-USm05zrsFxYLPdWWq+K3STlWiT/3ELn3RcV5hJMghpeAIhxfsUIg6mt12CBJBInWMV4VneoV7SfGv8xIwo2qNQ==",
 | 
			
		||||
      "version": "5.3.0",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.3.0.tgz",
 | 
			
		||||
      "integrity": "sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q==",
 | 
			
		||||
      "license": "MIT",
 | 
			
		||||
      "dependencies": {
 | 
			
		||||
        "@types/estree": "^1.0.0",
 | 
			
		||||
@@ -373,9 +360,9 @@
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/@rollup/rollup-android-arm-eabi": {
 | 
			
		||||
      "version": "4.29.1",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.29.1.tgz",
 | 
			
		||||
      "integrity": "sha512-ssKhA8RNltTZLpG6/QNkCSge+7mBQGUqJRisZ2MDQcEGaK93QESEgWK2iOpIDZ7k9zPVkG5AS3ksvD5ZWxmItw==",
 | 
			
		||||
      "version": "4.52.5",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.52.5.tgz",
 | 
			
		||||
      "integrity": "sha512-8c1vW4ocv3UOMp9K+gToY5zL2XiiVw3k7f1ksf4yO1FlDFQ1C2u72iACFnSOceJFsWskc2WZNqeRhFRPzv+wtQ==",
 | 
			
		||||
      "cpu": [
 | 
			
		||||
        "arm"
 | 
			
		||||
      ],
 | 
			
		||||
@@ -386,9 +373,9 @@
 | 
			
		||||
      ]
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/@rollup/rollup-android-arm64": {
 | 
			
		||||
      "version": "4.29.1",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.29.1.tgz",
 | 
			
		||||
      "integrity": "sha512-CaRfrV0cd+NIIcVVN/jx+hVLN+VRqnuzLRmfmlzpOzB87ajixsN/+9L5xNmkaUUvEbI5BmIKS+XTwXsHEb65Ew==",
 | 
			
		||||
      "version": "4.52.5",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.52.5.tgz",
 | 
			
		||||
      "integrity": "sha512-mQGfsIEFcu21mvqkEKKu2dYmtuSZOBMmAl5CFlPGLY94Vlcm+zWApK7F/eocsNzp8tKmbeBP8yXyAbx0XHsFNA==",
 | 
			
		||||
      "cpu": [
 | 
			
		||||
        "arm64"
 | 
			
		||||
      ],
 | 
			
		||||
@@ -399,9 +386,9 @@
 | 
			
		||||
      ]
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/@rollup/rollup-darwin-arm64": {
 | 
			
		||||
      "version": "4.29.1",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.29.1.tgz",
 | 
			
		||||
      "integrity": "sha512-2ORr7T31Y0Mnk6qNuwtyNmy14MunTAMx06VAPI6/Ju52W10zk1i7i5U3vlDRWjhOI5quBcrvhkCHyF76bI7kEw==",
 | 
			
		||||
      "version": "4.52.5",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.52.5.tgz",
 | 
			
		||||
      "integrity": "sha512-takF3CR71mCAGA+v794QUZ0b6ZSrgJkArC+gUiG6LB6TQty9T0Mqh3m2ImRBOxS2IeYBo4lKWIieSvnEk2OQWA==",
 | 
			
		||||
      "cpu": [
 | 
			
		||||
        "arm64"
 | 
			
		||||
      ],
 | 
			
		||||
@@ -412,9 +399,9 @@
 | 
			
		||||
      ]
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/@rollup/rollup-darwin-x64": {
 | 
			
		||||
      "version": "4.29.1",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.29.1.tgz",
 | 
			
		||||
      "integrity": "sha512-j/Ej1oanzPjmN0tirRd5K2/nncAhS9W6ICzgxV+9Y5ZsP0hiGhHJXZ2JQ53iSSjj8m6cRY6oB1GMzNn2EUt6Ng==",
 | 
			
		||||
      "version": "4.52.5",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.52.5.tgz",
 | 
			
		||||
      "integrity": "sha512-W901Pla8Ya95WpxDn//VF9K9u2JbocwV/v75TE0YIHNTbhqUTv9w4VuQ9MaWlNOkkEfFwkdNhXgcLqPSmHy0fA==",
 | 
			
		||||
      "cpu": [
 | 
			
		||||
        "x64"
 | 
			
		||||
      ],
 | 
			
		||||
@@ -425,9 +412,9 @@
 | 
			
		||||
      ]
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/@rollup/rollup-freebsd-arm64": {
 | 
			
		||||
      "version": "4.29.1",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.29.1.tgz",
 | 
			
		||||
      "integrity": "sha512-91C//G6Dm/cv724tpt7nTyP+JdN12iqeXGFM1SqnljCmi5yTXriH7B1r8AD9dAZByHpKAumqP1Qy2vVNIdLZqw==",
 | 
			
		||||
      "version": "4.52.5",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.52.5.tgz",
 | 
			
		||||
      "integrity": "sha512-QofO7i7JycsYOWxe0GFqhLmF6l1TqBswJMvICnRUjqCx8b47MTo46W8AoeQwiokAx3zVryVnxtBMcGcnX12LvA==",
 | 
			
		||||
      "cpu": [
 | 
			
		||||
        "arm64"
 | 
			
		||||
      ],
 | 
			
		||||
@@ -438,9 +425,9 @@
 | 
			
		||||
      ]
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/@rollup/rollup-freebsd-x64": {
 | 
			
		||||
      "version": "4.29.1",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.29.1.tgz",
 | 
			
		||||
      "integrity": "sha512-hEioiEQ9Dec2nIRoeHUP6hr1PSkXzQaCUyqBDQ9I9ik4gCXQZjJMIVzoNLBRGet+hIUb3CISMh9KXuCcWVW/8w==",
 | 
			
		||||
      "version": "4.52.5",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.52.5.tgz",
 | 
			
		||||
      "integrity": "sha512-jr21b/99ew8ujZubPo9skbrItHEIE50WdV86cdSoRkKtmWa+DDr6fu2c/xyRT0F/WazZpam6kk7IHBerSL7LDQ==",
 | 
			
		||||
      "cpu": [
 | 
			
		||||
        "x64"
 | 
			
		||||
      ],
 | 
			
		||||
@@ -451,9 +438,9 @@
 | 
			
		||||
      ]
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
 | 
			
		||||
      "version": "4.29.1",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.29.1.tgz",
 | 
			
		||||
      "integrity": "sha512-Py5vFd5HWYN9zxBv3WMrLAXY3yYJ6Q/aVERoeUFwiDGiMOWsMs7FokXihSOaT/PMWUty/Pj60XDQndK3eAfE6A==",
 | 
			
		||||
      "version": "4.52.5",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.52.5.tgz",
 | 
			
		||||
      "integrity": "sha512-PsNAbcyv9CcecAUagQefwX8fQn9LQ4nZkpDboBOttmyffnInRy8R8dSg6hxxl2Re5QhHBf6FYIDhIj5v982ATQ==",
 | 
			
		||||
      "cpu": [
 | 
			
		||||
        "arm"
 | 
			
		||||
      ],
 | 
			
		||||
@@ -464,9 +451,9 @@
 | 
			
		||||
      ]
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/@rollup/rollup-linux-arm-musleabihf": {
 | 
			
		||||
      "version": "4.29.1",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.29.1.tgz",
 | 
			
		||||
      "integrity": "sha512-RiWpGgbayf7LUcuSNIbahr0ys2YnEERD4gYdISA06wa0i8RALrnzflh9Wxii7zQJEB2/Eh74dX4y/sHKLWp5uQ==",
 | 
			
		||||
      "version": "4.52.5",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.52.5.tgz",
 | 
			
		||||
      "integrity": "sha512-Fw4tysRutyQc/wwkmcyoqFtJhh0u31K+Q6jYjeicsGJJ7bbEq8LwPWV/w0cnzOqR2m694/Af6hpFayLJZkG2VQ==",
 | 
			
		||||
      "cpu": [
 | 
			
		||||
        "arm"
 | 
			
		||||
      ],
 | 
			
		||||
@@ -477,9 +464,9 @@
 | 
			
		||||
      ]
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/@rollup/rollup-linux-arm64-gnu": {
 | 
			
		||||
      "version": "4.29.1",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.29.1.tgz",
 | 
			
		||||
      "integrity": "sha512-Z80O+taYxTQITWMjm/YqNoe9d10OX6kDh8X5/rFCMuPqsKsSyDilvfg+vd3iXIqtfmp+cnfL1UrYirkaF8SBZA==",
 | 
			
		||||
      "version": "4.52.5",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.52.5.tgz",
 | 
			
		||||
      "integrity": "sha512-a+3wVnAYdQClOTlyapKmyI6BLPAFYs0JM8HRpgYZQO02rMR09ZcV9LbQB+NL6sljzG38869YqThrRnfPMCDtZg==",
 | 
			
		||||
      "cpu": [
 | 
			
		||||
        "arm64"
 | 
			
		||||
      ],
 | 
			
		||||
@@ -490,9 +477,9 @@
 | 
			
		||||
      ]
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/@rollup/rollup-linux-arm64-musl": {
 | 
			
		||||
      "version": "4.29.1",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.29.1.tgz",
 | 
			
		||||
      "integrity": "sha512-fOHRtF9gahwJk3QVp01a/GqS4hBEZCV1oKglVVq13kcK3NeVlS4BwIFzOHDbmKzt3i0OuHG4zfRP0YoG5OF/rA==",
 | 
			
		||||
      "version": "4.52.5",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.52.5.tgz",
 | 
			
		||||
      "integrity": "sha512-AvttBOMwO9Pcuuf7m9PkC1PUIKsfaAJ4AYhy944qeTJgQOqJYJ9oVl2nYgY7Rk0mkbsuOpCAYSs6wLYB2Xiw0Q==",
 | 
			
		||||
      "cpu": [
 | 
			
		||||
        "arm64"
 | 
			
		||||
      ],
 | 
			
		||||
@@ -502,10 +489,10 @@
 | 
			
		||||
        "linux"
 | 
			
		||||
      ]
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/@rollup/rollup-linux-loongarch64-gnu": {
 | 
			
		||||
      "version": "4.29.1",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.29.1.tgz",
 | 
			
		||||
      "integrity": "sha512-5a7q3tnlbcg0OodyxcAdrrCxFi0DgXJSoOuidFUzHZ2GixZXQs6Tc3CHmlvqKAmOs5eRde+JJxeIf9DonkmYkw==",
 | 
			
		||||
    "node_modules/@rollup/rollup-linux-loong64-gnu": {
 | 
			
		||||
      "version": "4.52.5",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.52.5.tgz",
 | 
			
		||||
      "integrity": "sha512-DkDk8pmXQV2wVrF6oq5tONK6UHLz/XcEVow4JTTerdeV1uqPeHxwcg7aFsfnSm9L+OO8WJsWotKM2JJPMWrQtA==",
 | 
			
		||||
      "cpu": [
 | 
			
		||||
        "loong64"
 | 
			
		||||
      ],
 | 
			
		||||
@@ -515,10 +502,10 @@
 | 
			
		||||
        "linux"
 | 
			
		||||
      ]
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/@rollup/rollup-linux-powerpc64le-gnu": {
 | 
			
		||||
      "version": "4.29.1",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.29.1.tgz",
 | 
			
		||||
      "integrity": "sha512-9b4Mg5Yfz6mRnlSPIdROcfw1BU22FQxmfjlp/CShWwO3LilKQuMISMTtAu/bxmmrE6A902W2cZJuzx8+gJ8e9w==",
 | 
			
		||||
    "node_modules/@rollup/rollup-linux-ppc64-gnu": {
 | 
			
		||||
      "version": "4.52.5",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.52.5.tgz",
 | 
			
		||||
      "integrity": "sha512-W/b9ZN/U9+hPQVvlGwjzi+Wy4xdoH2I8EjaCkMvzpI7wJUs8sWJ03Rq96jRnHkSrcHTpQe8h5Tg3ZzUPGauvAw==",
 | 
			
		||||
      "cpu": [
 | 
			
		||||
        "ppc64"
 | 
			
		||||
      ],
 | 
			
		||||
@@ -529,9 +516,22 @@
 | 
			
		||||
      ]
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/@rollup/rollup-linux-riscv64-gnu": {
 | 
			
		||||
      "version": "4.29.1",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.29.1.tgz",
 | 
			
		||||
      "integrity": "sha512-G5pn0NChlbRM8OJWpJFMX4/i8OEU538uiSv0P6roZcbpe/WfhEO+AT8SHVKfp8qhDQzaz7Q+1/ixMy7hBRidnQ==",
 | 
			
		||||
      "version": "4.52.5",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.52.5.tgz",
 | 
			
		||||
      "integrity": "sha512-sjQLr9BW7R/ZiXnQiWPkErNfLMkkWIoCz7YMn27HldKsADEKa5WYdobaa1hmN6slu9oWQbB6/jFpJ+P2IkVrmw==",
 | 
			
		||||
      "cpu": [
 | 
			
		||||
        "riscv64"
 | 
			
		||||
      ],
 | 
			
		||||
      "license": "MIT",
 | 
			
		||||
      "optional": true,
 | 
			
		||||
      "os": [
 | 
			
		||||
        "linux"
 | 
			
		||||
      ]
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/@rollup/rollup-linux-riscv64-musl": {
 | 
			
		||||
      "version": "4.52.5",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.52.5.tgz",
 | 
			
		||||
      "integrity": "sha512-hq3jU/kGyjXWTvAh2awn8oHroCbrPm8JqM7RUpKjalIRWWXE01CQOf/tUNWNHjmbMHg/hmNCwc/Pz3k1T/j/Lg==",
 | 
			
		||||
      "cpu": [
 | 
			
		||||
        "riscv64"
 | 
			
		||||
      ],
 | 
			
		||||
@@ -542,9 +542,9 @@
 | 
			
		||||
      ]
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/@rollup/rollup-linux-s390x-gnu": {
 | 
			
		||||
      "version": "4.29.1",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.29.1.tgz",
 | 
			
		||||
      "integrity": "sha512-WM9lIkNdkhVwiArmLxFXpWndFGuOka4oJOZh8EP3Vb8q5lzdSCBuhjavJsw68Q9AKDGeOOIHYzYm4ZFvmWez5g==",
 | 
			
		||||
      "version": "4.52.5",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.52.5.tgz",
 | 
			
		||||
      "integrity": "sha512-gn8kHOrku8D4NGHMK1Y7NA7INQTRdVOntt1OCYypZPRt6skGbddska44K8iocdpxHTMMNui5oH4elPH4QOLrFQ==",
 | 
			
		||||
      "cpu": [
 | 
			
		||||
        "s390x"
 | 
			
		||||
      ],
 | 
			
		||||
@@ -555,9 +555,9 @@
 | 
			
		||||
      ]
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/@rollup/rollup-linux-x64-gnu": {
 | 
			
		||||
      "version": "4.29.1",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.29.1.tgz",
 | 
			
		||||
      "integrity": "sha512-87xYCwb0cPGZFoGiErT1eDcssByaLX4fc0z2nRM6eMtV9njAfEE6OW3UniAoDhX4Iq5xQVpE6qO9aJbCFumKYQ==",
 | 
			
		||||
      "version": "4.52.5",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.52.5.tgz",
 | 
			
		||||
      "integrity": "sha512-hXGLYpdhiNElzN770+H2nlx+jRog8TyynpTVzdlc6bndktjKWyZyiCsuDAlpd+j+W+WNqfcyAWz9HxxIGfZm1Q==",
 | 
			
		||||
      "cpu": [
 | 
			
		||||
        "x64"
 | 
			
		||||
      ],
 | 
			
		||||
@@ -568,9 +568,9 @@
 | 
			
		||||
      ]
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/@rollup/rollup-linux-x64-musl": {
 | 
			
		||||
      "version": "4.29.1",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.29.1.tgz",
 | 
			
		||||
      "integrity": "sha512-xufkSNppNOdVRCEC4WKvlR1FBDyqCSCpQeMMgv9ZyXqqtKBfkw1yfGMTUTs9Qsl6WQbJnsGboWCp7pJGkeMhKA==",
 | 
			
		||||
      "version": "4.52.5",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.52.5.tgz",
 | 
			
		||||
      "integrity": "sha512-arCGIcuNKjBoKAXD+y7XomR9gY6Mw7HnFBv5Rw7wQRvwYLR7gBAgV7Mb2QTyjXfTveBNFAtPt46/36vV9STLNg==",
 | 
			
		||||
      "cpu": [
 | 
			
		||||
        "x64"
 | 
			
		||||
      ],
 | 
			
		||||
@@ -580,10 +580,23 @@
 | 
			
		||||
        "linux"
 | 
			
		||||
      ]
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/@rollup/rollup-openharmony-arm64": {
 | 
			
		||||
      "version": "4.52.5",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.52.5.tgz",
 | 
			
		||||
      "integrity": "sha512-QoFqB6+/9Rly/RiPjaomPLmR/13cgkIGfA40LHly9zcH1S0bN2HVFYk3a1eAyHQyjs3ZJYlXvIGtcCs5tko9Cw==",
 | 
			
		||||
      "cpu": [
 | 
			
		||||
        "arm64"
 | 
			
		||||
      ],
 | 
			
		||||
      "license": "MIT",
 | 
			
		||||
      "optional": true,
 | 
			
		||||
      "os": [
 | 
			
		||||
        "openharmony"
 | 
			
		||||
      ]
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/@rollup/rollup-win32-arm64-msvc": {
 | 
			
		||||
      "version": "4.29.1",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.29.1.tgz",
 | 
			
		||||
      "integrity": "sha512-F2OiJ42m77lSkizZQLuC+jiZ2cgueWQL5YC9tjo3AgaEw+KJmVxHGSyQfDUoYR9cci0lAywv2Clmckzulcq6ig==",
 | 
			
		||||
      "version": "4.52.5",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.52.5.tgz",
 | 
			
		||||
      "integrity": "sha512-w0cDWVR6MlTstla1cIfOGyl8+qb93FlAVutcor14Gf5Md5ap5ySfQ7R9S/NjNaMLSFdUnKGEasmVnu3lCMqB7w==",
 | 
			
		||||
      "cpu": [
 | 
			
		||||
        "arm64"
 | 
			
		||||
      ],
 | 
			
		||||
@@ -594,9 +607,9 @@
 | 
			
		||||
      ]
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/@rollup/rollup-win32-ia32-msvc": {
 | 
			
		||||
      "version": "4.29.1",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.29.1.tgz",
 | 
			
		||||
      "integrity": "sha512-rYRe5S0FcjlOBZQHgbTKNrqxCBUmgDJem/VQTCcTnA2KCabYSWQDrytOzX7avb79cAAweNmMUb/Zw18RNd4mng==",
 | 
			
		||||
      "version": "4.52.5",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.52.5.tgz",
 | 
			
		||||
      "integrity": "sha512-Aufdpzp7DpOTULJCuvzqcItSGDH73pF3ko/f+ckJhxQyHtp67rHw3HMNxoIdDMUITJESNE6a8uh4Lo4SLouOUg==",
 | 
			
		||||
      "cpu": [
 | 
			
		||||
        "ia32"
 | 
			
		||||
      ],
 | 
			
		||||
@@ -606,10 +619,23 @@
 | 
			
		||||
        "win32"
 | 
			
		||||
      ]
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/@rollup/rollup-win32-x64-gnu": {
 | 
			
		||||
      "version": "4.52.5",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.52.5.tgz",
 | 
			
		||||
      "integrity": "sha512-UGBUGPFp1vkj6p8wCRraqNhqwX/4kNQPS57BCFc8wYh0g94iVIW33wJtQAx3G7vrjjNtRaxiMUylM0ktp/TRSQ==",
 | 
			
		||||
      "cpu": [
 | 
			
		||||
        "x64"
 | 
			
		||||
      ],
 | 
			
		||||
      "license": "MIT",
 | 
			
		||||
      "optional": true,
 | 
			
		||||
      "os": [
 | 
			
		||||
        "win32"
 | 
			
		||||
      ]
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/@rollup/rollup-win32-x64-msvc": {
 | 
			
		||||
      "version": "4.29.1",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.29.1.tgz",
 | 
			
		||||
      "integrity": "sha512-+10CMg9vt1MoHj6x1pxyjPSMjHTIlqs8/tBztXvPAx24SKs9jwVnKqHJumlH/IzhaPUaj3T6T6wfZr8okdXaIg==",
 | 
			
		||||
      "version": "4.52.5",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.52.5.tgz",
 | 
			
		||||
      "integrity": "sha512-TAcgQh2sSkykPRWLrdyy2AiceMckNf5loITqXxFI5VuQjS5tSuw3WlwdN8qv8vzjLAUTvYaH/mVjSFpbkFbpTg==",
 | 
			
		||||
      "cpu": [
 | 
			
		||||
        "x64"
 | 
			
		||||
      ],
 | 
			
		||||
@@ -620,9 +646,9 @@
 | 
			
		||||
      ]
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/@types/estree": {
 | 
			
		||||
      "version": "1.0.6",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz",
 | 
			
		||||
      "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==",
 | 
			
		||||
      "version": "1.0.8",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
 | 
			
		||||
      "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
 | 
			
		||||
      "license": "MIT"
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/@types/resolve": {
 | 
			
		||||
@@ -632,9 +658,9 @@
 | 
			
		||||
      "license": "MIT"
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/acorn": {
 | 
			
		||||
      "version": "8.14.0",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz",
 | 
			
		||||
      "integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==",
 | 
			
		||||
      "version": "8.15.0",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
 | 
			
		||||
      "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
 | 
			
		||||
      "dev": true,
 | 
			
		||||
      "license": "MIT",
 | 
			
		||||
      "bin": {
 | 
			
		||||
@@ -652,9 +678,9 @@
 | 
			
		||||
      "license": "MIT"
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/codemirror": {
 | 
			
		||||
      "version": "6.0.1",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/codemirror/-/codemirror-6.0.1.tgz",
 | 
			
		||||
      "integrity": "sha512-J8j+nZ+CdWmIeFIGXEFbFPtpiYacFMDR8GlHK3IyHQJMCaVRfGx9NT+Hxivv1ckLWPvNdZqndbr/7lVhrf/Svg==",
 | 
			
		||||
      "version": "6.0.2",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/codemirror/-/codemirror-6.0.2.tgz",
 | 
			
		||||
      "integrity": "sha512-VhydHotNW5w1UGK0Qj96BwSk/Zqbp9WbnyK2W/eVMv4QyF41INRGpjUhFJY7/uDNuudSc33a/PKr4iDqRduvHw==",
 | 
			
		||||
      "license": "MIT",
 | 
			
		||||
      "dependencies": {
 | 
			
		||||
        "@codemirror/autocomplete": "^6.0.0",
 | 
			
		||||
@@ -757,9 +783,9 @@
 | 
			
		||||
      "license": "MIT"
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/picomatch": {
 | 
			
		||||
      "version": "4.0.2",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz",
 | 
			
		||||
      "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==",
 | 
			
		||||
      "version": "4.0.3",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
 | 
			
		||||
      "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
 | 
			
		||||
      "license": "MIT",
 | 
			
		||||
      "engines": {
 | 
			
		||||
        "node": ">=12"
 | 
			
		||||
@@ -779,12 +805,12 @@
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/resolve": {
 | 
			
		||||
      "version": "1.22.10",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz",
 | 
			
		||||
      "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==",
 | 
			
		||||
      "version": "1.22.11",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz",
 | 
			
		||||
      "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==",
 | 
			
		||||
      "license": "MIT",
 | 
			
		||||
      "dependencies": {
 | 
			
		||||
        "is-core-module": "^2.16.0",
 | 
			
		||||
        "is-core-module": "^2.16.1",
 | 
			
		||||
        "path-parse": "^1.0.7",
 | 
			
		||||
        "supports-preserve-symlinks-flag": "^1.0.0"
 | 
			
		||||
      },
 | 
			
		||||
@@ -799,12 +825,12 @@
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/rollup": {
 | 
			
		||||
      "version": "4.29.1",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.29.1.tgz",
 | 
			
		||||
      "integrity": "sha512-RaJ45M/kmJUzSWDs1Nnd5DdV4eerC98idtUOVr6FfKcgxqvjwHmxc5upLF9qZU9EpsVzzhleFahrT3shLuJzIw==",
 | 
			
		||||
      "version": "4.52.5",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.52.5.tgz",
 | 
			
		||||
      "integrity": "sha512-3GuObel8h7Kqdjt0gxkEzaifHTqLVW56Y/bjN7PSQtkKr0w3V/QYSdt6QWYtd7A1xUtYQigtdUfgj1RvWVtorw==",
 | 
			
		||||
      "license": "MIT",
 | 
			
		||||
      "dependencies": {
 | 
			
		||||
        "@types/estree": "1.0.6"
 | 
			
		||||
        "@types/estree": "1.0.8"
 | 
			
		||||
      },
 | 
			
		||||
      "bin": {
 | 
			
		||||
        "rollup": "dist/bin/rollup"
 | 
			
		||||
@@ -814,25 +840,28 @@
 | 
			
		||||
        "npm": ">=8.0.0"
 | 
			
		||||
      },
 | 
			
		||||
      "optionalDependencies": {
 | 
			
		||||
        "@rollup/rollup-android-arm-eabi": "4.29.1",
 | 
			
		||||
        "@rollup/rollup-android-arm64": "4.29.1",
 | 
			
		||||
        "@rollup/rollup-darwin-arm64": "4.29.1",
 | 
			
		||||
        "@rollup/rollup-darwin-x64": "4.29.1",
 | 
			
		||||
        "@rollup/rollup-freebsd-arm64": "4.29.1",
 | 
			
		||||
        "@rollup/rollup-freebsd-x64": "4.29.1",
 | 
			
		||||
        "@rollup/rollup-linux-arm-gnueabihf": "4.29.1",
 | 
			
		||||
        "@rollup/rollup-linux-arm-musleabihf": "4.29.1",
 | 
			
		||||
        "@rollup/rollup-linux-arm64-gnu": "4.29.1",
 | 
			
		||||
        "@rollup/rollup-linux-arm64-musl": "4.29.1",
 | 
			
		||||
        "@rollup/rollup-linux-loongarch64-gnu": "4.29.1",
 | 
			
		||||
        "@rollup/rollup-linux-powerpc64le-gnu": "4.29.1",
 | 
			
		||||
        "@rollup/rollup-linux-riscv64-gnu": "4.29.1",
 | 
			
		||||
        "@rollup/rollup-linux-s390x-gnu": "4.29.1",
 | 
			
		||||
        "@rollup/rollup-linux-x64-gnu": "4.29.1",
 | 
			
		||||
        "@rollup/rollup-linux-x64-musl": "4.29.1",
 | 
			
		||||
        "@rollup/rollup-win32-arm64-msvc": "4.29.1",
 | 
			
		||||
        "@rollup/rollup-win32-ia32-msvc": "4.29.1",
 | 
			
		||||
        "@rollup/rollup-win32-x64-msvc": "4.29.1",
 | 
			
		||||
        "@rollup/rollup-android-arm-eabi": "4.52.5",
 | 
			
		||||
        "@rollup/rollup-android-arm64": "4.52.5",
 | 
			
		||||
        "@rollup/rollup-darwin-arm64": "4.52.5",
 | 
			
		||||
        "@rollup/rollup-darwin-x64": "4.52.5",
 | 
			
		||||
        "@rollup/rollup-freebsd-arm64": "4.52.5",
 | 
			
		||||
        "@rollup/rollup-freebsd-x64": "4.52.5",
 | 
			
		||||
        "@rollup/rollup-linux-arm-gnueabihf": "4.52.5",
 | 
			
		||||
        "@rollup/rollup-linux-arm-musleabihf": "4.52.5",
 | 
			
		||||
        "@rollup/rollup-linux-arm64-gnu": "4.52.5",
 | 
			
		||||
        "@rollup/rollup-linux-arm64-musl": "4.52.5",
 | 
			
		||||
        "@rollup/rollup-linux-loong64-gnu": "4.52.5",
 | 
			
		||||
        "@rollup/rollup-linux-ppc64-gnu": "4.52.5",
 | 
			
		||||
        "@rollup/rollup-linux-riscv64-gnu": "4.52.5",
 | 
			
		||||
        "@rollup/rollup-linux-riscv64-musl": "4.52.5",
 | 
			
		||||
        "@rollup/rollup-linux-s390x-gnu": "4.52.5",
 | 
			
		||||
        "@rollup/rollup-linux-x64-gnu": "4.52.5",
 | 
			
		||||
        "@rollup/rollup-linux-x64-musl": "4.52.5",
 | 
			
		||||
        "@rollup/rollup-openharmony-arm64": "4.52.5",
 | 
			
		||||
        "@rollup/rollup-win32-arm64-msvc": "4.52.5",
 | 
			
		||||
        "@rollup/rollup-win32-ia32-msvc": "4.52.5",
 | 
			
		||||
        "@rollup/rollup-win32-x64-gnu": "4.52.5",
 | 
			
		||||
        "@rollup/rollup-win32-x64-msvc": "4.52.5",
 | 
			
		||||
        "fsevents": "~2.3.2"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
@@ -896,9 +925,9 @@
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/style-mod": {
 | 
			
		||||
      "version": "4.1.2",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.1.2.tgz",
 | 
			
		||||
      "integrity": "sha512-wnD1HyVqpJUI2+eKZ+eo1UwghftP6yuFheBqqe+bWCotBjC2K1YnteJILRMs3SM4V/0dLEW1SC27MWP5y+mwmw==",
 | 
			
		||||
      "version": "4.1.3",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.1.3.tgz",
 | 
			
		||||
      "integrity": "sha512-i/n8VsZydrugj3Iuzll8+x/00GH2vnYsk1eomD8QiRrSAeW6ItbCQDtfXCeJHd0iwiNagqjQkvpvREEPtW3IoQ==",
 | 
			
		||||
      "license": "MIT"
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/supports-preserve-symlinks-flag": {
 | 
			
		||||
@@ -914,14 +943,14 @@
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/terser": {
 | 
			
		||||
      "version": "5.37.0",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/terser/-/terser-5.37.0.tgz",
 | 
			
		||||
      "integrity": "sha512-B8wRRkmre4ERucLM/uXx4MOV5cbnOlVAqUst+1+iLKPI0dOgFO28f84ptoQt9HEI537PMzfYa/d+GEPKTRXmYA==",
 | 
			
		||||
      "version": "5.44.0",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/terser/-/terser-5.44.0.tgz",
 | 
			
		||||
      "integrity": "sha512-nIVck8DK+GM/0Frwd+nIhZ84pR/BX7rmXMfYwyg+Sri5oGVE99/E3KvXqpC2xHFxyqXyGHTKBSioxxplrO4I4w==",
 | 
			
		||||
      "dev": true,
 | 
			
		||||
      "license": "BSD-2-Clause",
 | 
			
		||||
      "dependencies": {
 | 
			
		||||
        "@jridgewell/source-map": "^0.3.3",
 | 
			
		||||
        "acorn": "^8.8.2",
 | 
			
		||||
        "acorn": "^8.15.0",
 | 
			
		||||
        "commander": "^2.20.0",
 | 
			
		||||
        "source-map-support": "~0.5.20"
 | 
			
		||||
      },
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										2
									
								
								deps/libbacktrace
									
									
									
									
										vendored
									
									
								
							
							
								
								
								
								
								
							
						
						
							
								
								
									
										2
									
								
								deps/libuv
									
									
									
									
										vendored
									
									
								
							
							
								
								
								
								
								
							
						
						
							
								
								
									
										42
									
								
								deps/lit/lit-all.min.js
									
									
									
									
										vendored
									
									
								
							
							
						
						
							
								
								
									
										2
									
								
								deps/lit/lit-all.min.js.map
									
									
									
									
										vendored
									
									
								
							
							
						
						
							
								
								
									
										1
									
								
								deps/openssl_src
									
									
									
									
										vendored
									
									
								
							
							
								
								
								
								
								
							
						
						
							
								
								
									
										2
									
								
								deps/picohttpparser
									
									
									
									
										vendored
									
									
								
							
							
								
								
								
								
								
							
						
						
							
								
								
									
										2
									
								
								deps/quickjs
									
									
									
									
										vendored
									
									
								
							
							
								
								
								
								
								
							
						
						
							
								
								
									
										4
									
								
								deps/speedscope/demangle-cpp.1768f4cc.js
									
									
									
									
										vendored
									
									
								
							
							
						
						
							
								
								
									
										2
									
								
								deps/speedscope/favicon-16x16-V2DMIAZS.js
									
									
									
									
										vendored
									
									
										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
									
								
								deps/speedscope/favicon-32x32-THY3JDJL.js
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,2 @@
 | 
			
		||||
(()=>{var T="./favicon-32x32-3EB2YCUY.png";})();
 | 
			
		||||
//# sourceMappingURL=favicon-32x32-THY3JDJL.js.map
 | 
			
		||||
							
								
								
									
										
											BIN
										
									
								
								deps/speedscope/favicon-FOKUP5Y5.ico
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 15 KiB  | 
							
								
								
									
										2
									
								
								deps/speedscope/favicon-M34RF7BI.js
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,2 @@
 | 
			
		||||
(()=>{var m="./favicon-FOKUP5Y5.ico";})();
 | 
			
		||||
//# sourceMappingURL=favicon-M34RF7BI.js.map
 | 
			
		||||