Compare commits
	
		
			884 Commits
		
	
	
		
			v0.0.5
			...
			c5140ee8e8
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| c5140ee8e8 | |||
| 6270fd8118 | |||
| 3fff706848 | |||
| c259defab5 | |||
| e5fee5c306 | |||
| 9d35b4bdfb | |||
| 9497d7cf64 | |||
| c7d3e602cb | |||
| 0076eb4ed4 | |||
| 6070bde413 | |||
| c7a6d426f0 | |||
| f66cf0f802 | |||
| e4b6c81024 | |||
| 44d784cd04 | |||
| 0394201113 | |||
| e270c16516 | |||
| 
						
						
							
						
						4c10538632
	
				 | 
					
					
						|||
| 71329c5532 | |||
| feb4bf9e87 | |||
| 5d5567e94c | |||
| 684e6fb9cb | |||
| 
						
						
							
						
						ee21fa6d03
	
				 | 
					
					
						|||
| 7a2974e54f | |||
| f4dfc1dd98 | |||
| 2eebfa9a7a | |||
| 10097ffeb8 | |||
| cbe1f54a2a | |||
| 4d8f081a59 | |||
| 29e79c9484 | |||
| ba35869b0a | |||
| 580688381e | |||
| e63d69a440 | |||
| be64fe04fb | |||
| 801ab20723 | |||
| d974a5e044 | |||
| 1be94ae0be | |||
| b883e6a485 | |||
| a0210379ae | |||
| e56dc207d1 | |||
| 523c9c9ad2 | |||
| 74bb2151c1 | |||
| f79d7b35a4 | |||
| 
						
						
							
						
						3b36496dac
	
				 | 
					
					
						|||
| 
						
						
							
						
						4ebd6c24a9
	
				 | 
					
					
						|||
| 
						
						
							
						
						05451d98b3
	
				 | 
					
					
						|||
| 
						
						
							
						
						22a4bce3c8
	
				 | 
					
					
						|||
| 76d499f00b | |||
| 
						
						
							
						
						f0772f9b99
	
				 | 
					
					
						|||
| 46e711f0a5 | |||
| abffac3f82 | |||
| 27b275548e | |||
| 93ce253d1e | |||
| a5af312b39 | |||
| 4b5e8e8a43 | |||
| 443dd4d168 | |||
| 907479df84 | |||
| 9887a78e98 | |||
| f669371349 | |||
| 24c720c79a | |||
| 
						
						
							
						
						4485234980
	
				 | 
					
					
						|||
| 
						
						
							
						
						b6871c0b1f
	
				 | 
					
					
						|||
| 47838d5e48 | |||
| 69fccd56d3 | |||
| ca00c4fb5d | |||
| 427ca3f265 | |||
| c1a80e50e7 | |||
| 52962f3a5e | |||
| b3f095b61f | |||
| a5004c8ba9 | |||
| 7d9b1b508b | |||
| 5e265dfc83 | |||
| 3a43d6f8ac | |||
| 11a6649847 | |||
| 7caf4a0173 | |||
| 385524352c | |||
| 5ca5323782 | |||
| ba6da856bb | |||
| c0e72246cc | |||
| c7ab5447ea | |||
| 5fdd461159 | |||
| 421955f2a0 | |||
| a28f6985ed | |||
| 8244dddab7 | |||
| a5ca436eaa | |||
| d7fc1c2c88 | |||
| 382627ef8d | |||
| 17667b4cf8 | |||
| 5231ec22e7 | |||
| 929ae1b709 | |||
| f01f7a5ab9 | |||
| a2dce833f8 | |||
| de6c7a4fd4 | |||
| 4edee0f7f6 | |||
| 988a807fa4 | |||
| 5258e4253d | |||
| 09ba86dec5 | |||
| 78d8a1aa23 | |||
| 22def15209 | |||
| 4cbda7a849 | |||
| be85a620ef | |||
| 0b07b678b4 | |||
| 4733ce9287 | |||
| 48d6bf4c15 | |||
| 8c759bcbac | |||
| b5ed7014f6 | |||
| 6cd9dea186 | |||
| 202b416acf | |||
| 93d46f5610 | |||
| c5ddf3ac99 | |||
| a9cb913a47 | |||
| b7b5d4f1a5 | |||
| a947396bad | |||
| d528bc808e | |||
| c6fd05c2cf | |||
| d6bb9d311a | |||
| 53b4cbbf8c | |||
| 628716ec28 | |||
| bd14168627 | |||
| 96037d4da6 | |||
| 5448e773d8 | |||
| 848ef21c7c | |||
| 2ecae7da93 | |||
| d9ce569eb9 | |||
| eacaf392b1 | |||
| ce16592b6a | |||
| 295d76d354 | |||
| 23b3c998bd | |||
| b5e966c9a1 | |||
| 96cb6f4b12 | |||
| e2c0f82ec0 | |||
| dbf28c03e6 | |||
| 26165e30de | |||
| c52331a23a | |||
| 8007e71e1d | |||
| 28d08e013f | |||
| 64bbd383de | |||
| 8a9f53102b | |||
| 0412b97170 | |||
| c8b8a8fc03 | |||
| 95d3090b9b | |||
| 49129ee6dd | |||
| 6a7ecb0d4a | |||
| 1ceeed1007 | |||
| a7922ff44e | |||
| a421604ed5 | |||
| 7d182db32f | |||
| c5cb9979d3 | |||
| b9a73106ed | |||
| c674cca482 | |||
| 81d1228b92 | |||
| 6ae61d5b81 | |||
| 9cb872eec2 | |||
| 68e8c010b7 | |||
| 9671413906 | |||
| 4c8d24c319 | |||
| e50144bd34 | |||
| 9f3171e3f1 | |||
| cc92748747 | |||
| 0a0b0c1adb | |||
| 92a74026a6 | |||
| 3fa1c6c420 | |||
| b04eccdbda | |||
| 9ce30dee70 | |||
| 3c0b680b8e | |||
| 895356897b | |||
| 9164be2f37 | |||
| 5385264f94 | |||
| 610e756c07 | |||
| 15c9f8f458 | |||
| fb704a5b83 | |||
| fdda628be8 | |||
| 2b45d8aa05 | |||
| 0e2fc65301 | |||
| e8ef7e74de | |||
| c32e1b9583 | |||
| 9d0f6ec155 | |||
| 855d603795 | |||
| af25782185 | |||
| e5ba51b80a | |||
| 5e240de677 | |||
| 418cfac0e3 | |||
| 9d09607013 | |||
| eddf25b622 | |||
| 537a8654fa | |||
| 9de33d06d2 | |||
| 0e5f320664 | |||
| 88d8e60511 | |||
| 439f07162e | |||
| efe2b6cbd9 | |||
| 0aa1ed9464 | |||
| cb94ed6a2a | |||
| cf187ee46b | |||
| 3e71fc20fd | |||
| f3601321f7 | |||
| 540059368c | |||
| 7ce89123f7 | |||
| e3c7c86212 | |||
| 794804e27f | |||
| 6d89c1da6e | |||
| d059554464 | |||
| 3a392d4a9f | |||
| e3071b372a | |||
| 18bd279b0c | |||
| 5b93db7463 | |||
| 5b7e5eb91b | |||
| 78ca383e3c | |||
| c1eed9ada3 | |||
| 8d6feb5394 | |||
| 42994f8977 | |||
| f0a871e1f8 | |||
| a710c30572 | |||
| c991763b00 | |||
| 72dae14f87 | |||
| 5800340762 | |||
| c5f5adcac6 | |||
| 591642efb3 | |||
| 6182ffa1d4 | |||
| 402a898d96 | |||
| 13d43d8319 | |||
| 7bcdbd3813 | |||
| 60ada22674 | |||
| 637119d46d | |||
| 40f3da6a65 | |||
| f4697fe7f7 | |||
| 3bc18b9021 | |||
| c21581aefa | |||
| 165f25db69 | |||
| 9aa0617aa1 | |||
| ddce88dce6 | |||
| 6aa2bce2be | |||
| a43c1d3d1e | |||
| 1ed0e817e8 | |||
| 709ca55e65 | |||
| 8c13f5dbba | |||
| 4cb82d81b7 | |||
| 0c42921387 | |||
| 70a3e7fc7d | |||
| d5267be38c | |||
| 8e7e0ed490 | |||
| 8cf2837725 | |||
| 63ae186c76 | |||
| dbf5c7b832 | |||
| bfbfc01e99 | |||
| 8fa9d0e843 | |||
| 2d3e108fd9 | |||
| 7822b30dcb | |||
| 2701b7d04e | |||
| e361c3f975 | |||
| 260706c172 | |||
| 390668ec34 | |||
| 1d5cdf9607 | |||
| a4bf3542e0 | |||
| df82cfe66b | |||
| f23414adaf | |||
| 41024ddb79 | |||
| 53f9547cc5 | |||
| 4bfd9de100 | |||
| c01e00d77d | |||
| 825191c08f | |||
| 9dc6670795 | |||
| 1db8eee9f7 | |||
| 1bc50cb62c | |||
| 450b07fd08 | |||
| 12c7515ee8 | |||
| ed65da4340 | |||
| d9d2917cf5 | |||
| ce5ca1875b | |||
| 4f869252a2 | |||
| 17b92126de | |||
| 6e88c44229 | |||
| 6c3d338c12 | |||
| 4ebd44cb4e | |||
| 75cb9f7fd2 | |||
| eacca9d2ab | |||
| d0e11bc68b | |||
| 1958623a7a | |||
| 498d8b6520 | |||
| a12f2fec5a | |||
| 22bf046643 | |||
| dca48fae36 | |||
| 9af4068bb6 | |||
| 2992d8ec12 | |||
| 33dd2560e0 | |||
| aeb5c6ee25 | |||
| 08a2436b8f | |||
| fbc3cfeda4 | |||
| c8812b1add | |||
| 8d82e80639 | |||
| ed741d53d7 | |||
| 685754895b | |||
| e7791d38ff | |||
| 9f14653001 | |||
| 6c5a7b0751 | |||
| 51a327c52d | |||
| 5a978bb30d | |||
| 6801758cb3 | |||
| 14de3dd9e5 | |||
| ed2d57fb4b | |||
| e87acc6286 | |||
| 0de932bc9e | |||
| d021d9f757 | |||
| eb5da26004 | |||
| 6765254f43 | |||
| e98802f5b2 | |||
| af54b6483e | |||
| 96167c3167 | |||
| eecfdf482f | |||
| 7ceb865206 | |||
| b919670706 | |||
| 72120b8842 | |||
| 1e53c08d9d | |||
| 2d1b6a09e9 | |||
| 7f661d9af9 | |||
| 81c66bdddd | |||
| 4bd46a1657 | |||
| 244a752ae1 | |||
| 72369ab745 | |||
| b62a05f627 | |||
| 03eb8e7fae | |||
| a5a00b6987 | |||
| 542162c78a | |||
| 8cfe0fb7d2 | |||
| 49c831cb62 | |||
| 2c79e03094 | |||
| 21e6cf10b6 | |||
| dc655bb359 | |||
| b9987580ee | |||
| cb2dfc696d | |||
| 7f0643f9c0 | |||
| 14a4117aff | |||
| 55fb5dce1a | |||
| 923d6f9835 | |||
| 08b5ade8ec | |||
| 91f41c7497 | |||
| fa06282ff9 | |||
| 48b967f5b6 | |||
| f479165aac | |||
| 2f83ecc1ac | |||
| 01efc215fd | |||
| 00ba74a6c4 | |||
| 44b5ba1a9a | |||
| 843e172e56 | |||
| a0df336abe | |||
| 0db4bb06c9 | |||
| ad2b49b838 | |||
| ab519342e8 | |||
| 1f0b9012e3 | |||
| 1ddad6be93 | |||
| cf311003c0 | |||
| 64249976a8 | |||
| 6ecb3ccd08 | |||
| 4867bacb72 | |||
| 7d029d3d7a | |||
| 455befc18f | |||
| 6e57845512 | |||
| 1335a6e1e5 | |||
| 1eab44464c | |||
| c3e2da3d51 | |||
| 1716f71c12 | |||
| b52e99c958 | |||
| 86531bfd7e | |||
| 874a22325e | |||
| 2380b65853 | |||
| f72e8cbd91 | |||
| 24e418344e | |||
| 2b7077ca70 | |||
| 10d438e723 | |||
| 331846ee2e | |||
| dc0e58afc1 | |||
| 18e9252998 | |||
| b2e3c04036 | |||
| 4fd155e68a | |||
| 59ac0b5f20 | |||
| f4979c841a | |||
| 74eb74deb1 | |||
| 9e5e7b70d4 | |||
| 2384fc9fa9 | |||
| 576e58b1e3 | |||
| a0af058f5e | |||
| b40457d774 | |||
| 2353b43514 | |||
| b11d5192c2 | |||
| d38c58ce1d | |||
| a0f390b7dc | |||
| cb12799111 | |||
| 86fb5c53a1 | |||
| 29fc728509 | |||
| 0fb341f378 | |||
| 8a1a182479 | |||
| 49907bc8ee | |||
| 21d4a9b328 | |||
| d5ede43a13 | |||
| b73f5011cf | |||
| 32ebfa78cd | |||
| 39c942a205 | |||
| ebc4533b10 | |||
| 4e5f9c86a8 | |||
| d89a7a5556 | |||
| 8ab53f2da3 | |||
| 4c8eab2692 | |||
| 08989f54d9 | |||
| c78753f3ff | |||
| 34a87d8b3b | |||
| 7516524d69 | |||
| 549d7ffec8 | |||
| ccafc23d3c | |||
| 709b57d84f | |||
| 9ef909c9a1 | |||
| d7c0ffaac4 | |||
| e4cd5312f1 | |||
| 197fca6d3b | |||
| 04af1f0053 | |||
| 93d9b1ed93 | |||
| 2d73116bc0 | |||
| f2f6d78790 | |||
| 797509fc11 | |||
| 6920504762 | |||
| 9d1476a760 | |||
| c1890775dc | |||
| 72e5fe5b8f | |||
| c81ec214e2 | |||
| 0dcc879eb1 | |||
| 4f3f4295ea | |||
| d02f17a8cf | |||
| 2f6a92168e | |||
| b6a3923b27 | |||
| d556cbc835 | |||
| f186806117 | |||
| f4f560b164 | |||
| 14b7f9237b | |||
| f3518b3d0f | |||
| 7964524e0a | |||
| 8ab8335baa | |||
| cd43bf9dfa | |||
| ccebf831e7 | |||
| 9f2f9bd8b0 | |||
| adf8c14536 | |||
| 606e82d718 | |||
| 1621f1753a | |||
| 196ab66e14 | |||
| 38ab32dad9 | |||
| 86046e52f0 | |||
| 9e7c860414 | |||
| 7dc8b86ee2 | |||
| 6ecbfe3de6 | |||
| f9940fc436 | |||
| 58e75ee276 | |||
| e7771f539d | |||
| c2f62cd8e0 | |||
| f4b6812675 | |||
| 03e4b37c04 | |||
| 7b3a9e0f63 | |||
| 067f546580 | |||
| 2f7697b7ec | |||
| 1d214f89ed | |||
| 0b47207949 | |||
| 94dd573a81 | |||
| 6fa4896155 | |||
| 28c99f9d8b | |||
| 88fbb5f73b | |||
| 402c185dd4 | |||
| ae2015a604 | |||
| 023731fc3f | |||
| 99998aac8a | |||
| 360d0bc110 | |||
| 817838e522 | |||
| deb3cfb4b6 | |||
| af61519632 | |||
| b1714cf554 | |||
| f0984b19f2 | |||
| eb3c9cd6f3 | |||
| e677b0ac3c | |||
| dd909bfe53 | |||
| b13b111614 | |||
| 5511530926 | |||
| 5e1ef01bc0 | |||
| a060eadab7 | |||
| 70db31bb8f | |||
| 1292775a75 | |||
| 0fbc84d364 | |||
| 0dd0b835ec | |||
| d96e0a7497 | |||
| 625504b8eb | |||
| a185ded47e | |||
| 5a0c77d06a | |||
| e54bd316d5 | |||
| f908b45cc7 | |||
| d02751ee08 | |||
| 13ab9786f7 | |||
| ba13a08e78 | |||
| 8c92a5ff7b | |||
| 37f728835b | |||
| a6f1eaa09e | |||
| dff8eca16e | |||
| a3cca9aae6 | |||
| edabd7735c | |||
| 461e7b7d5a | |||
| 06ea8d4781 | |||
| 62ef0bcb42 | |||
| a0043ec49f | |||
| 305f5232e7 | |||
| d467c4dd8a | |||
| 5b2ace80d4 | |||
| 1484a87cad | |||
| b23bc0b16b | |||
| a6c8dd846c | |||
| 5fff3b8161 | |||
| b4236d0ec0 | |||
| 5e8cd12760 | |||
| 699438602c | |||
| 52aa6eed0d | |||
| 6cdf207dcd | |||
| 607e9ef71b | |||
| 367c489fc3 | |||
| b3c9ad2fcb | |||
| ee9cb63327 | |||
| 889773c38d | |||
| b83704a218 | |||
| dfe5d51d04 | |||
| 280dee0438 | |||
| aa10ab69f6 | |||
| 6ef14d985d | |||
| 61a3226e14 | |||
| f9c370212b | |||
| 021f3ad5bc | |||
| 8bdc27bf5c | |||
| 00eb5222f8 | |||
| d06f490cc2 | |||
| b087a09d37 | |||
| 4cedc54d2d | |||
| 8fe7adc50e | |||
| bf5236e68b | |||
| da1d686705 | |||
| 2ce5bc73d5 | |||
| c6ae9313cc | |||
| 8f3883563f | |||
| 950273da41 | |||
| 391da742fd | |||
| 4414676076 | |||
| 0da45b7b40 | |||
| 7e02cb90f6 | |||
| 01ba90fdba | |||
| b394140f9e | |||
| 4a1d136721 | |||
| ab3009f771 | |||
| bf340f3de4 | |||
| 68f5827dee | |||
| b695a4ba3b | |||
| d84ab2734e | |||
| 4f0cc793c7 | |||
| b7a4ac22b2 | |||
| 5db9acae1d | |||
| c299c1432c | |||
| d8551ab732 | |||
| 25bc1279c2 | |||
| f6d9d23456 | |||
| b20d95d616 | |||
| 071c2f1c20 | |||
| 566d00f0df | |||
| 0550aa4e98 | |||
| baf69355a5 | |||
| 17c0266998 | |||
| 14a2207064 | |||
| 7c721fc6eb | |||
| ab03692a4c | |||
| 626fa4f27b | |||
| 15676e0f4f | |||
| 8709ce8ad5 | |||
| bb924d79d6 | |||
| 8e075e33d9 | |||
| b0b002104a | |||
| 43d1f34fa3 | |||
| 6db1a097aa | |||
| 6dae2f0749 | |||
| dc87c26b99 | |||
| 234d597083 | |||
| b74c347c7c | |||
| 996996e609 | |||
| cd9050f61f | |||
| b70b309977 | |||
| ee510f3f3f | |||
| 8a70b8ea3e | |||
| 27ee73bb89 | |||
| 3aeb47e447 | |||
| b9ceb30ecf | |||
| 5b6ee20b2d | |||
| d062ec0dfe | |||
| d5a0daa0d3 | |||
| 8c4ec71e26 | |||
| 1e5aa0ba93 | |||
| 0326a1f8cc | |||
| 8311404a09 | |||
| c81111c2cf | |||
| e499be12ae | |||
| e785f7f10a | |||
| c3da10bef6 | |||
| c8c8cb305e | |||
| efdd046ef8 | |||
| 496eefd2ee | |||
| 9da79b3a21 | |||
| 1b2b0970fb | |||
| 0bb45a7fa8 | |||
| 15a25a41aa | |||
| 76b6ff78cd | |||
| 5285b3f222 | |||
| 0c993c251b | |||
| b3a1f17452 | |||
| 11929e8c68 | |||
| fc9a081250 | |||
| 2583221117 | |||
| a69e551968 | |||
| 8bd0027e71 | |||
| 84eaa3e2fd | |||
| a57916b3db | |||
| 87e769786a | |||
| 05a7e941cf | |||
| ed307e6b3b | |||
| e8aa957209 | |||
| a39f820ff1 | |||
| 1c621a602f | |||
| 7169f4a6cb | |||
| e202c1a40e | |||
| 0f4b4da0aa | |||
| 1f96413bd3 | |||
| 9695621c91 | |||
| 2d67f5ead6 | |||
| 29d2a45abc | |||
| 13c8b05f9a | |||
| 2c1a5359c6 | |||
| d8530f228e | |||
| 575f6c2f0a | |||
| 62cdc592c0 | |||
| 11373faf23 | |||
| 0473eec0a2 | |||
| 7fc23dc085 | |||
| c741cc06b2 | |||
| 39dbfdec82 | |||
| 0e5d6056e4 | |||
| d711993b3b | |||
| 615bf7fe43 | |||
| 424b9b5a2f | |||
| d1e494b730 | |||
| 623e4b8fff | |||
| b5dd1f2f86 | |||
| ec83f9c747 | |||
| 31af27529e | |||
| 6302565942 | |||
| cda724b2da | |||
| c2e2ba2a40 | |||
| b5d3f5faa7 | |||
| 71f3910055 | |||
| 70ed8c3b32 | |||
| af13bfc920 | |||
| e24fd92f85 | |||
| 7e27cefe6a | |||
| 450cf6424e | |||
| 54898d3dbb | |||
| dd851a2b25 | |||
| 4c6b44eb30 | |||
| 74a3efe78d | |||
| 51301fc49e | |||
| 02dd8c3dd0 | |||
| 26a778c3b2 | |||
| 9fecbd97e8 | |||
| e1383e3903 | |||
| 47532b8512 | |||
| 3c4959433a | |||
| e921b4a86a | |||
| b23b0ca239 | |||
| 191b45f054 | |||
| 15d0383349 | |||
| d2485583fd | |||
| 2b94704916 | |||
| 85ac6c215a | |||
| e83e665db9 | |||
| 645aafef16 | |||
| 152c893a6f | |||
| 7c130dda56 | |||
| 2d82dad806 | |||
| e8ac5b759d | |||
| 4833d18968 | |||
| 6eafded1f6 | |||
| 7b440b720e | |||
| e20ba7384f | |||
| 45231c6ede | |||
| 35475defb5 | |||
| 8741841f27 | |||
| 5282d19b55 | |||
| d9782aa0fb | |||
| 9751facfb4 | |||
| e0110203e7 | |||
| 088b44cc2c | |||
| 8f63bcbfbf | |||
| c8029388c9 | |||
| d9c4d847a1 | |||
| df9d9425ec | |||
| 90bb3c684e | |||
| 9c81b6de8a | |||
| 6383498041 | |||
| daeb88785d | |||
| dcea08f73b | |||
| b252b921f8 | |||
| 172826bf13 | |||
| 060f1980f5 | |||
| e223d35252 | |||
| 99dba1a4c6 | |||
| b52026c81f | |||
| 47b8c86426 | |||
| 2e55c68648 | |||
| b7362dd84d | |||
| 01637b31e1 | |||
| 0e9a39608a | |||
| 79404e4d41 | |||
| 35c21fbdaf | |||
| 8c7bd7dc11 | |||
| 09ad4f0320 | |||
| d96b836bef | |||
| 59b2ffaf95 | |||
| f1b55ddd64 | |||
| 85acac3a30 | |||
| befff5c1e5 | |||
| d72ba81a67 | |||
| fef88e2032 | |||
| 20557e8ce4 | |||
| 99c905e908 | |||
| d7b58ee2c5 | |||
| faca2d387b | |||
| 358d02d97f | |||
| b66dac7465 | |||
| f7d201859a | |||
| 61d2ef5469 | |||
| ac994b9c62 | |||
| 264dcbc331 | |||
| e5425c0ffb | |||
| e10803de68 | |||
| 07b1a0e403 | |||
| 6ed2c702d8 | |||
| 5c1c33d33e | |||
| 70d37c88b5 | |||
| 1ba37d95b5 | |||
| 0d82198849 | |||
| 39927e75f2 | |||
| e6fd33b969 | |||
| e8fe32d5af | |||
| bfc8bb864d | |||
| 9179746763 | |||
| d0177d24cb | |||
| 0573008c9c | |||
| 9506f518c2 | |||
| 0f0ae9153b | |||
| 09c7c8ac64 | |||
| 5e2dfff148 | |||
| 958b47548d | |||
| 16155ef746 | |||
| 5755b61ea6 | |||
| 353847a77f | |||
| bdf64edeb8 | |||
| b5768dd927 | |||
| 3e5abf3a4d | |||
| d3029639de | |||
| d21d7e4add | |||
| afde69b5d9 | |||
| 3319df3df0 | |||
| 1102feaac3 | |||
| deede728be | |||
| fc3dd84122 | |||
| 9239441d73 | |||
| b984811851 | |||
| 1c52446331 | |||
| b6dffa8e66 | |||
| 315d650d27 | |||
| 07c121044a | |||
| f3169afcf5 | |||
| c371fc2a8e | |||
| 6889e11fd1 | |||
| fb73fd0afc | |||
| 6fcebd7a08 | |||
| 15ea62a546 | |||
| b0cd58f5aa | |||
| 7fe8f66fd3 | |||
| 68ca99e9d9 | |||
| a2542c658b | |||
| eb203c7e62 | |||
| 6ef466f3ed | |||
| 5074246462 | |||
| 73bbcebddb | |||
| 18128303b6 | |||
| c4a2d790a3 | |||
| c1ec150696 | |||
| f4b856df15 | |||
| 85b87553dd | |||
| 5decdf3afa | |||
| a4acee4939 | |||
| d06aea2831 | |||
| ae0a8b0a33 | |||
| f0452704a1 | |||
| b8b1f1ba80 | |||
| caf7478da4 | |||
| 0e40ba78a4 | |||
| d1eac6c9eb | |||
| 8f5201b2bc | |||
| 6022001d66 | |||
| f018c367ed | |||
| 48c47f097a | |||
| 39ac215b5a | |||
| 7d562ce85c | |||
| 51b317233a | |||
| 87ce715011 | |||
| ef5afc1e23 | |||
| 486212f22a | |||
| 0e8867dd6e | |||
| ca28b5ca82 | |||
| 19e26c1759 | |||
| 790f6643a4 | |||
| 2158ad3c0b | |||
| d904d8922f | |||
| da50792500 | |||
| b4629acc48 | |||
| 0cf4118330 | |||
| dd61a6ecc3 | |||
| 8e6f1284e1 | |||
| 813d3cd492 | |||
| f421606e21 | |||
| 1ccb9183b4 | |||
| 7d9b627f37 | |||
| 3038138909 | |||
| 2ca08d21e4 | |||
| 478e96fc5f | |||
| e237c7ea1d | |||
| bf9ff088fd | |||
| e073ebedd1 | |||
| 10d4ae7dcc | |||
| 5b8bdbb3e4 | |||
| c807e21c6b | |||
| cc92d0e316 | |||
| 09c396d5a3 | |||
| bc5bbca951 | |||
| ed4faedcd7 | |||
| 251556ebed | |||
| 1324afb459 | |||
| 1119804fc2 | |||
| cdf6440197 | |||
| 8727fe00af | |||
| 7da7890bb6 | |||
| 706bd2c51f | |||
| acabec940e | |||
| 470b998b61 | |||
| 80fad05f23 | |||
| 07a912fb9a | |||
| e9d83262c4 | |||
| 74323c22f9 | |||
| 2614e89b1b | |||
| e092fe1399 | |||
| 9cbe895cb8 | |||
| b0b0f74e83 | |||
| d9eaa92c37 | |||
| 566d07117e | |||
| 2bffdb1168 | |||
| 1359b48c9f | |||
| a69fb5eeac | |||
| 38e313350e | |||
| 5052dc04f2 | |||
| 9ef3a3aca0 | |||
| 7b91a2ec37 | |||
| 2926f855a1 | |||
| 639419db60 | |||
| 54747c127c | |||
| 791c3dd787 | |||
| b00d75ab7c | |||
| 956ea0df56 | |||
| 30014040e7 | |||
| ab055c3394 | |||
| 1e37eeea05 | |||
| 84aec0278d | |||
| 06642f58c5 | |||
| e6d44b32f4 | |||
| 1f3f6e2b92 | |||
| 8f2d3e3bcd | |||
| 2df2fc5792 | |||
| 20b0337e0a | |||
| e86b9dae48 | |||
| 71de897419 | |||
| 3edfaf9137 | |||
| 19c1784864 | 
							
								
								
									
										20
									
								
								.clang-format
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								.clang-format
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,20 @@
 | 
				
			|||||||
 | 
					# Format Style Options - Created with Clang Power Tools
 | 
				
			||||||
 | 
					---
 | 
				
			||||||
 | 
					BasedOnStyle: WebKit
 | 
				
			||||||
 | 
					AlignEscapedNewlines: DontAlign
 | 
				
			||||||
 | 
					AlignOperands: DontAlign
 | 
				
			||||||
 | 
					AllowShortCaseLabelsOnASingleLine: false
 | 
				
			||||||
 | 
					AllowShortFunctionsOnASingleLine: false
 | 
				
			||||||
 | 
					BreakBeforeBinaryOperators: None
 | 
				
			||||||
 | 
					BreakBeforeBraces: Allman
 | 
				
			||||||
 | 
					ColumnLimit: 180
 | 
				
			||||||
 | 
					ContinuationIndentWidth: 4
 | 
				
			||||||
 | 
					IndentCaseBlocks: true
 | 
				
			||||||
 | 
					IndentWidth: 4
 | 
				
			||||||
 | 
					MaxEmptyLinesToKeep: 1
 | 
				
			||||||
 | 
					ObjCBlockIndentWidth: 4
 | 
				
			||||||
 | 
					ObjCBreakBeforeNestedBlockParam: false
 | 
				
			||||||
 | 
					SortIncludes: false
 | 
				
			||||||
 | 
					TabWidth: 4
 | 
				
			||||||
 | 
					UseTab: Always
 | 
				
			||||||
 | 
					...
 | 
				
			||||||
							
								
								
									
										2
									
								
								.git-blame-ignore-revs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								.git-blame-ignore-revs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,2 @@
 | 
				
			|||||||
 | 
					# Add prettier to the project
 | 
				
			||||||
 | 
					41024ddb7961b04a5688bbc997cb74de6fab4763
 | 
				
			||||||
							
								
								
									
										11
									
								
								.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@@ -0,0 +1,11 @@
 | 
				
			|||||||
 | 
					db.*
 | 
				
			||||||
 | 
					deps/ios_toolchain/
 | 
				
			||||||
 | 
					deps/openssl/
 | 
				
			||||||
 | 
					dist/
 | 
				
			||||||
 | 
					.keys
 | 
				
			||||||
 | 
					**/node_modules
 | 
				
			||||||
 | 
					out
 | 
				
			||||||
 | 
					*.swo
 | 
				
			||||||
 | 
					*.swp
 | 
				
			||||||
 | 
					.zsign_cache/
 | 
				
			||||||
 | 
					result
 | 
				
			||||||
							
								
								
									
										21
									
								
								.gitmodules
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								.gitmodules
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@@ -0,0 +1,21 @@
 | 
				
			|||||||
 | 
					[submodule "deps/zlib"]
 | 
				
			||||||
 | 
						path = deps/zlib
 | 
				
			||||||
 | 
						url = https://github.com/madler/zlib.git
 | 
				
			||||||
 | 
					[submodule "deps/libsodium"]
 | 
				
			||||||
 | 
						path = deps/libsodium
 | 
				
			||||||
 | 
						url = https://github.com/jedisct1/libsodium.git
 | 
				
			||||||
 | 
					[submodule "deps/quickjs"]
 | 
				
			||||||
 | 
						path = deps/quickjs
 | 
				
			||||||
 | 
						url = https://github.com/bellard/quickjs.git
 | 
				
			||||||
 | 
					[submodule "deps/crypt_blowfish"]
 | 
				
			||||||
 | 
						path = deps/crypt_blowfish
 | 
				
			||||||
 | 
						url = https://github.com/openwall/crypt_blowfish.git
 | 
				
			||||||
 | 
					[submodule "deps/libbacktrace"]
 | 
				
			||||||
 | 
						path = deps/libbacktrace
 | 
				
			||||||
 | 
						url = https://github.com/ianlancetaylor/libbacktrace.git
 | 
				
			||||||
 | 
					[submodule "deps/libuv"]
 | 
				
			||||||
 | 
						path = deps/libuv
 | 
				
			||||||
 | 
						url = https://github.com/libuv/libuv.git
 | 
				
			||||||
 | 
					[submodule "deps/picohttpparser"]
 | 
				
			||||||
 | 
						path = deps/picohttpparser
 | 
				
			||||||
 | 
						url = https://github.com/h2o/picohttpparser.git
 | 
				
			||||||
							
								
								
									
										15
									
								
								.prettierignore
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								.prettierignore
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,15 @@
 | 
				
			|||||||
 | 
					node_modules
 | 
				
			||||||
 | 
					src
 | 
				
			||||||
 | 
					deps
 | 
				
			||||||
 | 
					.clang-format
 | 
				
			||||||
 | 
					flake.lock
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Minified files
 | 
				
			||||||
 | 
					**/*.min.css
 | 
				
			||||||
 | 
					**/*.min.js
 | 
				
			||||||
 | 
					**/leaflet.*
 | 
				
			||||||
 | 
					**/commonmark*
 | 
				
			||||||
 | 
					**/w3.css
 | 
				
			||||||
 | 
					apps/ssb/tribute.esm.js
 | 
				
			||||||
 | 
					apps/api/app.js
 | 
				
			||||||
 | 
					**/emojis.json
 | 
				
			||||||
							
								
								
									
										5
									
								
								.prettierrc.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								.prettierrc.yaml
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,5 @@
 | 
				
			|||||||
 | 
					trailingComma: 'es5'
 | 
				
			||||||
 | 
					useTabs: true
 | 
				
			||||||
 | 
					semi: true
 | 
				
			||||||
 | 
					singleQuote: true
 | 
				
			||||||
 | 
					bracketSpacing: false
 | 
				
			||||||
							
								
								
									
										37
									
								
								CONTRIBUTING.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										37
									
								
								CONTRIBUTING.md
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,37 @@
 | 
				
			|||||||
 | 
					# Contributing to Tilde Friends
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Thank you for your interest in Tilde Friends.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Above all, Tilde Friends aims to be a fun, safe place to play. When that is at
 | 
				
			||||||
 | 
					odds with the course of development, we will work through it with respectful
 | 
				
			||||||
 | 
					communication.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## How can I contribute?
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					The nature of Tilde Friends makes for a wide range of ways to contribute
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					- Just use it. Really, just kicking the tires will probably shake out issues
 | 
				
			||||||
 | 
					  in useful ways at this point.
 | 
				
			||||||
 | 
					- Report and comment on bugs: https://dev.tildefriends.net/issues.
 | 
				
			||||||
 | 
					- Make apps. You don't need my permission to make and share apps with Tilde
 | 
				
			||||||
 | 
					  Friends. I hope that an ecosystem of good apps grows outside of this
 | 
				
			||||||
 | 
					  repository. If you want to recreate better versions of the stock apps, just
 | 
				
			||||||
 | 
					  do it. If you make a better ssb app or whatever and drop me a line however
 | 
				
			||||||
 | 
					  is most convenient for you, I will probably take a look and consider
 | 
				
			||||||
 | 
					  replacing the stock one with it.
 | 
				
			||||||
 | 
					- Write about it. Docs in the git repository, blog posts, private messages to
 | 
				
			||||||
 | 
					  me with ideas...really there is no wrong answer. Just make some noise, and
 | 
				
			||||||
 | 
					  I'll do my best to incorporate or otherwise link your feedback and make the
 | 
				
			||||||
 | 
					  most of it.
 | 
				
			||||||
 | 
					- Write C code in the git repository. I'm really striving for it to be the
 | 
				
			||||||
 | 
					  case that other people don't really need to meddle in there, but if you can
 | 
				
			||||||
 | 
					  help out, I will gladly review your pull requests via
 | 
				
			||||||
 | 
					  https://dev.tildefriends.net/pulls.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## Best practices
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					- The C code is formatted with clang-format. Run `make format`.
 | 
				
			||||||
 | 
					- The rest is formatted with prettier. Run `npm run prettier`.
 | 
				
			||||||
 | 
					- We strive to have code compile on all platforms with no warnings and run with
 | 
				
			||||||
 | 
					  no sanitizer issues.
 | 
				
			||||||
 | 
					- There are tests. Run `out/debug/tildefriends test`.
 | 
				
			||||||
							
								
								
									
										915
									
								
								GNUmakefile
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										915
									
								
								GNUmakefile
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,915 @@
 | 
				
			|||||||
 | 
					.ONESHELL:
 | 
				
			||||||
 | 
					.DELETE_ON_ERROR:
 | 
				
			||||||
 | 
					MAKEFLAGS += --warn-undefined-variables
 | 
				
			||||||
 | 
					MAKEFLAGS += --no-builtin-rules
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					VERSION_CODE := 20
 | 
				
			||||||
 | 
					VERSION_NUMBER := 0.0.20-wip
 | 
				
			||||||
 | 
					VERSION_NAME := One word all lowercase four words all uppercase.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					SQLITE_URL := https://www.sqlite.org/2024/sqlite-amalgamation-3460000.zip
 | 
				
			||||||
 | 
					LIBUV_URL := https://dist.libuv.org/dist/v1.48.0/libuv-v1.48.0.tar.gz
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					PROJECT = tildefriends
 | 
				
			||||||
 | 
					BUILD_DIR ?= out
 | 
				
			||||||
 | 
					UNAME_S := $(shell uname -s)
 | 
				
			||||||
 | 
					UNAME_M := $(shell uname -m)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					ANDROID_SDK ?= ~/Android/Sdk
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					HAVE_WIN := 0
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					ifeq ($(UNAME_S),Darwin)
 | 
				
			||||||
 | 
					BUILD_TYPES := macosdebug macosrelease iosdebug iosrelease iossimdebug iossimrelease
 | 
				
			||||||
 | 
					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)
 | 
				
			||||||
 | 
					else ifeq ($(UNAME_S),Haiku)
 | 
				
			||||||
 | 
					BUILD_TYPES := debug release
 | 
				
			||||||
 | 
					CFLAGS += -Dstatic_assert=_Static_assert
 | 
				
			||||||
 | 
					LDFLAGS += \
 | 
				
			||||||
 | 
						-lbsd \
 | 
				
			||||||
 | 
						-lnetwork
 | 
				
			||||||
 | 
					else ifeq ($(UNAME_S),OpenBSD)
 | 
				
			||||||
 | 
					BUILD_TYPES := debug release
 | 
				
			||||||
 | 
					CFLAGS += \
 | 
				
			||||||
 | 
						-Wno-unknown-warning-option
 | 
				
			||||||
 | 
					LDFLAGS += \
 | 
				
			||||||
 | 
						-lexecinfo \
 | 
				
			||||||
 | 
						-lc++abi
 | 
				
			||||||
 | 
					HAVE_ANDROID := 0
 | 
				
			||||||
 | 
					HAVE_LINUX_IOS := 0
 | 
				
			||||||
 | 
					else
 | 
				
			||||||
 | 
					$(error Unexpected host platform $(UNAME_S).)
 | 
				
			||||||
 | 
					endif
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					CFLAGS += \
 | 
				
			||||||
 | 
						-std=gnu11 \
 | 
				
			||||||
 | 
						-Wall \
 | 
				
			||||||
 | 
						-Wextra \
 | 
				
			||||||
 | 
						-Wno-unused-parameter \
 | 
				
			||||||
 | 
						-MMD \
 | 
				
			||||||
 | 
						-MP \
 | 
				
			||||||
 | 
						-ffunction-sections \
 | 
				
			||||||
 | 
						-fdata-sections \
 | 
				
			||||||
 | 
						-fno-exceptions \
 | 
				
			||||||
 | 
						-g
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					ANDROID_MIN_SDK_VERSION := 24
 | 
				
			||||||
 | 
					ANDROID_TARGET_SDK_VERSION := 34
 | 
				
			||||||
 | 
					ANDROID_BUILD_TOOLS := $(ANDROID_SDK)/build-tools/34.0.0
 | 
				
			||||||
 | 
					ANDROID_PLATFORM := $(ANDROID_SDK)/platforms/android-$(ANDROID_TARGET_SDK_VERSION)
 | 
				
			||||||
 | 
					ANDROID_NDK ?= $(ANDROID_SDK)/ndk/26.2.11394342
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					ANDROID_ARMV7A_TARGETS := \
 | 
				
			||||||
 | 
						out/androiddebug-armv7a/tildefriends \
 | 
				
			||||||
 | 
						out/androidrelease-armv7a/tildefriends
 | 
				
			||||||
 | 
					ANDROID_ARM64_TARGETS := \
 | 
				
			||||||
 | 
						out/androiddebug/tildefriends \
 | 
				
			||||||
 | 
						out/androidrelease/tildefriends
 | 
				
			||||||
 | 
					ANDROID_X86_TARGETS := \
 | 
				
			||||||
 | 
						out/androiddebug-x86/tildefriends \
 | 
				
			||||||
 | 
						out/androidrelease-x86/tildefriends
 | 
				
			||||||
 | 
					ANDROID_X86_64_TARGETS := \
 | 
				
			||||||
 | 
						out/androiddebug-x86_64/tildefriends \
 | 
				
			||||||
 | 
						out/androidrelease-x86_64/tildefriends
 | 
				
			||||||
 | 
					ANDROID_TARGETS := \
 | 
				
			||||||
 | 
						$(ANDROID_X86_TARGETS) \
 | 
				
			||||||
 | 
						$(ANDROID_X86_64_TARGETS) \
 | 
				
			||||||
 | 
						$(ANDROID_ARMV7A_TARGETS) \
 | 
				
			||||||
 | 
						$(ANDROID_ARM64_TARGETS)
 | 
				
			||||||
 | 
					ifeq ($(HAVE_ANDROID),1)
 | 
				
			||||||
 | 
					BUILD_TYPES += \
 | 
				
			||||||
 | 
						androiddebug \
 | 
				
			||||||
 | 
						androidrelease \
 | 
				
			||||||
 | 
						androiddebug-armv7a \
 | 
				
			||||||
 | 
						androidrelease-armv7a \
 | 
				
			||||||
 | 
						androiddebug-x86 \
 | 
				
			||||||
 | 
						androidrelease-x86 \
 | 
				
			||||||
 | 
						androiddebug-x86_64 \
 | 
				
			||||||
 | 
						androidrelease-x86_64
 | 
				
			||||||
 | 
					all: out/TildeFriends-arm-debug.apk out/TildeFriends-arm-release.apk out/TildeFriends-x86-debug.apk out/TildeFriends-x86-release.apk
 | 
				
			||||||
 | 
					endif
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					WINDOWS_TARGETS := \
 | 
				
			||||||
 | 
						out/windebug/tildefriends.exe \
 | 
				
			||||||
 | 
						out/winrelease/tildefriends.exe
 | 
				
			||||||
 | 
					ifeq ($(HAVE_WIN),1)
 | 
				
			||||||
 | 
					BUILD_TYPES += windebug winrelease
 | 
				
			||||||
 | 
					endif
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					LINUX_TARGETS := \
 | 
				
			||||||
 | 
						out/debug/tildefriends \
 | 
				
			||||||
 | 
						out/release/tildefriends
 | 
				
			||||||
 | 
					MACOS_TARGETS := \
 | 
				
			||||||
 | 
						out/macosdebug/tildefriends \
 | 
				
			||||||
 | 
						out/macosrelease/tildefriends
 | 
				
			||||||
 | 
					IOS_TARGETS := \
 | 
				
			||||||
 | 
						out/iosdebug/tildefriends \
 | 
				
			||||||
 | 
						out/iosrelease/tildefriends
 | 
				
			||||||
 | 
					IOSSIM_TARGETS := \
 | 
				
			||||||
 | 
						out/iossimdebug/tildefriends \
 | 
				
			||||||
 | 
						out/iossimrelease/tildefriends
 | 
				
			||||||
 | 
					IOS_APPS = \
 | 
				
			||||||
 | 
						out/tildefriends-iosdebug.app/tildefriends \
 | 
				
			||||||
 | 
						out/tildefriends-iosrelease.app/tildefriends
 | 
				
			||||||
 | 
					ifeq ($(HAVE_LINUX_IOS),1)
 | 
				
			||||||
 | 
					BUILD_TYPES += iosdebug iosrelease
 | 
				
			||||||
 | 
					all: $(IOS_APPS)
 | 
				
			||||||
 | 
					endif
 | 
				
			||||||
 | 
					ifeq ($(UNAME_S),Darwin)
 | 
				
			||||||
 | 
					all: $(IOS_APPS) \
 | 
				
			||||||
 | 
						out/tildefriends-iossimdebug.app/tildefriends \
 | 
				
			||||||
 | 
						out/tildefriends-iossimrelease.app/tildefriends
 | 
				
			||||||
 | 
					endif
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					DEBUG_TARGETS := \
 | 
				
			||||||
 | 
						out/debug/tildefriends \
 | 
				
			||||||
 | 
						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
 | 
				
			||||||
 | 
					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
 | 
				
			||||||
 | 
					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))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					$(NONANDROID_TARGETS): CFLAGS += -fno-omit-frame-pointer
 | 
				
			||||||
 | 
					$(filter-out $(ANDROID_TARGETS) $(WINDOWS_TARGETS),$(ALL_TARGETS)): LDFLAGS += -rdynamic
 | 
				
			||||||
 | 
					$(ANDROID_TARGETS): CFLAGS += \
 | 
				
			||||||
 | 
						--sysroot $(ANDROID_NDK)/toolchains/llvm/prebuilt/linux-x86_64/sysroot \
 | 
				
			||||||
 | 
						-fPIC \
 | 
				
			||||||
 | 
						-fdebug-compilation-dir . \
 | 
				
			||||||
 | 
						-fomit-frame-pointer \
 | 
				
			||||||
 | 
						-fno-asynchronous-unwind-tables \
 | 
				
			||||||
 | 
						-funwind-tables
 | 
				
			||||||
 | 
					$(ANDROID_TARGETS): LDFLAGS += --sysroot $(ANDROID_NDK)/toolchains/llvm/prebuilt/linux-x86_64/sysroot -fPIC
 | 
				
			||||||
 | 
					$(DEBUG_TARGETS): CFLAGS += -DDEBUG -Og
 | 
				
			||||||
 | 
					$(RELEASE_TARGETS): CFLAGS += -DNDEBUG
 | 
				
			||||||
 | 
					$(NONANDROID_RELEASE_TARGETS): CFLAGS += -O3
 | 
				
			||||||
 | 
					$(ANDROID_RELEASE_TARGETS): CFLAGS += -Oz
 | 
				
			||||||
 | 
					$(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
 | 
				
			||||||
 | 
					$(WINDOWS_TARGETS): LDFLAGS += \
 | 
				
			||||||
 | 
						-static \
 | 
				
			||||||
 | 
						-lm \
 | 
				
			||||||
 | 
						-Ldeps/openssl/mingw64/usr/local/lib
 | 
				
			||||||
 | 
					ifeq ($(UNAME_S),Darwin)
 | 
				
			||||||
 | 
					$(MACOS_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
 | 
				
			||||||
 | 
					endif
 | 
				
			||||||
 | 
					$(ANDROID_X86_64_TARGETS): ANDROID_NDK_TARGET_TRIPLE := x86_64-linux-android
 | 
				
			||||||
 | 
					$(ANDROID_X86_TARGETS): ANDROID_NDK_TARGET_TRIPLE := i686-linux-android
 | 
				
			||||||
 | 
					$(ANDROID_ARMV7A_TARGETS): ANDROID_NDK_TARGET_TRIPLE := armv7a-linux-androideabi
 | 
				
			||||||
 | 
					$(ANDROID_ARM64_TARGETS): ANDROID_NDK_TARGET_TRIPLE := aarch64-linux-android
 | 
				
			||||||
 | 
					$(ANDROID_TARGETS): CC = $(ANDROID_NDK)/toolchains/llvm/prebuilt/linux-x86_64/bin/$(ANDROID_NDK_TARGET_TRIPLE)$(ANDROID_MIN_SDK_VERSION)-clang
 | 
				
			||||||
 | 
					$(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
 | 
				
			||||||
 | 
					$(NONMACOS_TARGETS): LDFLAGS += -Wl,--gc-sections
 | 
				
			||||||
 | 
					$(IOS_TARGETS): CFLAGS += -mios-version-min=9.0 -Ideps/openssl/ios/ios64-xcrun/usr/local/include
 | 
				
			||||||
 | 
					$(IOS_TARGETS): LDFLAGS += -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
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					ifeq ($(UNAME_M),x86_64)
 | 
				
			||||||
 | 
					ifneq ($(UNAME_S),Haiku)
 | 
				
			||||||
 | 
					out/debug/tildefriends: CFLAGS += -fsanitize=address -fsanitize=undefined -fno-common
 | 
				
			||||||
 | 
					out/debug/tildefriends: LDFLAGS += -fsanitize=address -fsanitize=undefined
 | 
				
			||||||
 | 
					endif
 | 
				
			||||||
 | 
					endif
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					ifeq ($(UNAME_M),aarch64)
 | 
				
			||||||
 | 
					out/debug/tildefriends: CFLAGS += -fsanitize=address -fsanitize=undefined -fno-common
 | 
				
			||||||
 | 
					out/debug/tildefriends: LDFLAGS += -fsanitize=address -fsanitize=undefined
 | 
				
			||||||
 | 
					endif
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					get_objs = \
 | 
				
			||||||
 | 
						$(foreach build_type,$(BUILD_TYPES),$(addprefix $(BUILD_DIR)/$(build_type)/,$(addsuffix .o,$(basename $(value $(1)))))) \
 | 
				
			||||||
 | 
						$(foreach build_type,debug release,$(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)_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)))))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					APP_SOURCES := $(wildcard src/*.c)
 | 
				
			||||||
 | 
					APP_SOURCES_ios := $(wildcard src/*.m)
 | 
				
			||||||
 | 
					APP_OBJS := $(call get_objs,APP_SOURCES)
 | 
				
			||||||
 | 
					$(APP_OBJS): CFLAGS += \
 | 
				
			||||||
 | 
						-Ideps/base64c/include \
 | 
				
			||||||
 | 
						-Ideps/crypt_blowfish \
 | 
				
			||||||
 | 
						-Ideps/libbacktrace \
 | 
				
			||||||
 | 
						-Ideps/libsodium \
 | 
				
			||||||
 | 
						-Ideps/libsodium/src/libsodium/include \
 | 
				
			||||||
 | 
						-Ideps/libuv/include \
 | 
				
			||||||
 | 
						-Ideps/zlib \
 | 
				
			||||||
 | 
						-Ideps/zlib/contrib/minizip \
 | 
				
			||||||
 | 
						-Ideps/picohttpparser \
 | 
				
			||||||
 | 
						-Ideps/quickjs \
 | 
				
			||||||
 | 
						-Ideps/sqlite \
 | 
				
			||||||
 | 
						-Ideps/valgrind \
 | 
				
			||||||
 | 
						-Wdouble-promotion \
 | 
				
			||||||
 | 
						-Werror
 | 
				
			||||||
 | 
					ifeq ($(UNAME_M),x86_64)
 | 
				
			||||||
 | 
					$(filter-out $(BUILD_DIR)/android% $(BUILD_DIR)/macos% $(BUILD_DIR)/ios%,$(APP_OBJS)): CFLAGS += \
 | 
				
			||||||
 | 
						-fanalyzer
 | 
				
			||||||
 | 
					endif
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					BLOWFISH_SOURCES := \
 | 
				
			||||||
 | 
						deps/crypt_blowfish/crypt_blowfish.c \
 | 
				
			||||||
 | 
						deps/crypt_blowfish/crypt_gensalt.c \
 | 
				
			||||||
 | 
						deps/crypt_blowfish/wrapper.c
 | 
				
			||||||
 | 
					BLOWFISH_SOURCES_win := \
 | 
				
			||||||
 | 
						deps/crypt_blowfish/x86.S
 | 
				
			||||||
 | 
					BLOWFISH_SOURCES_x86 := \
 | 
				
			||||||
 | 
						deps/crypt_blowfish/x86.S
 | 
				
			||||||
 | 
					BLOWFISH_OBJS := $(call get_objs,BLOWFISH_SOURCES)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					UV_SOURCES := \
 | 
				
			||||||
 | 
						deps/libuv/src/fs-poll.c \
 | 
				
			||||||
 | 
						deps/libuv/src/idna.c \
 | 
				
			||||||
 | 
						deps/libuv/src/inet.c \
 | 
				
			||||||
 | 
						deps/libuv/src/random.c \
 | 
				
			||||||
 | 
						deps/libuv/src/strscpy.c \
 | 
				
			||||||
 | 
						deps/libuv/src/strtok.c \
 | 
				
			||||||
 | 
						deps/libuv/src/threadpool.c \
 | 
				
			||||||
 | 
						deps/libuv/src/timer.c \
 | 
				
			||||||
 | 
						deps/libuv/src/uv-common.c \
 | 
				
			||||||
 | 
						deps/libuv/src/uv-data-getter-setters.c \
 | 
				
			||||||
 | 
						deps/libuv/src/version.c
 | 
				
			||||||
 | 
					UV_SOURCES_unix := \
 | 
				
			||||||
 | 
						deps/libuv/src/unix/async.c \
 | 
				
			||||||
 | 
						deps/libuv/src/unix/core.c \
 | 
				
			||||||
 | 
						deps/libuv/src/unix/dl.c \
 | 
				
			||||||
 | 
						deps/libuv/src/unix/fs.c \
 | 
				
			||||||
 | 
						deps/libuv/src/unix/getaddrinfo.c \
 | 
				
			||||||
 | 
						deps/libuv/src/unix/getnameinfo.c \
 | 
				
			||||||
 | 
						deps/libuv/src/unix/loop-watcher.c \
 | 
				
			||||||
 | 
						deps/libuv/src/unix/loop.c \
 | 
				
			||||||
 | 
						deps/libuv/src/unix/pipe.c \
 | 
				
			||||||
 | 
						deps/libuv/src/unix/poll.c \
 | 
				
			||||||
 | 
						deps/libuv/src/unix/process.c \
 | 
				
			||||||
 | 
						deps/libuv/src/unix/random-devurandom.c \
 | 
				
			||||||
 | 
						deps/libuv/src/unix/random-getrandom.c \
 | 
				
			||||||
 | 
						deps/libuv/src/unix/signal.c \
 | 
				
			||||||
 | 
						deps/libuv/src/unix/stream.c \
 | 
				
			||||||
 | 
						deps/libuv/src/unix/tcp.c \
 | 
				
			||||||
 | 
						deps/libuv/src/unix/thread.c \
 | 
				
			||||||
 | 
						deps/libuv/src/unix/tty.c \
 | 
				
			||||||
 | 
						deps/libuv/src/unix/udp.c
 | 
				
			||||||
 | 
					ifeq ($(UNAME_S),Linux)
 | 
				
			||||||
 | 
						UV_SOURCES_unix += \
 | 
				
			||||||
 | 
							deps/libuv/src/unix/linux.c \
 | 
				
			||||||
 | 
							deps/libuv/src/unix/procfs-exepath.c \
 | 
				
			||||||
 | 
							deps/libuv/src/unix/proctitle.c \
 | 
				
			||||||
 | 
							deps/libuv/src/unix/random-sysctl-linux.c
 | 
				
			||||||
 | 
					else ifeq ($(UNAME_S),Haiku)
 | 
				
			||||||
 | 
						UV_SOURCES_unix += \
 | 
				
			||||||
 | 
							deps/libuv/src/unix/bsd-ifaddrs.c \
 | 
				
			||||||
 | 
							deps/libuv/src/unix/haiku.c \
 | 
				
			||||||
 | 
							deps/libuv/src/unix/no-fsevents.c \
 | 
				
			||||||
 | 
							deps/libuv/src/unix/no-proctitle.c \
 | 
				
			||||||
 | 
							deps/libuv/src/unix/posix-hrtime.c \
 | 
				
			||||||
 | 
							deps/libuv/src/unix/posix-poll.c
 | 
				
			||||||
 | 
					else ifeq ($(UNAME_S),OpenBSD)
 | 
				
			||||||
 | 
						UV_SOURCES_unix += \
 | 
				
			||||||
 | 
							deps/libuv/src/unix/bsd-ifaddrs.c \
 | 
				
			||||||
 | 
							deps/libuv/src/unix/kqueue.c \
 | 
				
			||||||
 | 
							deps/libuv/src/unix/no-proctitle.c \
 | 
				
			||||||
 | 
							deps/libuv/src/unix/openbsd.c \
 | 
				
			||||||
 | 
							deps/libuv/src/unix/posix-hrtime.c \
 | 
				
			||||||
 | 
							deps/libuv/src/unix/random-getentropy.c
 | 
				
			||||||
 | 
					endif
 | 
				
			||||||
 | 
					UV_SOURCES_android := \
 | 
				
			||||||
 | 
						deps/libuv/src/unix/random-getentropy.c
 | 
				
			||||||
 | 
					UV_SOURCES_win := \
 | 
				
			||||||
 | 
						deps/libuv/src/win/async.c \
 | 
				
			||||||
 | 
						deps/libuv/src/win/core.c \
 | 
				
			||||||
 | 
						deps/libuv/src/win/detect-wakeup.c \
 | 
				
			||||||
 | 
						deps/libuv/src/win/dl.c \
 | 
				
			||||||
 | 
						deps/libuv/src/win/error.c \
 | 
				
			||||||
 | 
						deps/libuv/src/win/fs-event.c \
 | 
				
			||||||
 | 
						deps/libuv/src/win/fs.c \
 | 
				
			||||||
 | 
						deps/libuv/src/win/getaddrinfo.c \
 | 
				
			||||||
 | 
						deps/libuv/src/win/getnameinfo.c \
 | 
				
			||||||
 | 
						deps/libuv/src/win/handle.c \
 | 
				
			||||||
 | 
						deps/libuv/src/win/loop-watcher.c \
 | 
				
			||||||
 | 
						deps/libuv/src/win/pipe.c \
 | 
				
			||||||
 | 
						deps/libuv/src/win/poll.c \
 | 
				
			||||||
 | 
						deps/libuv/src/win/process-stdio.c \
 | 
				
			||||||
 | 
						deps/libuv/src/win/process.c \
 | 
				
			||||||
 | 
						deps/libuv/src/win/signal.c \
 | 
				
			||||||
 | 
						deps/libuv/src/win/snprintf.c \
 | 
				
			||||||
 | 
						deps/libuv/src/win/stream.c \
 | 
				
			||||||
 | 
						deps/libuv/src/win/tcp.c \
 | 
				
			||||||
 | 
						deps/libuv/src/win/thread.c \
 | 
				
			||||||
 | 
						deps/libuv/src/win/tty.c \
 | 
				
			||||||
 | 
						deps/libuv/src/win/udp.c \
 | 
				
			||||||
 | 
						deps/libuv/src/win/util.c \
 | 
				
			||||||
 | 
						deps/libuv/src/win/winapi.c \
 | 
				
			||||||
 | 
						deps/libuv/src/win/winsock.c
 | 
				
			||||||
 | 
					UV_SOURCES_macos := \
 | 
				
			||||||
 | 
						deps/libuv/src/unix/async.c \
 | 
				
			||||||
 | 
						deps/libuv/src/unix/bsd-ifaddrs.c \
 | 
				
			||||||
 | 
						deps/libuv/src/unix/core.c \
 | 
				
			||||||
 | 
						deps/libuv/src/unix/darwin.c \
 | 
				
			||||||
 | 
						deps/libuv/src/unix/darwin-proctitle.c \
 | 
				
			||||||
 | 
						deps/libuv/src/unix/dl.c \
 | 
				
			||||||
 | 
						deps/libuv/src/unix/fs.c \
 | 
				
			||||||
 | 
						deps/libuv/src/unix/fsevents.c \
 | 
				
			||||||
 | 
						deps/libuv/src/unix/getaddrinfo.c \
 | 
				
			||||||
 | 
						deps/libuv/src/unix/getnameinfo.c \
 | 
				
			||||||
 | 
						deps/libuv/src/unix/kqueue.c \
 | 
				
			||||||
 | 
						deps/libuv/src/unix/loop-watcher.c \
 | 
				
			||||||
 | 
						deps/libuv/src/unix/loop.c \
 | 
				
			||||||
 | 
						deps/libuv/src/unix/pipe.c \
 | 
				
			||||||
 | 
						deps/libuv/src/unix/poll.c \
 | 
				
			||||||
 | 
						deps/libuv/src/unix/process.c \
 | 
				
			||||||
 | 
						deps/libuv/src/unix/proctitle.c \
 | 
				
			||||||
 | 
						deps/libuv/src/unix/random-devurandom.c \
 | 
				
			||||||
 | 
						deps/libuv/src/unix/random-getentropy.c \
 | 
				
			||||||
 | 
						deps/libuv/src/unix/signal.c \
 | 
				
			||||||
 | 
						deps/libuv/src/unix/stream.c \
 | 
				
			||||||
 | 
						deps/libuv/src/unix/tcp.c \
 | 
				
			||||||
 | 
						deps/libuv/src/unix/thread.c \
 | 
				
			||||||
 | 
						deps/libuv/src/unix/tty.c \
 | 
				
			||||||
 | 
						deps/libuv/src/unix/udp.c
 | 
				
			||||||
 | 
					UV_OBJS := $(call get_objs,UV_SOURCES)
 | 
				
			||||||
 | 
					$(UV_OBJS): CFLAGS += \
 | 
				
			||||||
 | 
						-Ideps/libuv/include \
 | 
				
			||||||
 | 
						-Ideps/libuv/src \
 | 
				
			||||||
 | 
						-Wno-dangling-pointer \
 | 
				
			||||||
 | 
						-Wno-incompatible-pointer-types \
 | 
				
			||||||
 | 
						-Wno-maybe-uninitialized \
 | 
				
			||||||
 | 
						-Wno-sign-compare \
 | 
				
			||||||
 | 
						-Wno-unused-but-set-parameter \
 | 
				
			||||||
 | 
						-Wno-unused-but-set-variable \
 | 
				
			||||||
 | 
						-Wno-unused-result \
 | 
				
			||||||
 | 
						-Wno-unused-variable
 | 
				
			||||||
 | 
					ifeq ($(UNAME_S),Linux)
 | 
				
			||||||
 | 
					$(UV_OBJS): CFLAGS += \
 | 
				
			||||||
 | 
						-D_GNU_SOURCE
 | 
				
			||||||
 | 
					else ifeq ($(UNAME_S),Haiku)
 | 
				
			||||||
 | 
					$(UV_OBJS): CFLAGS += \
 | 
				
			||||||
 | 
						-D_BSD_SOURCE \
 | 
				
			||||||
 | 
						-Wno-format-truncation
 | 
				
			||||||
 | 
					endif
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					SODIUM_SOURCES := \
 | 
				
			||||||
 | 
						deps/libsodium/src/libsodium/crypto_aead/aegis128l/aead_aegis128l.c \
 | 
				
			||||||
 | 
						deps/libsodium/src/libsodium/crypto_aead/aegis128l/aegis128l_soft.c \
 | 
				
			||||||
 | 
						deps/libsodium/src/libsodium/crypto_aead/aegis256/aead_aegis256.c \
 | 
				
			||||||
 | 
						deps/libsodium/src/libsodium/crypto_aead/aegis256/aegis256_soft.c \
 | 
				
			||||||
 | 
						deps/libsodium/src/libsodium/crypto_auth/hmacsha512/auth_hmacsha512.c \
 | 
				
			||||||
 | 
						deps/libsodium/src/libsodium/crypto_auth/hmacsha512256/auth_hmacsha512256.c \
 | 
				
			||||||
 | 
						deps/libsodium/src/libsodium/crypto_box/crypto_box.c \
 | 
				
			||||||
 | 
						deps/libsodium/src/libsodium/crypto_box/curve25519xsalsa20poly1305/box_curve25519xsalsa20poly1305.c \
 | 
				
			||||||
 | 
						deps/libsodium/src/libsodium/crypto_core/ed25519/ref10/ed25519_ref10.c \
 | 
				
			||||||
 | 
						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/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 \
 | 
				
			||||||
 | 
						deps/libsodium/src/libsodium/crypto_hash/sha256/cp/hash_sha256_cp.c \
 | 
				
			||||||
 | 
						deps/libsodium/src/libsodium/crypto_hash/sha256/hash_sha256.c \
 | 
				
			||||||
 | 
						deps/libsodium/src/libsodium/crypto_hash/sha512/cp/hash_sha512_cp.c \
 | 
				
			||||||
 | 
						deps/libsodium/src/libsodium/crypto_onetimeauth/poly1305/donna/poly1305_donna.c \
 | 
				
			||||||
 | 
						deps/libsodium/src/libsodium/crypto_onetimeauth/poly1305/onetimeauth_poly1305.c \
 | 
				
			||||||
 | 
						deps/libsodium/src/libsodium/crypto_pwhash/argon2/argon2-core.c \
 | 
				
			||||||
 | 
						deps/libsodium/src/libsodium/crypto_pwhash/argon2/argon2-fill-block-ref.c \
 | 
				
			||||||
 | 
						deps/libsodium/src/libsodium/crypto_pwhash/argon2/blake2b-long.c \
 | 
				
			||||||
 | 
						deps/libsodium/src/libsodium/crypto_scalarmult/crypto_scalarmult.c \
 | 
				
			||||||
 | 
						deps/libsodium/src/libsodium/crypto_scalarmult/curve25519/ref10/x25519_ref10.c \
 | 
				
			||||||
 | 
						deps/libsodium/src/libsodium/crypto_scalarmult/curve25519/sandy2x/curve25519_sandy2x.c \
 | 
				
			||||||
 | 
						deps/libsodium/src/libsodium/crypto_scalarmult/curve25519/scalarmult_curve25519.c \
 | 
				
			||||||
 | 
						deps/libsodium/src/libsodium/crypto_secretbox/crypto_secretbox_easy.c \
 | 
				
			||||||
 | 
						deps/libsodium/src/libsodium/crypto_secretbox/xsalsa20poly1305/secretbox_xsalsa20poly1305.c \
 | 
				
			||||||
 | 
						deps/libsodium/src/libsodium/crypto_sign/crypto_sign.c \
 | 
				
			||||||
 | 
						deps/libsodium/src/libsodium/crypto_sign/ed25519/ref10/keypair.c \
 | 
				
			||||||
 | 
						deps/libsodium/src/libsodium/crypto_sign/ed25519/ref10/open.c \
 | 
				
			||||||
 | 
						deps/libsodium/src/libsodium/crypto_sign/ed25519/ref10/sign.c \
 | 
				
			||||||
 | 
						deps/libsodium/src/libsodium/crypto_sign/ed25519/sign_ed25519.c \
 | 
				
			||||||
 | 
						deps/libsodium/src/libsodium/crypto_stream/chacha20/ref/chacha20_ref.c \
 | 
				
			||||||
 | 
						deps/libsodium/src/libsodium/crypto_stream/chacha20/stream_chacha20.c \
 | 
				
			||||||
 | 
						deps/libsodium/src/libsodium/crypto_stream/salsa20/ref/salsa20_ref.c \
 | 
				
			||||||
 | 
						deps/libsodium/src/libsodium/crypto_stream/salsa20/stream_salsa20.c \
 | 
				
			||||||
 | 
						deps/libsodium/src/libsodium/crypto_stream/xsalsa20/stream_xsalsa20.c \
 | 
				
			||||||
 | 
						deps/libsodium/src/libsodium/crypto_verify/verify.c \
 | 
				
			||||||
 | 
						deps/libsodium/src/libsodium/randombytes/randombytes.c \
 | 
				
			||||||
 | 
						deps/libsodium/src/libsodium/randombytes/sysrandom/randombytes_sysrandom.c \
 | 
				
			||||||
 | 
						deps/libsodium/src/libsodium/sodium/core.c \
 | 
				
			||||||
 | 
						deps/libsodium/src/libsodium/sodium/codecs.c \
 | 
				
			||||||
 | 
						deps/libsodium/src/libsodium/sodium/runtime.c \
 | 
				
			||||||
 | 
						deps/libsodium/src/libsodium/sodium/utils.c \
 | 
				
			||||||
 | 
						deps/libsodium/src/libsodium/sodium/version.c
 | 
				
			||||||
 | 
					SODIUM_OBJS := $(call get_objs,SODIUM_SOURCES)
 | 
				
			||||||
 | 
					$(SODIUM_OBJS): CFLAGS += \
 | 
				
			||||||
 | 
						-DCONFIGURED=1 \
 | 
				
			||||||
 | 
						-DMINIMAL=1 \
 | 
				
			||||||
 | 
						-DHAVE_ALLOCA \
 | 
				
			||||||
 | 
						-DHAVE_CPUID_V \
 | 
				
			||||||
 | 
						-DHAVE_GCC_MEMORY_FENCES \
 | 
				
			||||||
 | 
						-Wno-unused-function \
 | 
				
			||||||
 | 
						-Wno-unused-variable \
 | 
				
			||||||
 | 
						-Wno-type-limits \
 | 
				
			||||||
 | 
						-Wno-unknown-pragmas \
 | 
				
			||||||
 | 
						-Wno-attributes \
 | 
				
			||||||
 | 
						-Ideps/libsodium/builds/msvc \
 | 
				
			||||||
 | 
						-Ideps/libsodium/src/libsodium/include/sodium
 | 
				
			||||||
 | 
					ifneq ($(UNAME_S),OpenBSD)
 | 
				
			||||||
 | 
					$(filter-out $(BUILD_DIR)/win%,$(SODIUM_OBJS)): CFLAGS += \
 | 
				
			||||||
 | 
						-DHAVE_ALLOCA_H
 | 
				
			||||||
 | 
					endif
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					SQLITE_SOURCES := deps/sqlite/sqlite3.c
 | 
				
			||||||
 | 
					SQLITE_OBJS := $(call get_objs,SQLITE_SOURCES)
 | 
				
			||||||
 | 
					$(SQLITE_OBJS): CFLAGS += \
 | 
				
			||||||
 | 
						-DSQLITE_DBCONFIG_DEFAULT_DEFENSIVE \
 | 
				
			||||||
 | 
						-DSQLITE_DEFAULT_MEMSTATUS=0 \
 | 
				
			||||||
 | 
						-DSQLITE_DQS=0 \
 | 
				
			||||||
 | 
						-DSQLITE_ENABLE_MEMSYS5 \
 | 
				
			||||||
 | 
						-DSQLITE_ENABLE_FTS5 \
 | 
				
			||||||
 | 
						-DSQLITE_ENABLE_JSON1 \
 | 
				
			||||||
 | 
						-DSQLITE_LIKE_DOESNT_MATCH_BLOBS \
 | 
				
			||||||
 | 
						-DSQLITE_MAX_ATTACHED=1 \
 | 
				
			||||||
 | 
						-DSQLITE_MAX_COLUMN=100 \
 | 
				
			||||||
 | 
						-DSQLITE_MAX_COMPOUND_SELECT=300 \
 | 
				
			||||||
 | 
						-DSQLITE_MAX_EXPR_DEPTH=40 \
 | 
				
			||||||
 | 
						-DSQLITE_MAX_FUNCTION_ARG=8 \
 | 
				
			||||||
 | 
						-DSQLITE_MAX_LENGTH=5242880 \
 | 
				
			||||||
 | 
						-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_OMIT_DEPRECATED \
 | 
				
			||||||
 | 
						-DSQLITE_OMIT_DESERIALIZE \
 | 
				
			||||||
 | 
						-DSQLITE_OMIT_LOAD_EXTENSION \
 | 
				
			||||||
 | 
						-DSQLITE_OMIT_TCL_VARIABLE \
 | 
				
			||||||
 | 
						-DSQLITE_PRAGMA_DEFAULT_WAL_SYNCHRONOUS=1 \
 | 
				
			||||||
 | 
						-DSQLITE_SECURE_DELETE \
 | 
				
			||||||
 | 
						-DSQLITE_THREADSAFE=2 \
 | 
				
			||||||
 | 
						-DSQLITE_UNTESTABLE \
 | 
				
			||||||
 | 
						-DSQLITE_USE_ALLOCA \
 | 
				
			||||||
 | 
						-DHAVE_ISNAN \
 | 
				
			||||||
 | 
						-DHAVE_GETHOSTUUID=0 \
 | 
				
			||||||
 | 
						-Wno-implicit-fallthrough \
 | 
				
			||||||
 | 
						-Wno-unused-but-set-variable \
 | 
				
			||||||
 | 
						-Wno-unused-function \
 | 
				
			||||||
 | 
						-Wno-unused-variable
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					QUICKJS_SOURCES := \
 | 
				
			||||||
 | 
						deps/quickjs/cutils.c \
 | 
				
			||||||
 | 
						deps/quickjs/libbf.c \
 | 
				
			||||||
 | 
						deps/quickjs/libregexp.c \
 | 
				
			||||||
 | 
						deps/quickjs/libunicode.c \
 | 
				
			||||||
 | 
						deps/quickjs/quickjs.c
 | 
				
			||||||
 | 
					QUICKJS_OBJS := $(call get_objs,QUICKJS_SOURCES)
 | 
				
			||||||
 | 
					$(QUICKJS_OBJS): CFLAGS += \
 | 
				
			||||||
 | 
						-DCONFIG_VERSION=\"$(shell cat deps/quickjs/VERSION)\" \
 | 
				
			||||||
 | 
						-DCONFIG_BIGNUM \
 | 
				
			||||||
 | 
						-D_GNU_SOURCE \
 | 
				
			||||||
 | 
						-Wno-enum-conversion \
 | 
				
			||||||
 | 
						-Wno-implicit-const-int-float-conversion \
 | 
				
			||||||
 | 
						-Wno-implicit-fallthrough \
 | 
				
			||||||
 | 
						-Wno-sign-compare \
 | 
				
			||||||
 | 
						-Wno-unused-but-set-variable \
 | 
				
			||||||
 | 
						-Wno-unused-variable
 | 
				
			||||||
 | 
					$(NONANDROID_TARGETS): CFLAGS += -DDUMP_LEAKS
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					ifeq ($(UNAME_S),Haiku)
 | 
				
			||||||
 | 
					$(QUICKJS_OBJS): CFLAGS += "-Dmalloc_usable_size(x)=0"
 | 
				
			||||||
 | 
					else ifeq ($(UNAME_S),OpenBSD)
 | 
				
			||||||
 | 
					$(QUICKJS_OBJS): CFLAGS += "-Dmalloc_usable_size(x)=0"
 | 
				
			||||||
 | 
					endif
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					LIBBACKTRACE_SOURCES := \
 | 
				
			||||||
 | 
						deps/libbacktrace/atomic.c \
 | 
				
			||||||
 | 
						deps/libbacktrace/backtrace.c \
 | 
				
			||||||
 | 
						deps/libbacktrace/dwarf.c \
 | 
				
			||||||
 | 
						deps/libbacktrace/fileline.c \
 | 
				
			||||||
 | 
						deps/libbacktrace/print.c \
 | 
				
			||||||
 | 
						deps/libbacktrace/simple.c \
 | 
				
			||||||
 | 
						deps/libbacktrace/sort.c \
 | 
				
			||||||
 | 
						deps/libbacktrace/state.c
 | 
				
			||||||
 | 
					LIBBACKTRACE_SOURCES_unix := \
 | 
				
			||||||
 | 
						deps/libbacktrace/elf.c \
 | 
				
			||||||
 | 
						deps/libbacktrace/mmap.c \
 | 
				
			||||||
 | 
						deps/libbacktrace/mmapio.c \
 | 
				
			||||||
 | 
						deps/libbacktrace/posix.c
 | 
				
			||||||
 | 
					LIBBACKTRACE_SOURCES_win := \
 | 
				
			||||||
 | 
						deps/libbacktrace/alloc.c \
 | 
				
			||||||
 | 
						deps/libbacktrace/pecoff.c \
 | 
				
			||||||
 | 
						deps/libbacktrace/posix.c \
 | 
				
			||||||
 | 
						deps/libbacktrace/read.c
 | 
				
			||||||
 | 
					LIBBACKTRACE_SOURCES_macos := \
 | 
				
			||||||
 | 
						deps/libbacktrace/dwarf.c \
 | 
				
			||||||
 | 
						deps/libbacktrace/macho.c \
 | 
				
			||||||
 | 
						deps/libbacktrace/mmap.c \
 | 
				
			||||||
 | 
						deps/libbacktrace/mmapio.c \
 | 
				
			||||||
 | 
						deps/libbacktrace/posix.c
 | 
				
			||||||
 | 
					LIBBACKTRACE_OBJS := $(call get_objs,LIBBACKTRACE_SOURCES)
 | 
				
			||||||
 | 
					$(LIBBACKTRACE_OBJS): CFLAGS += \
 | 
				
			||||||
 | 
						-Ideps/libbacktrace_config \
 | 
				
			||||||
 | 
						-Wno-unused-but-set-variable \
 | 
				
			||||||
 | 
						-Wno-maybe-initialized \
 | 
				
			||||||
 | 
						-Wno-unused-function \
 | 
				
			||||||
 | 
						-DBACKTRACE_ELF_SIZE=64
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					PICOHTTPPARSER_SOURCES := \
 | 
				
			||||||
 | 
						deps/picohttpparser/picohttpparser.c
 | 
				
			||||||
 | 
					PICOHTTPPARSER_OBJS := $(call get_objs,PICOHTTPPARSER_SOURCES)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					MINIUNZIP_SOURCES := \
 | 
				
			||||||
 | 
						deps/zlib/contrib/minizip/unzip.c \
 | 
				
			||||||
 | 
						deps/zlib/contrib/minizip/ioapi.c \
 | 
				
			||||||
 | 
						deps/zlib/adler32.c \
 | 
				
			||||||
 | 
						deps/zlib/crc32.c \
 | 
				
			||||||
 | 
						deps/zlib/inffast.c \
 | 
				
			||||||
 | 
						deps/zlib/inflate.c \
 | 
				
			||||||
 | 
						deps/zlib/inftrees.c \
 | 
				
			||||||
 | 
						deps/zlib/zutil.c
 | 
				
			||||||
 | 
					MINIUNZIP_OBJS := $(call get_objs,MINIUNZIP_SOURCES)
 | 
				
			||||||
 | 
					$(MINIUNZIP_OBJS): CFLAGS += \
 | 
				
			||||||
 | 
						-Ideps/zlib \
 | 
				
			||||||
 | 
						-Wno-maybe-uninitialized
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					LDFLAGS += \
 | 
				
			||||||
 | 
						-pthread \
 | 
				
			||||||
 | 
						-lm
 | 
				
			||||||
 | 
					$(LINUX_TARGETS) $(MACOS_TARGETS) $(IOS_TARGETS) $(IOSSIM_TARGETS): LDFLAGS += \
 | 
				
			||||||
 | 
						-lssl \
 | 
				
			||||||
 | 
						-lcrypto
 | 
				
			||||||
 | 
					ifneq ($(UNAME_S),Haiku)
 | 
				
			||||||
 | 
					ifneq ($(UNAME_S),OpenBSD)
 | 
				
			||||||
 | 
					debug release $(MACOS_TARGETS) $(IOS_TARGETS) $(IOSSIM_TARGETS): LDFLAGS += \
 | 
				
			||||||
 | 
						-ldl
 | 
				
			||||||
 | 
					endif
 | 
				
			||||||
 | 
					endif
 | 
				
			||||||
 | 
					$(WINDOWS_TARGETS): LDFLAGS += \
 | 
				
			||||||
 | 
						-lssl \
 | 
				
			||||||
 | 
						-lcrypto \
 | 
				
			||||||
 | 
						-lcrypt32 \
 | 
				
			||||||
 | 
						-ldbghelp \
 | 
				
			||||||
 | 
						-liphlpapi \
 | 
				
			||||||
 | 
						-lkernel32 \
 | 
				
			||||||
 | 
						-lole32 \
 | 
				
			||||||
 | 
						-luserenv \
 | 
				
			||||||
 | 
						-luuid \
 | 
				
			||||||
 | 
						-lws2_32 \
 | 
				
			||||||
 | 
						-lwsock32
 | 
				
			||||||
 | 
					$(ANDROID_TARGETS): LDFLAGS += \
 | 
				
			||||||
 | 
						-target $(ANDROID_NDK_TARGET_TRIPLE)$(ANDROID_MIN_SDK_VERSION) \
 | 
				
			||||||
 | 
						-ldl \
 | 
				
			||||||
 | 
						-llog \
 | 
				
			||||||
 | 
						-lssl \
 | 
				
			||||||
 | 
						-lcrypto
 | 
				
			||||||
 | 
					$(MACOS_TARGETS) $(IOS_TARGETS) $(IOSSIM_TARGETS): CFLAGS += \
 | 
				
			||||||
 | 
						-Wno-unknown-warning-option
 | 
				
			||||||
 | 
					$(IOS_TARGETS) $(IOSSIM_TARGETS): LDFLAGS += \
 | 
				
			||||||
 | 
						-framework Foundation \
 | 
				
			||||||
 | 
						-framework CoreFoundation \
 | 
				
			||||||
 | 
						-framework UIKit \
 | 
				
			||||||
 | 
						-framework WebKit
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					unix: debug release
 | 
				
			||||||
 | 
					win: windebug winrelease
 | 
				
			||||||
 | 
					all: $(BUILD_TYPES) default.nix
 | 
				
			||||||
 | 
					.PHONY: all win unix
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					ALL_APP_OBJS := \
 | 
				
			||||||
 | 
						$(APP_OBJS) \
 | 
				
			||||||
 | 
						$(BLOWFISH_OBJS) \
 | 
				
			||||||
 | 
						$(LIBBACKTRACE_OBJS) \
 | 
				
			||||||
 | 
						$(MINIUNZIP_OBJS) \
 | 
				
			||||||
 | 
						$(PICOHTTPPARSER_OBJS) \
 | 
				
			||||||
 | 
						$(QUICKJS_OBJS) \
 | 
				
			||||||
 | 
						$(SODIUM_OBJS) \
 | 
				
			||||||
 | 
						$(SQLITE_OBJS) \
 | 
				
			||||||
 | 
						$(UV_OBJS)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					DEPS = $(ALL_APP_OBJS:.o=.d)
 | 
				
			||||||
 | 
					-include $(DEPS)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					define build_rules
 | 
				
			||||||
 | 
					$(1): $(BUILD_DIR)/$(1)/$(PROJECT)$(if $(filter win%,$(1)),.exe)
 | 
				
			||||||
 | 
					.PHONY: $(1)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					$(BUILD_DIR)/$(1)/$(PROJECT)$(if $(filter win%,$(1)),.exe): $(filter $(BUILD_DIR)/$(1)/%,$(ALL_APP_OBJS))
 | 
				
			||||||
 | 
						@echo "[link] $$@"
 | 
				
			||||||
 | 
						@$$(CC) -o $$@ $$^ $$(LDFLAGS)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					$(BUILD_DIR)/$(1)/%.o: %.c
 | 
				
			||||||
 | 
						@mkdir -p $$(dir $$@)
 | 
				
			||||||
 | 
						@echo "[c] $$@"
 | 
				
			||||||
 | 
						@$$(CC) $$(CFLAGS) -c $$< -o $$@
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					$(BUILD_DIR)/$(1)/%.o: %.m
 | 
				
			||||||
 | 
						@mkdir -p $$(dir $$@)
 | 
				
			||||||
 | 
						@echo "[m] $$@"
 | 
				
			||||||
 | 
						@$$(CC) $$(CFLAGS) -c $$< -o $$@
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					$(BUILD_DIR)/$(1)/%.o: %.S
 | 
				
			||||||
 | 
						@mkdir -p $$(dir $$@)
 | 
				
			||||||
 | 
						@echo "[as] $$@"
 | 
				
			||||||
 | 
						@$$(AS) -c $$< -o $$@
 | 
				
			||||||
 | 
					endef
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					$(foreach build_type,$(BUILD_TYPES),$(eval $(call build_rules,$(build_type))))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					src/version.h : $(firstword $(MAKEFILE_LIST))
 | 
				
			||||||
 | 
						@echo "[version] $@"
 | 
				
			||||||
 | 
						@echo "#define VERSION_NUMBER \"$(VERSION_NUMBER)\"" > $@
 | 
				
			||||||
 | 
						@echo "#define VERSION_NAME \"$(VERSION_NAME)\"" >> $@
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					src/android/AndroidManifest.xml : $(firstword $(MAKEFILE_LIST))
 | 
				
			||||||
 | 
						@echo "[android_version] $@"
 | 
				
			||||||
 | 
						@sed -i \
 | 
				
			||||||
 | 
							-e 's/versionCode=".*"/versionCode="$(VERSION_CODE)"/' \
 | 
				
			||||||
 | 
							-e 's/versionName=".*"/versionName="$(VERSION_NUMBER)"/' \
 | 
				
			||||||
 | 
							-e 's/android:minSdkVersion="[[:digit:]]*"/android:minSdkVersion="$(ANDROID_MIN_SDK_VERSION)"/' \
 | 
				
			||||||
 | 
							-e 's/android:targetSdkVersion="[[:digit:]]*"/android:targetSdkVersion="$(ANDROID_TARGET_SDK_VERSION)"/' \
 | 
				
			||||||
 | 
							$@
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					default.nix : $(firstword $(MAKEFILE_LIST))
 | 
				
			||||||
 | 
						@echo "[version] $@"
 | 
				
			||||||
 | 
						@sed -i -e 's/version = ".*";/version = "$(VERSION_NUMBER)";/' $@
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Android support.
 | 
				
			||||||
 | 
					out/res/layout_activity_main.xml.flat: src/android/res/layout/activity_main.xml
 | 
				
			||||||
 | 
						@mkdir -p $(dir $@)
 | 
				
			||||||
 | 
						@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
 | 
				
			||||||
 | 
						@mkdir -p $(dir $@)
 | 
				
			||||||
 | 
						@echo "[aapt2] $@"
 | 
				
			||||||
 | 
						@$(ANDROID_BUILD_TOOLS)/aapt2 compile -o out/res/ src/android/res/drawable/icon.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 src/android/AndroidManifest.xml
 | 
				
			||||||
 | 
						@mkdir -p $(dir $@)
 | 
				
			||||||
 | 
						@$(ANDROID_BUILD_TOOLS)/aapt2 link -I $(ANDROID_PLATFORM)/android.jar out/res/layout_activity_main.xml.flat out/res/drawable_icon.xml.flat --manifest src/android/AndroidManifest.xml -o out/apk/res.apk --java out/gen/
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					JAVA_FILES := out/gen/com/unprompted/tildefriends/R.java $(wildcard src/android/com/unprompted/tildefriends/*.java)
 | 
				
			||||||
 | 
					CLASS_FILES := $(foreach src,$(JAVA_FILES),out/classes/com/unprompted/tildefriends/$(notdir $(src:.java=.class)))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					$(CLASS_FILES) &: $(JAVA_FILES)
 | 
				
			||||||
 | 
						@echo "[javac] $(CLASS_FILES)"
 | 
				
			||||||
 | 
						@javac --release 8 -encoding UTF-8 -Xlint:deprecation -XDuseUnsharedTable=true -classpath $(ANDROID_PLATFORM)/android.jar:$(ANDROID_BUILD_TOOLS)/core-lambda-stubs.jar -d out/classes $(JAVA_FILES)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					out/apk/classes.dex: $(CLASS_FILES)
 | 
				
			||||||
 | 
						@mkdir -p $(dir $@)
 | 
				
			||||||
 | 
						@echo "[d8] $@"
 | 
				
			||||||
 | 
						@$(ANDROID_BUILD_TOOLS)/d8 --$(BUILD_TYPE) --lib $(ANDROID_PLATFORM)/android.jar --output $(dir $@) out/classes/com/unprompted/tildefriends/*.class
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					PACKAGE_DIRS := \
 | 
				
			||||||
 | 
						apps/ \
 | 
				
			||||||
 | 
						core/ \
 | 
				
			||||||
 | 
						deps/codemirror/ \
 | 
				
			||||||
 | 
						deps/prettier/ \
 | 
				
			||||||
 | 
						deps/lit/
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					RAW_FILES := $(filter-out apps/blog% apps/issues% apps/welcome% apps/journal% %.map, $(shell find $(PACKAGE_DIRS) -type f))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					out/apk/TildeFriends-arm-debug.unsigned.apk: BUILD_TYPE := debug
 | 
				
			||||||
 | 
					out/apk/TildeFriends-arm-release.unsigned.apk: BUILD_TYPE := release
 | 
				
			||||||
 | 
					out/apk/TildeFriends-x86-debug.unsigned.apk: BUILD_TYPE := debug
 | 
				
			||||||
 | 
					out/apk/TildeFriends-x86-release.unsigned.apk: BUILD_TYPE := release
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					out/apk/TildeFriends-arm-debug.unsigned.apk: out/apk/classes.dex out/androiddebug/tildefriends out/androiddebug-armv7a/tildefriends $(RAW_FILES) out/apk/res.apk
 | 
				
			||||||
 | 
					out/apk/TildeFriends-arm-release.unsigned.apk: out/apk/classes.dex out/androidrelease/tildefriends out/androidrelease-armv7a/tildefriends $(RAW_FILES) out/apk/res.apk
 | 
				
			||||||
 | 
					out/apk/TildeFriends-x86-debug.unsigned.apk: out/apk/classes.dex out/androiddebug-x86_64/tildefriends out/androiddebug-x86/tildefriends $(RAW_FILES) out/apk/res.apk
 | 
				
			||||||
 | 
					out/apk/TildeFriends-x86-release.unsigned.apk: out/apk/classes.dex out/androidrelease-x86_64/tildefriends out/androidrelease-x86/tildefriends $(RAW_FILES) out/apk/res.apk
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					out/apk/TildeFriends-arm-%.unsigned.apk:
 | 
				
			||||||
 | 
						@mkdir -p $(dir $@) out/apk-arm-$(BUILD_TYPE)/lib/arm64-v8a/ out/apk-arm-$(BUILD_TYPE)/lib/armeabi-v7a/
 | 
				
			||||||
 | 
						@echo "[aapt] $@"
 | 
				
			||||||
 | 
						@cp out/android$(BUILD_TYPE)/tildefriends out/apk-arm-$(BUILD_TYPE)/lib/arm64-v8a/tildefriends.so
 | 
				
			||||||
 | 
						@cp out/android$(BUILD_TYPE)-armv7a/tildefriends out/apk-arm-$(BUILD_TYPE)/lib/armeabi-v7a/tildefriends.so
 | 
				
			||||||
 | 
						@$(ANDROID_NDK)/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-strip out/apk-arm-$(BUILD_TYPE)/lib/arm64-v8a/tildefriends.so
 | 
				
			||||||
 | 
						@$(ANDROID_NDK)/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-strip out/apk-arm-$(BUILD_TYPE)/lib/armeabi-v7a/tildefriends.so
 | 
				
			||||||
 | 
						@cp out/apk/res.apk $@.zip
 | 
				
			||||||
 | 
						@cp out/apk/classes.dex out/apk-arm-$(BUILD_TYPE)/
 | 
				
			||||||
 | 
						@cd out/apk-arm-$(BUILD_TYPE) && zip -u ../../$@.zip -q -9 -r . && cd ../../
 | 
				
			||||||
 | 
						@zip -u $@.zip -q -9 $(RAW_FILES)
 | 
				
			||||||
 | 
						@$(ANDROID_BUILD_TOOLS)/zipalign -f 4 $@.zip $@
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					out/apk/TildeFriends-x86-%.unsigned.apk:
 | 
				
			||||||
 | 
						@mkdir -p $(dir $@) out/apk-x86-$(BUILD_TYPE)/lib/x86_64/ out/apk-x86-$(BUILD_TYPE)/lib/x86/
 | 
				
			||||||
 | 
						@echo "[aapt] $@"
 | 
				
			||||||
 | 
						@cp out/android$(BUILD_TYPE)-x86_64/tildefriends out/apk-x86-$(BUILD_TYPE)/lib/x86_64/tildefriends.so
 | 
				
			||||||
 | 
						@cp out/android$(BUILD_TYPE)-x86/tildefriends out/apk-x86-$(BUILD_TYPE)/lib/x86/tildefriends.so
 | 
				
			||||||
 | 
						@$(ANDROID_NDK)/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-strip out/apk-x86-$(BUILD_TYPE)/lib/x86_64/tildefriends.so
 | 
				
			||||||
 | 
						@$(ANDROID_NDK)/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-strip out/apk-x86-$(BUILD_TYPE)/lib/x86/tildefriends.so
 | 
				
			||||||
 | 
						@cp out/apk/res.apk $@.zip
 | 
				
			||||||
 | 
						@cp out/apk/classes.dex out/apk-x86-$(BUILD_TYPE)/
 | 
				
			||||||
 | 
						@cd out/apk-x86-$(BUILD_TYPE) && zip -u ../../$@.zip -q -9 -r . && cd ../../
 | 
				
			||||||
 | 
						@zip -u $@.zip -q -9 $(RAW_FILES)
 | 
				
			||||||
 | 
						@$(ANDROID_BUILD_TOOLS)/zipalign -f 4 $@.zip $@
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					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 $@ $<
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					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
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					release-apk: out/TildeFriends-arm-release.zopfli.apk out/TildeFriends-x86-release.zopfli.apk
 | 
				
			||||||
 | 
					.PHONY: release-apk
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					releaseapkgo: out/TildeFriends-arm-release.apk
 | 
				
			||||||
 | 
						@adb install -r $<
 | 
				
			||||||
 | 
						@adb shell am start com.unprompted.tildefriends/.TildeFriendsActivity
 | 
				
			||||||
 | 
					.PHONY: releaseapkgo
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# iOS Support
 | 
				
			||||||
 | 
					out/%.app/Info.plist: src/ios/Info.plist
 | 
				
			||||||
 | 
						@mkdir -p $(dir $@)
 | 
				
			||||||
 | 
						@cp -v $< $@
 | 
				
			||||||
 | 
					out/%.app/tildefriends.png: src/ios/tildefriends.png
 | 
				
			||||||
 | 
						@mkdir -p $(dir $@)
 | 
				
			||||||
 | 
						@cp -v $< $@
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					out/data.zip: $(RAW_FILES)
 | 
				
			||||||
 | 
						@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
 | 
				
			||||||
 | 
						@mkdir -p $(dir $@)
 | 
				
			||||||
 | 
						@cp -v $< $@
 | 
				
			||||||
 | 
					ifeq ($(HAVE_LINUX_IOS),1)
 | 
				
			||||||
 | 
						@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 -R $(dir $<)/* $@.tmp/Payload/tildefriends.app/
 | 
				
			||||||
 | 
						@cd $@.tmp/ && zip -u ../../$@ -q -9 -r ./
 | 
				
			||||||
 | 
						@rm -rf $@.tmp/
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					out/%/tildefriends.standalone: out/%/tildefriends out/data.zip
 | 
				
			||||||
 | 
						@echo "[standalone] $@"
 | 
				
			||||||
 | 
						@cat $< out/data.zip > $@
 | 
				
			||||||
 | 
						@chmod +x $@
 | 
				
			||||||
 | 
					out/%/tildefriends.standalone.exe: out/%/tildefriends.exe out/data.zip
 | 
				
			||||||
 | 
						@echo "[standalone] $@"
 | 
				
			||||||
 | 
						@cat $< out/data.zip > $@
 | 
				
			||||||
 | 
						@chmod +x $@
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					iossimdebug-app: out/tildefriends-iossimdebug.app/tildefriends
 | 
				
			||||||
 | 
					iossimrelease-app: out/tildefriends-iossimrelease.app/tildefriends
 | 
				
			||||||
 | 
					iosdebug-app: out/tildefriends-iosdebug.app/tildefriends
 | 
				
			||||||
 | 
					iosrelease-app: out/tildefriends-iosrelease.app/tildefriends
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					iosdebug-ipa: out/tildefriends-debug.ipa
 | 
				
			||||||
 | 
					iosrelease-ipa: out/tildefriends-release.ipa
 | 
				
			||||||
 | 
					.PHONY: iossimdebug-app iossimrelease-app iosdebug-app iosrelease-app
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					ios%go: out/tildefriends-ios%.app/tildefriends
 | 
				
			||||||
 | 
						ideviceinstaller -i $(realpath $(dir $<))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					iossimdebuggo: out/tildefriends-iossimdebug.app/tildefriends
 | 
				
			||||||
 | 
						xcrun simctl install booted out/tildefriends-iossimdebug.app/
 | 
				
			||||||
 | 
						xcrun simctl launch booted com.unprompted.tildefriends
 | 
				
			||||||
 | 
					.PHONY: iossimdebuggo
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					apklog:
 | 
				
			||||||
 | 
						@adb logcat *:S tildefriends
 | 
				
			||||||
 | 
					.PHONY: apklog
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					fetchdeps:
 | 
				
			||||||
 | 
						@echo "[fetch] libuv"
 | 
				
			||||||
 | 
						@test -f out/deps/libuv.tar.gz && test "$$(cat out/deps/libuv.txt 2>/dev/null)" = $(LIBUV_URL) || (mkdir -p out/deps/ && curl -q $(LIBUV_URL) -o out/deps/libuv.tar.gz)
 | 
				
			||||||
 | 
						@test -d deps/libuv/ && test "$$(cat out/deps/libuv.txt 2>/dev/null)" = $(LIBUV_URL) || (rm -rf deps/libuv/ && mkdir -p deps/libuv/ && tar -C deps/libuv/ -m --strip=1 -xf out/deps/libuv.tar.gz)
 | 
				
			||||||
 | 
						@echo -n $(LIBUV_URL) > out/deps/libuv.txt
 | 
				
			||||||
 | 
						@echo "[fetch] sqlite"
 | 
				
			||||||
 | 
						@test -f out/deps/sqlite.zip && test "$$(cat out/deps/sqlite.txt 2>/dev/null)" = $(SQLITE_URL) || (mkdir -p out/deps/ && curl -q $(SQLITE_URL) -o out/deps/sqlite.zip)
 | 
				
			||||||
 | 
						@test -d deps/sqlite/ && test "$$(cat out/deps/sqlite.txt 2>/dev/null)" = $(SQLITE_URL) || (mkdir -p deps/sqlite/ && unzip -qDjo -d deps/sqlite/ out/deps/sqlite.zip)
 | 
				
			||||||
 | 
						@echo -n $(SQLITE_URL) > out/deps/sqlite.txt
 | 
				
			||||||
 | 
						@echo "[fetch] prettier"
 | 
				
			||||||
 | 
						@test -f deps/prettier/standalone.mjs || curl -q --create-dirs -O --output-dir deps/prettier/ https://cdn.jsdelivr.net/npm/prettier@3.2.5/standalone.mjs
 | 
				
			||||||
 | 
						@test -f deps/prettier/html.mjs || curl -q --create-dirs -O --output-dir deps/prettier/ https://cdn.jsdelivr.net/npm/prettier@3.2.5/plugins/html.mjs
 | 
				
			||||||
 | 
						@test -f deps/prettier/babel.mjs || curl -q --create-dirs -O --output-dir deps/prettier/ https://cdn.jsdelivr.net/npm/prettier@3.2.5/plugins/babel.mjs
 | 
				
			||||||
 | 
						@test -f deps/prettier/estree.mjs || curl -q --create-dirs -O --output-dir deps/prettier/ https://cdn.jsdelivr.net/npm/prettier@3.2.5/plugins/estree.mjs
 | 
				
			||||||
 | 
					.PHONY: fetchdeps
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					ANDROID_DEPS := deps/openssl/android/arm64-v8a/usr/local/lib/libssl.a
 | 
				
			||||||
 | 
					$(ANDROID_DEPS):
 | 
				
			||||||
 | 
						+@tools/ssl-android
 | 
				
			||||||
 | 
					$(filter $(BUILD_DIR)/android%,$(APP_OBJS)): | $(ANDROID_DEPS)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					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
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					clean:
 | 
				
			||||||
 | 
						rm -rf $(BUILD_DIR)
 | 
				
			||||||
 | 
					.PHONY: clean
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					dist: release-apk iosrelease-ipa $(if $(HAVE_WIN), out/winrelease/tildefriends.standalone.exe) default.nix
 | 
				
			||||||
 | 
						@echo [archive] dist/tildefriends-$(VERSION_NUMBER).tar.xz
 | 
				
			||||||
 | 
						@rm -rf out/tildefriends-$(VERSION_NUMBER)
 | 
				
			||||||
 | 
						@mkdir -p dist/ out/tildefriends-$(VERSION_NUMBER)
 | 
				
			||||||
 | 
						@git ls-files --recurse-submodules | tar -c -T- | tar -x -C out/tildefriends-$(VERSION_NUMBER)
 | 
				
			||||||
 | 
						@tar \
 | 
				
			||||||
 | 
							--exclude=apps/welcome* \
 | 
				
			||||||
 | 
							--exclude=deps/libbacktrace/Isaac.Newton-Opticks.txt \
 | 
				
			||||||
 | 
							--exclude=deps/libsodium/builds/msvc/vs* \
 | 
				
			||||||
 | 
							--exclude=deps/libsodium/builds/msvc/build \
 | 
				
			||||||
 | 
							--exclude=deps/libsodium/builds/msvc/properties \
 | 
				
			||||||
 | 
							--exclude=deps/libsodium/configure \
 | 
				
			||||||
 | 
							--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 \
 | 
				
			||||||
 | 
							--exclude=deps/zlib/doc \
 | 
				
			||||||
 | 
							-caf dist/tildefriends-$(VERSION_NUMBER).tar.xz \
 | 
				
			||||||
 | 
							-C out/ \
 | 
				
			||||||
 | 
							tildefriends-$(VERSION_NUMBER)
 | 
				
			||||||
 | 
						@echo "[cp] TildeFriends-x86-$(VERSION_NUMBER).apk"
 | 
				
			||||||
 | 
						@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_WIN) && echo "[cp] tildefriends-$(VERSION_NUMBER).exe"
 | 
				
			||||||
 | 
						@test $(HAVE_WIN) && cp out/winrelease/tildefriends.standalone.exe dist/tildefriends-$(VERSION_NUMBER).exe
 | 
				
			||||||
 | 
					.PHONY: dist
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					dist-test: dist
 | 
				
			||||||
 | 
						@tar -xf tildefriends-$(VERSION_NUMBER).tar.xz
 | 
				
			||||||
 | 
						@$(MAKE) -C tildefriends-$(VERSION_NUMBER)/ debug release
 | 
				
			||||||
 | 
						@docker build tildefriends-$(VERSION_NUMBER)/
 | 
				
			||||||
 | 
						@rm -rf tildefriends-$(VERSION_NUMBER)
 | 
				
			||||||
 | 
					.PHONY: dist-test
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					format:
 | 
				
			||||||
 | 
						@clang-format -i $(wildcard src/*.c src/*.h src/*.m)
 | 
				
			||||||
 | 
					.PHONY: format
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					prettier:
 | 
				
			||||||
 | 
						@npm run prettier
 | 
				
			||||||
 | 
					.PHONY: prettier
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					docs:
 | 
				
			||||||
 | 
						@doxygen
 | 
				
			||||||
 | 
					.PHONY: docs
 | 
				
			||||||
							
								
								
									
										497
									
								
								Makefile
									
									
									
									
									
								
							
							
						
						
									
										497
									
								
								Makefile
									
									
									
									
									
								
							@@ -1,497 +0,0 @@
 | 
				
			|||||||
.ONESHELL:
 | 
					 | 
				
			||||||
.DELETE_ON_ERROR:
 | 
					 | 
				
			||||||
MAKEFLAGS += --warn-undefined-variables
 | 
					 | 
				
			||||||
MAKEFLAGS += --no-builtin-rules
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
PROJECT = tildefriends
 | 
					 | 
				
			||||||
BUILD_DIR ?= out
 | 
					 | 
				
			||||||
BUILD_TYPES := debug release windebug winrelease androiddebug androidrelease androiddebug-x86_64 androidrelease-x86_64
 | 
					 | 
				
			||||||
UNAME_M := $(shell uname -m)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
CFLAGS += \
 | 
					 | 
				
			||||||
	-Wall \
 | 
					 | 
				
			||||||
	-Wextra \
 | 
					 | 
				
			||||||
	-Wno-unused-parameter \
 | 
					 | 
				
			||||||
	-Wno-cast-function-type \
 | 
					 | 
				
			||||||
	-MMD \
 | 
					 | 
				
			||||||
	-ffunction-sections \
 | 
					 | 
				
			||||||
	-fdata-sections \
 | 
					 | 
				
			||||||
	-fno-exceptions \
 | 
					 | 
				
			||||||
	-g
 | 
					 | 
				
			||||||
LDFLAGS += -Wl,--gc-sections
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
ANDROID_SDK ?= ~/Android/Sdk
 | 
					 | 
				
			||||||
ANDROID_BUILD_TOOLS := $(ANDROID_SDK)/build-tools/33.0.1
 | 
					 | 
				
			||||||
ANDROID_PLATFORM := $(ANDROID_SDK)/platforms/android-33
 | 
					 | 
				
			||||||
ANDROID_NDK ?= $(ANDROID_SDK)/ndk/23.1.7779620
 | 
					 | 
				
			||||||
ANDROID_NDK_API_VERSION := 31
 | 
					 | 
				
			||||||
ANDROID_MIN_SDK_VERSION := 26
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
ANDROID_ARM64_TARGETS := \
 | 
					 | 
				
			||||||
	out/androiddebug/tildefriends \
 | 
					 | 
				
			||||||
	out/androidrelease/tildefriends
 | 
					 | 
				
			||||||
ANDROID_X86_64_TARGETS := \
 | 
					 | 
				
			||||||
	out/androiddebug-x86_64/tildefriends \
 | 
					 | 
				
			||||||
	out/androidrelease-x86_64/tildefriends
 | 
					 | 
				
			||||||
ANDROID_TARGETS := \
 | 
					 | 
				
			||||||
	$(ANDROID_X86_64_TARGETS) \
 | 
					 | 
				
			||||||
	$(ANDROID_ARM64_TARGETS)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
DEBUG_TARGETS := \
 | 
					 | 
				
			||||||
	out/debug/tildefriends \
 | 
					 | 
				
			||||||
	out/windebug/tildefriends \
 | 
					 | 
				
			||||||
	out/androiddebug/tildefriends \
 | 
					 | 
				
			||||||
	out/androiddebug-x86_64/tildefriends
 | 
					 | 
				
			||||||
RELEASE_TARGETS := \
 | 
					 | 
				
			||||||
	out/release/tildefriends \
 | 
					 | 
				
			||||||
	out/winrelease/tildefriends \
 | 
					 | 
				
			||||||
	out/androidrelease/tildefriends \
 | 
					 | 
				
			||||||
	out/androidrelease-x86_64/tildefriends
 | 
					 | 
				
			||||||
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),$(DEBUG_TARGETS) $(RELEASE_TARGETS))
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
$(NONANDROID_TARGETS): CFLAGS += -fno-omit-frame-pointer
 | 
					 | 
				
			||||||
$(NONANDROID_TARGETS): LDFLAGS += -rdynamic
 | 
					 | 
				
			||||||
$(ANDROID_TARGETS): CFLAGS += \
 | 
					 | 
				
			||||||
	--sysroot $(ANDROID_NDK)/toolchains/llvm/prebuilt/linux-x86_64/sysroot \
 | 
					 | 
				
			||||||
	-fPIC \
 | 
					 | 
				
			||||||
	-fomit-frame-pointer \
 | 
					 | 
				
			||||||
	-fno-asynchronous-unwind-tables
 | 
					 | 
				
			||||||
$(ANDROID_TARGETS): LDFLAGS += --sysroot $(ANDROID_NDK)/toolchains/llvm/prebuilt/linux-x86_64/sysroot -fPIC
 | 
					 | 
				
			||||||
$(DEBUG_TARGETS): CFLAGS += -DDEBUG -Og
 | 
					 | 
				
			||||||
$(RELEASE_TARGETS): CFLAGS += -DNDEBUG
 | 
					 | 
				
			||||||
$(NONANDROID_RELEASE_TARGETS): CFLAGS += -O3
 | 
					 | 
				
			||||||
$(ANDROID_RELEASE_TARGETS): CFLAGS += -Os
 | 
					 | 
				
			||||||
windebug winrelease: CC = x86_64-w64-mingw32-gcc-win32
 | 
					 | 
				
			||||||
windebug winrelease: AS = $(CC)
 | 
					 | 
				
			||||||
windebug winrelease: CFLAGS += \
 | 
					 | 
				
			||||||
	-D_WIN32_WINNT=0x0A00 \
 | 
					 | 
				
			||||||
	-DWINVER=0x0A00 \
 | 
					 | 
				
			||||||
	-DNTDDI_VERSION=NTDDI_WIN10 \
 | 
					 | 
				
			||||||
	-Ideps/openssl/mingw64/include
 | 
					 | 
				
			||||||
windebug winrelease: LDFLAGS += \
 | 
					 | 
				
			||||||
	-static \
 | 
					 | 
				
			||||||
	-lm \
 | 
					 | 
				
			||||||
	-Ldeps/openssl/mingw64/lib
 | 
					 | 
				
			||||||
$(ANDROID_X86_64_TARGETS): ANDROID_NDK_TARGET_TRIPLE := x86_64-linux-android
 | 
					 | 
				
			||||||
$(ANDROID_ARM64_TARGETS): ANDROID_NDK_TARGET_TRIPLE := aarch64-linux-android
 | 
					 | 
				
			||||||
$(ANDROID_TARGETS): CC = $(ANDROID_NDK)/toolchains/llvm/prebuilt/linux-x86_64/bin/clang
 | 
					 | 
				
			||||||
$(ANDROID_TARGETS): AS = $(CC)
 | 
					 | 
				
			||||||
$(ANDROID_TARGETS): CFLAGS += \
 | 
					 | 
				
			||||||
	-target $(ANDROID_NDK_TARGET_TRIPLE)$(ANDROID_NDK_API_VERSION) \
 | 
					 | 
				
			||||||
	-Wno-unknown-warning-option
 | 
					 | 
				
			||||||
$(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_64_TARGETS): CFLAGS += -Ideps/openssl/android/x86_64/usr/local/include
 | 
					 | 
				
			||||||
$(ANDROID_X86_64_TARGETS): LDFLAGS += -Ldeps/openssl/android/x86_64/usr/local/lib
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
ifeq ($(UNAME_M),x86_64)
 | 
					 | 
				
			||||||
debug: CFLAGS += -fsanitize=address -fsanitize=undefined -fno-common
 | 
					 | 
				
			||||||
debug: LDFLAGS += -fsanitize=address -fsanitize=undefined
 | 
					 | 
				
			||||||
endif
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
get_objs = \
 | 
					 | 
				
			||||||
	$(foreach build_type,$(BUILD_TYPES),$(addprefix $(BUILD_DIR)/$(build_type)/,$(addsuffix .o,$(basename $(value $(1)))))) \
 | 
					 | 
				
			||||||
	$(foreach build_type,debug release,$(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_64 androidrelease-x86_64,$(addprefix $(BUILD_DIR)/$(build_type)/,$(addsuffix .o,$(basename $(value $(1)_android))))) \
 | 
					 | 
				
			||||||
	$(foreach build_type,androiddebug androidrelease androiddebug-x86_64 androidrelease-x86_64,$(addprefix $(BUILD_DIR)/$(build_type)/,$(addsuffix .o,$(basename $(value $(1)_unix)))))
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
APP_SOURCES := $(wildcard src/*.c)
 | 
					 | 
				
			||||||
APP_OBJS := $(call get_objs,APP_SOURCES)
 | 
					 | 
				
			||||||
$(APP_OBJS): CFLAGS += \
 | 
					 | 
				
			||||||
	-Ideps/base64c/include \
 | 
					 | 
				
			||||||
	-Ideps/crypt_blowfish \
 | 
					 | 
				
			||||||
	-Ideps/libbacktrace \
 | 
					 | 
				
			||||||
	-Ideps/libsodium \
 | 
					 | 
				
			||||||
	-Ideps/libsodium/src/libsodium/include \
 | 
					 | 
				
			||||||
	-Ideps/libuv/include \
 | 
					 | 
				
			||||||
	-Ideps/zlib \
 | 
					 | 
				
			||||||
	-Ideps/zlib/contrib/minizip \
 | 
					 | 
				
			||||||
	-Ideps/picohttpparser \
 | 
					 | 
				
			||||||
	-Ideps/quickjs \
 | 
					 | 
				
			||||||
	-Ideps/sqlite \
 | 
					 | 
				
			||||||
	-Ideps/valgrind \
 | 
					 | 
				
			||||||
	-Ideps/xopt \
 | 
					 | 
				
			||||||
	-Werror
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
BLOWFISH_SOURCES := \
 | 
					 | 
				
			||||||
	deps/crypt_blowfish/crypt_blowfish.c \
 | 
					 | 
				
			||||||
	deps/crypt_blowfish/crypt_gensalt.c \
 | 
					 | 
				
			||||||
	deps/crypt_blowfish/wrapper.c
 | 
					 | 
				
			||||||
BLOWFISH_SOURCES_win = \
 | 
					 | 
				
			||||||
	deps/crypt_blowfish/x86.S
 | 
					 | 
				
			||||||
BLOWFISH_OBJS := $(call get_objs,BLOWFISH_SOURCES)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
UV_SOURCES := \
 | 
					 | 
				
			||||||
	deps/libuv/src/fs-poll.c \
 | 
					 | 
				
			||||||
	deps/libuv/src/idna.c \
 | 
					 | 
				
			||||||
	deps/libuv/src/inet.c \
 | 
					 | 
				
			||||||
	deps/libuv/src/random.c \
 | 
					 | 
				
			||||||
	deps/libuv/src/strscpy.c \
 | 
					 | 
				
			||||||
	deps/libuv/src/strtok.c \
 | 
					 | 
				
			||||||
	deps/libuv/src/threadpool.c \
 | 
					 | 
				
			||||||
	deps/libuv/src/timer.c \
 | 
					 | 
				
			||||||
	deps/libuv/src/uv-common.c \
 | 
					 | 
				
			||||||
	deps/libuv/src/uv-data-getter-setters.c \
 | 
					 | 
				
			||||||
	deps/libuv/src/version.c
 | 
					 | 
				
			||||||
UV_SOURCES_unix := \
 | 
					 | 
				
			||||||
	deps/libuv/src/unix/async.c \
 | 
					 | 
				
			||||||
	deps/libuv/src/unix/core.c \
 | 
					 | 
				
			||||||
	deps/libuv/src/unix/dl.c \
 | 
					 | 
				
			||||||
	deps/libuv/src/unix/epoll.c \
 | 
					 | 
				
			||||||
	deps/libuv/src/unix/fs.c \
 | 
					 | 
				
			||||||
	deps/libuv/src/unix/getaddrinfo.c \
 | 
					 | 
				
			||||||
	deps/libuv/src/unix/getnameinfo.c \
 | 
					 | 
				
			||||||
	deps/libuv/src/unix/linux-core.c \
 | 
					 | 
				
			||||||
	deps/libuv/src/unix/linux-inotify.c \
 | 
					 | 
				
			||||||
	deps/libuv/src/unix/linux-syscalls.c \
 | 
					 | 
				
			||||||
	deps/libuv/src/unix/loop-watcher.c \
 | 
					 | 
				
			||||||
	deps/libuv/src/unix/loop.c \
 | 
					 | 
				
			||||||
	deps/libuv/src/unix/pipe.c \
 | 
					 | 
				
			||||||
	deps/libuv/src/unix/poll.c \
 | 
					 | 
				
			||||||
	deps/libuv/src/unix/process.c \
 | 
					 | 
				
			||||||
	deps/libuv/src/unix/procfs-exepath.c \
 | 
					 | 
				
			||||||
	deps/libuv/src/unix/proctitle.c \
 | 
					 | 
				
			||||||
	deps/libuv/src/unix/random-devurandom.c \
 | 
					 | 
				
			||||||
	deps/libuv/src/unix/random-getrandom.c \
 | 
					 | 
				
			||||||
	deps/libuv/src/unix/random-sysctl-linux.c \
 | 
					 | 
				
			||||||
	deps/libuv/src/unix/signal.c \
 | 
					 | 
				
			||||||
	deps/libuv/src/unix/stream.c \
 | 
					 | 
				
			||||||
	deps/libuv/src/unix/tcp.c \
 | 
					 | 
				
			||||||
	deps/libuv/src/unix/thread.c \
 | 
					 | 
				
			||||||
	deps/libuv/src/unix/tty.c \
 | 
					 | 
				
			||||||
	deps/libuv/src/unix/udp.c
 | 
					 | 
				
			||||||
UV_SOURCES_android := \
 | 
					 | 
				
			||||||
	deps/libuv/src/unix/pthread-fixes.c \
 | 
					 | 
				
			||||||
	deps/libuv/src/unix/random-getentropy.c
 | 
					 | 
				
			||||||
UV_SOURCES_win := \
 | 
					 | 
				
			||||||
	deps/libuv/src/win/async.c \
 | 
					 | 
				
			||||||
	deps/libuv/src/win/core.c \
 | 
					 | 
				
			||||||
	deps/libuv/src/win/detect-wakeup.c \
 | 
					 | 
				
			||||||
	deps/libuv/src/win/dl.c \
 | 
					 | 
				
			||||||
	deps/libuv/src/win/error.c \
 | 
					 | 
				
			||||||
	deps/libuv/src/win/fs-event.c \
 | 
					 | 
				
			||||||
	deps/libuv/src/win/fs.c \
 | 
					 | 
				
			||||||
	deps/libuv/src/win/getaddrinfo.c \
 | 
					 | 
				
			||||||
	deps/libuv/src/win/getnameinfo.c \
 | 
					 | 
				
			||||||
	deps/libuv/src/win/handle.c \
 | 
					 | 
				
			||||||
	deps/libuv/src/win/loop-watcher.c \
 | 
					 | 
				
			||||||
	deps/libuv/src/win/pipe.c \
 | 
					 | 
				
			||||||
	deps/libuv/src/win/poll.c \
 | 
					 | 
				
			||||||
	deps/libuv/src/win/process-stdio.c \
 | 
					 | 
				
			||||||
	deps/libuv/src/win/process.c \
 | 
					 | 
				
			||||||
	deps/libuv/src/win/signal.c \
 | 
					 | 
				
			||||||
	deps/libuv/src/win/snprintf.c \
 | 
					 | 
				
			||||||
	deps/libuv/src/win/stream.c \
 | 
					 | 
				
			||||||
	deps/libuv/src/win/tcp.c \
 | 
					 | 
				
			||||||
	deps/libuv/src/win/thread.c \
 | 
					 | 
				
			||||||
	deps/libuv/src/win/tty.c \
 | 
					 | 
				
			||||||
	deps/libuv/src/win/udp.c \
 | 
					 | 
				
			||||||
	deps/libuv/src/win/util.c \
 | 
					 | 
				
			||||||
	deps/libuv/src/win/winapi.c \
 | 
					 | 
				
			||||||
	deps/libuv/src/win/winsock.c
 | 
					 | 
				
			||||||
UV_OBJS := $(call get_objs,UV_SOURCES)
 | 
					 | 
				
			||||||
$(UV_OBJS): CFLAGS += \
 | 
					 | 
				
			||||||
	-Ideps/libuv/include \
 | 
					 | 
				
			||||||
	-Ideps/libuv/src \
 | 
					 | 
				
			||||||
	-Wno-unused-but-set-variable \
 | 
					 | 
				
			||||||
	-Wno-incompatible-pointer-types \
 | 
					 | 
				
			||||||
	-Wno-sign-compare \
 | 
					 | 
				
			||||||
	-Wno-unused-variable \
 | 
					 | 
				
			||||||
	-Wno-dangling-pointer \
 | 
					 | 
				
			||||||
	-Wno-maybe-uninitialized \
 | 
					 | 
				
			||||||
	-D_GNU_SOURCE
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
SODIUM_SOURCES := \
 | 
					 | 
				
			||||||
	deps/libsodium/src/libsodium/crypto_auth/hmacsha512/auth_hmacsha512.c \
 | 
					 | 
				
			||||||
	deps/libsodium/src/libsodium/crypto_auth/hmacsha512256/auth_hmacsha512256.c \
 | 
					 | 
				
			||||||
	deps/libsodium/src/libsodium/crypto_box/crypto_box.c \
 | 
					 | 
				
			||||||
	deps/libsodium/src/libsodium/crypto_box/curve25519xsalsa20poly1305/box_curve25519xsalsa20poly1305.c \
 | 
					 | 
				
			||||||
	deps/libsodium/src/libsodium/crypto_core/ed25519/ref10/ed25519_ref10.c \
 | 
					 | 
				
			||||||
	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_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 \
 | 
					 | 
				
			||||||
	deps/libsodium/src/libsodium/crypto_hash/sha256/cp/hash_sha256_cp.c \
 | 
					 | 
				
			||||||
	deps/libsodium/src/libsodium/crypto_hash/sha256/hash_sha256.c \
 | 
					 | 
				
			||||||
	deps/libsodium/src/libsodium/crypto_hash/sha512/cp/hash_sha512_cp.c \
 | 
					 | 
				
			||||||
	deps/libsodium/src/libsodium/crypto_onetimeauth/poly1305/donna/poly1305_donna.c \
 | 
					 | 
				
			||||||
	deps/libsodium/src/libsodium/crypto_onetimeauth/poly1305/onetimeauth_poly1305.c \
 | 
					 | 
				
			||||||
	deps/libsodium/src/libsodium/crypto_pwhash/argon2/argon2-core.c \
 | 
					 | 
				
			||||||
	deps/libsodium/src/libsodium/crypto_pwhash/argon2/argon2-fill-block-ref.c \
 | 
					 | 
				
			||||||
	deps/libsodium/src/libsodium/crypto_pwhash/argon2/blake2b-long.c \
 | 
					 | 
				
			||||||
	deps/libsodium/src/libsodium/crypto_scalarmult/crypto_scalarmult.c \
 | 
					 | 
				
			||||||
	deps/libsodium/src/libsodium/crypto_scalarmult/curve25519/ref10/x25519_ref10.c \
 | 
					 | 
				
			||||||
	deps/libsodium/src/libsodium/crypto_scalarmult/curve25519/scalarmult_curve25519.c \
 | 
					 | 
				
			||||||
	deps/libsodium/src/libsodium/crypto_secretbox/crypto_secretbox_easy.c \
 | 
					 | 
				
			||||||
	deps/libsodium/src/libsodium/crypto_secretbox/xsalsa20poly1305/secretbox_xsalsa20poly1305.c \
 | 
					 | 
				
			||||||
	deps/libsodium/src/libsodium/crypto_sign/crypto_sign.c \
 | 
					 | 
				
			||||||
	deps/libsodium/src/libsodium/crypto_sign/ed25519/ref10/keypair.c \
 | 
					 | 
				
			||||||
	deps/libsodium/src/libsodium/crypto_sign/ed25519/ref10/open.c \
 | 
					 | 
				
			||||||
	deps/libsodium/src/libsodium/crypto_sign/ed25519/ref10/sign.c \
 | 
					 | 
				
			||||||
	deps/libsodium/src/libsodium/crypto_sign/ed25519/sign_ed25519.c \
 | 
					 | 
				
			||||||
	deps/libsodium/src/libsodium/crypto_stream/chacha20/ref/chacha20_ref.c \
 | 
					 | 
				
			||||||
	deps/libsodium/src/libsodium/crypto_stream/chacha20/stream_chacha20.c \
 | 
					 | 
				
			||||||
	deps/libsodium/src/libsodium/crypto_stream/salsa20/ref/salsa20_ref.c \
 | 
					 | 
				
			||||||
	deps/libsodium/src/libsodium/crypto_stream/salsa20/stream_salsa20.c \
 | 
					 | 
				
			||||||
	deps/libsodium/src/libsodium/crypto_stream/xsalsa20/stream_xsalsa20.c \
 | 
					 | 
				
			||||||
	deps/libsodium/src/libsodium/crypto_verify/sodium/verify.c \
 | 
					 | 
				
			||||||
	deps/libsodium/src/libsodium/randombytes/randombytes.c \
 | 
					 | 
				
			||||||
	deps/libsodium/src/libsodium/randombytes/sysrandom/randombytes_sysrandom.c \
 | 
					 | 
				
			||||||
	deps/libsodium/src/libsodium/sodium/core.c \
 | 
					 | 
				
			||||||
	deps/libsodium/src/libsodium/sodium/codecs.c \
 | 
					 | 
				
			||||||
	deps/libsodium/src/libsodium/sodium/runtime.c \
 | 
					 | 
				
			||||||
	deps/libsodium/src/libsodium/sodium/utils.c
 | 
					 | 
				
			||||||
SODIUM_OBJS := $(call get_objs,SODIUM_SOURCES)
 | 
					 | 
				
			||||||
$(SODIUM_OBJS): CFLAGS += \
 | 
					 | 
				
			||||||
	-DCONFIGURED=1 \
 | 
					 | 
				
			||||||
	-DMINIMAL=1 \
 | 
					 | 
				
			||||||
	-Wno-unused-function \
 | 
					 | 
				
			||||||
	-Wno-unused-variable \
 | 
					 | 
				
			||||||
	-Wno-type-limits \
 | 
					 | 
				
			||||||
	-Wno-unknown-pragmas \
 | 
					 | 
				
			||||||
	-Ideps/libsodium/src/libsodium/include/sodium
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
SQLITE_SOURCES := deps/sqlite/sqlite3.c
 | 
					 | 
				
			||||||
SQLITE_OBJS := $(call get_objs,SQLITE_SOURCES)
 | 
					 | 
				
			||||||
$(SQLITE_OBJS): CFLAGS += \
 | 
					 | 
				
			||||||
	-DSQLITE_DBCONFIG_DEFAULT_DEFENSIVE \
 | 
					 | 
				
			||||||
	-DSQLITE_DEFAULT_MEMSTATUS=0 \
 | 
					 | 
				
			||||||
	-DSQLITE_DQS=0 \
 | 
					 | 
				
			||||||
	-DSQLITE_ENABLE_MEMSYS5 \
 | 
					 | 
				
			||||||
	-DSQLITE_ENABLE_FTS5 \
 | 
					 | 
				
			||||||
	-DSQLITE_ENABLE_JSON1 \
 | 
					 | 
				
			||||||
	-DSQLITE_LIKE_DOESNT_MATCH_BLOBS \
 | 
					 | 
				
			||||||
	-DSQLITE_MAX_ATTACHED=0 \
 | 
					 | 
				
			||||||
	-DSQLITE_MAX_COLUMN=100 \
 | 
					 | 
				
			||||||
	-DSQLITE_MAX_COMPOUND_SELECT=300 \
 | 
					 | 
				
			||||||
	-DSQLITE_MAX_EXPR_DEPTH=40 \
 | 
					 | 
				
			||||||
	-DSQLITE_MAX_FUNCTION_ARG=8 \
 | 
					 | 
				
			||||||
	-DSQLITE_MAX_LENGTH=5242880 \
 | 
					 | 
				
			||||||
	-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_OMIT_DEPRECATED \
 | 
					 | 
				
			||||||
	-DSQLITE_OMIT_DESERIALIZE \
 | 
					 | 
				
			||||||
	-DSQLITE_OMIT_LOAD_EXTENSION \
 | 
					 | 
				
			||||||
	-DSQLITE_OMIT_TCL_VARIABLE \
 | 
					 | 
				
			||||||
	-DSQLITE_PRAGMA_DEFAULT_WAL_SYNCHRONOUS=1 \
 | 
					 | 
				
			||||||
	-DSQLITE_SECURE_DELETE \
 | 
					 | 
				
			||||||
	-DSQLITE_THREADSAFE=0 \
 | 
					 | 
				
			||||||
	-DSQLITE_UNTESTABLE \
 | 
					 | 
				
			||||||
	-DSQLITE_USE_ALLOCA \
 | 
					 | 
				
			||||||
	-DHAVE_ISNAN \
 | 
					 | 
				
			||||||
	-Wno-implicit-fallthrough \
 | 
					 | 
				
			||||||
	-Wno-unused-but-set-variable \
 | 
					 | 
				
			||||||
	-Wno-unused-function \
 | 
					 | 
				
			||||||
	-Wno-unused-variable
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
XOPT_SOURCES := deps/xopt/xopt.c
 | 
					 | 
				
			||||||
XOPT_OBJS := $(call get_objs,XOPT_SOURCES)
 | 
					 | 
				
			||||||
$(filter $(BUILD_DIR)/win%,$(XOPT_OBJS)): CFLAGS += \
 | 
					 | 
				
			||||||
	-DHAVE_SNPRINTF \
 | 
					 | 
				
			||||||
	-DHAVE_VSNPRINTF \
 | 
					 | 
				
			||||||
	-DHAVE_VASNPRINTF \
 | 
					 | 
				
			||||||
	-DHAVE_VASPRINTF \
 | 
					 | 
				
			||||||
	-Dvsnprintf=rpl_vsnprintf
 | 
					 | 
				
			||||||
$(XOPT_OBJS): CFLAGS += \
 | 
					 | 
				
			||||||
	-Wno-implicit-const-int-float-conversion
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
QUICKJS_SOURCES := \
 | 
					 | 
				
			||||||
	deps/quickjs/cutils.c \
 | 
					 | 
				
			||||||
	deps/quickjs/libbf.c \
 | 
					 | 
				
			||||||
	deps/quickjs/libregexp.c \
 | 
					 | 
				
			||||||
	deps/quickjs/libunicode.c \
 | 
					 | 
				
			||||||
	deps/quickjs/quickjs.c
 | 
					 | 
				
			||||||
QUICKJS_OBJS := $(call get_objs,QUICKJS_SOURCES)
 | 
					 | 
				
			||||||
$(QUICKJS_OBJS): CFLAGS += \
 | 
					 | 
				
			||||||
	-DCONFIG_VERSION=\"$(shell cat deps/quickjs/VERSION)\" \
 | 
					 | 
				
			||||||
	-DCONFIG_BIGNUM \
 | 
					 | 
				
			||||||
	-D_GNU_SOURCE \
 | 
					 | 
				
			||||||
	-Wno-enum-conversion \
 | 
					 | 
				
			||||||
	-Wno-implicit-const-int-float-conversion \
 | 
					 | 
				
			||||||
	-Wno-implicit-fallthrough \
 | 
					 | 
				
			||||||
	-Wno-sign-compare \
 | 
					 | 
				
			||||||
	-Wno-unused-but-set-variable \
 | 
					 | 
				
			||||||
	-Wno-unused-variable
 | 
					 | 
				
			||||||
$(NONANDROID_TARGETS): CFLAGS += -DDUMP_LEAKS
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
LIBBACKTRACE_SOURCES := \
 | 
					 | 
				
			||||||
	deps/libbacktrace/atomic.c \
 | 
					 | 
				
			||||||
	deps/libbacktrace/backtrace.c \
 | 
					 | 
				
			||||||
	deps/libbacktrace/dwarf.c \
 | 
					 | 
				
			||||||
	deps/libbacktrace/fileline.c \
 | 
					 | 
				
			||||||
	deps/libbacktrace/print.c \
 | 
					 | 
				
			||||||
	deps/libbacktrace/simple.c \
 | 
					 | 
				
			||||||
	deps/libbacktrace/sort.c \
 | 
					 | 
				
			||||||
	deps/libbacktrace/state.c
 | 
					 | 
				
			||||||
LIBBACKTRACE_SOURCES_unix := \
 | 
					 | 
				
			||||||
	deps/libbacktrace/elf.c \
 | 
					 | 
				
			||||||
	deps/libbacktrace/mmap.c \
 | 
					 | 
				
			||||||
	deps/libbacktrace/mmapio.c \
 | 
					 | 
				
			||||||
	deps/libbacktrace/posix.c
 | 
					 | 
				
			||||||
LIBBACKTRACE_SOURCES_win := \
 | 
					 | 
				
			||||||
	deps/libbacktrace/alloc.c \
 | 
					 | 
				
			||||||
	deps/libbacktrace/pecoff.c \
 | 
					 | 
				
			||||||
	deps/libbacktrace/posix.c \
 | 
					 | 
				
			||||||
	deps/libbacktrace/read.c
 | 
					 | 
				
			||||||
LIBBACKTRACE_OBJS := $(call get_objs,LIBBACKTRACE_SOURCES)
 | 
					 | 
				
			||||||
$(LIBBACKTRACE_OBJS): CFLAGS += \
 | 
					 | 
				
			||||||
	-Ideps/libbacktrace_config \
 | 
					 | 
				
			||||||
	-Wno-unused-but-set-variable \
 | 
					 | 
				
			||||||
	-Wno-maybe-initialized \
 | 
					 | 
				
			||||||
	-Wno-unused-function \
 | 
					 | 
				
			||||||
	-DBACKTRACE_ELF_SIZE=64
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
PICOHTTPPARSER_SOURCES := \
 | 
					 | 
				
			||||||
	deps/picohttpparser/picohttpparser.c
 | 
					 | 
				
			||||||
PICOHTTPPARSER_OBJS := $(call get_objs,PICOHTTPPARSER_SOURCES)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
MINIUNZIP_SOURCES := \
 | 
					 | 
				
			||||||
	deps/zlib/contrib/minizip/unzip.c \
 | 
					 | 
				
			||||||
	deps/zlib/contrib/minizip/ioapi.c \
 | 
					 | 
				
			||||||
	deps/zlib/adler32.c \
 | 
					 | 
				
			||||||
	deps/zlib/crc32.c \
 | 
					 | 
				
			||||||
	deps/zlib/inffast.c \
 | 
					 | 
				
			||||||
	deps/zlib/inflate.c \
 | 
					 | 
				
			||||||
	deps/zlib/inftrees.c \
 | 
					 | 
				
			||||||
	deps/zlib/zutil.c
 | 
					 | 
				
			||||||
MINIUNZIP_OBJS := $(call get_objs,MINIUNZIP_SOURCES)
 | 
					 | 
				
			||||||
$(MINIUNZIP_OBJS): CFLAGS += \
 | 
					 | 
				
			||||||
	-Ideps/zlib \
 | 
					 | 
				
			||||||
	-Wno-maybe-uninitialized
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
LDFLAGS += \
 | 
					 | 
				
			||||||
	-pthread \
 | 
					 | 
				
			||||||
	-lm
 | 
					 | 
				
			||||||
debug release: LDFLAGS += \
 | 
					 | 
				
			||||||
	-ldl \
 | 
					 | 
				
			||||||
	-lssl \
 | 
					 | 
				
			||||||
	-lcrypto
 | 
					 | 
				
			||||||
windebug winrelease: LDFLAGS += \
 | 
					 | 
				
			||||||
	-lwsock32 \
 | 
					 | 
				
			||||||
	-lws2_32 \
 | 
					 | 
				
			||||||
	-lkernel32 \
 | 
					 | 
				
			||||||
	-liphlpapi \
 | 
					 | 
				
			||||||
	-luserenv \
 | 
					 | 
				
			||||||
	-lssl \
 | 
					 | 
				
			||||||
	-lcrypto \
 | 
					 | 
				
			||||||
	-lws2_32 \
 | 
					 | 
				
			||||||
	-lcrypt32
 | 
					 | 
				
			||||||
$(ANDROID_TARGETS): LDFLAGS += \
 | 
					 | 
				
			||||||
	-target $(ANDROID_NDK_TARGET_TRIPLE)$(ANDROID_NDK_API_VERSION) \
 | 
					 | 
				
			||||||
	-ldl \
 | 
					 | 
				
			||||||
	-llog \
 | 
					 | 
				
			||||||
	-lssl \
 | 
					 | 
				
			||||||
	-lcrypto
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
unix: debug release
 | 
					 | 
				
			||||||
win: windebug winrelease
 | 
					 | 
				
			||||||
all: $(BUILD_TYPES) out/TildeFriends-debug.apk out/TildeFriends-release.apk
 | 
					 | 
				
			||||||
.PHONY: all win unix
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
ALL_APP_OBJS := \
 | 
					 | 
				
			||||||
	$(APP_OBJS) \
 | 
					 | 
				
			||||||
	$(BLOWFISH_OBJS) \
 | 
					 | 
				
			||||||
	$(LIBBACKTRACE_OBJS) \
 | 
					 | 
				
			||||||
	$(MINIUNZIP_OBJS) \
 | 
					 | 
				
			||||||
	$(PICOHTTPPARSER_OBJS) \
 | 
					 | 
				
			||||||
	$(QUICKJS_OBJS) \
 | 
					 | 
				
			||||||
	$(SODIUM_OBJS) \
 | 
					 | 
				
			||||||
	$(SQLITE_OBJS) \
 | 
					 | 
				
			||||||
	$(UV_OBJS) \
 | 
					 | 
				
			||||||
	$(XOPT_OBJS)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
DEPS = $(ALL_APP_OBJS:.o=.d)
 | 
					 | 
				
			||||||
-include $(DEPS)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
define build_rules
 | 
					 | 
				
			||||||
$(1): $(BUILD_DIR)/$(1)/$(PROJECT)$(if $(filter win%,$(1)),.exe)
 | 
					 | 
				
			||||||
.PHONY: $(1)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
$(BUILD_DIR)/$(1)/$(PROJECT)$(if $(filter win%,$(1)),.exe): $(filter $(BUILD_DIR)/$(1)/%,$(ALL_APP_OBJS))
 | 
					 | 
				
			||||||
	@echo [link] $$@
 | 
					 | 
				
			||||||
	@$$(CC) -o $$@ -Wl,-Map,$$@.map $$^ $$(LDFLAGS)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
$(BUILD_DIR)/$(1)/%.o: %.c
 | 
					 | 
				
			||||||
	@mkdir -p $$(dir $$@)
 | 
					 | 
				
			||||||
	@echo [c] $$@
 | 
					 | 
				
			||||||
	@$$(CC) $$(CFLAGS) -c $$< -o $$@
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
$(BUILD_DIR)/$(1)/%.o: %.S
 | 
					 | 
				
			||||||
	@mkdir -p $$(dir $$@)
 | 
					 | 
				
			||||||
	@echo [as] $$@
 | 
					 | 
				
			||||||
	@$$(AS) -c $$< -o $$@
 | 
					 | 
				
			||||||
endef
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
$(foreach build_type,$(BUILD_TYPES),$(eval $(call build_rules,$(build_type))))
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
# Android support.
 | 
					 | 
				
			||||||
out/res/layout_activity_main.xml.flat: src/android/res/layout/activity_main.xml
 | 
					 | 
				
			||||||
	@mkdir -p $(dir $@)
 | 
					 | 
				
			||||||
	@echo [aapt2] $@
 | 
					 | 
				
			||||||
	@$(ANDROID_BUILD_TOOLS)/aapt2 compile -o out/res/ src/android/res/layout/activity_main.xml
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
out/apk/res.apk out/gen/com/unprompted/tildefriends/R.java: out/res/layout_activity_main.xml.flat src/android/AndroidManifest.xml
 | 
					 | 
				
			||||||
	@mkdir -p $(dir $@)
 | 
					 | 
				
			||||||
	@$(ANDROID_BUILD_TOOLS)/aapt2 link -I $(ANDROID_PLATFORM)/android.jar out/res/layout_activity_main.xml.flat --manifest src/android/AndroidManifest.xml -o out/apk/res.apk --java out/gen/
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
JAVA_FILES := out/gen/com/unprompted/tildefriends/R.java $(wildcard src/android/com/unprompted/tildefriends/*.java)
 | 
					 | 
				
			||||||
CLASS_FILES := $(foreach src,$(JAVA_FILES),out/classes/com/unprompted/tildefriends/$(notdir $(src:.java=.class)))
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
$(CLASS_FILES) &: $(JAVA_FILES)
 | 
					 | 
				
			||||||
	@echo [javac] $(CLASS_FILES)
 | 
					 | 
				
			||||||
	@javac --release 8 -Xlint:deprecation -classpath $(ANDROID_PLATFORM)/android.jar -d out/classes $(JAVA_FILES)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
out/apk/classes.dex: $(CLASS_FILES)
 | 
					 | 
				
			||||||
	@mkdir -p $(dir $@)
 | 
					 | 
				
			||||||
	@echo [d8] $@
 | 
					 | 
				
			||||||
	@$(ANDROID_BUILD_TOOLS)/d8 --$(BUILD_TYPE) --lib $(ANDROID_PLATFORM)/android.jar --output $(dir $@) out/classes/com/unprompted/tildefriends/*.class
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
PACKAGE_DIRS := \
 | 
					 | 
				
			||||||
	apps/ \
 | 
					 | 
				
			||||||
	core/ \
 | 
					 | 
				
			||||||
	deps/codemirror/ \
 | 
					 | 
				
			||||||
	deps/split/ \
 | 
					 | 
				
			||||||
	deps/smoothie/
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
RAW_FILES := $(shell find $(PACKAGE_DIRS) -type f)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
out/apk/TildeFriends-debug.unsigned.apk: BUILD_TYPE := debug
 | 
					 | 
				
			||||||
out/apk/TildeFriends-release.unsigned.apk: BUILD_TYPE := release
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
out/apk/TildeFriends-debug.unsigned.apk: out/apk/classes.dex out/androiddebug/tildefriends out/androiddebug-x86_64/tildefriends $(RAW_FILES) out/apk/res.apk
 | 
					 | 
				
			||||||
out/apk/TildeFriends-release.unsigned.apk: out/apk/classes.dex out/androidrelease/tildefriends out/androidrelease-x86_64/tildefriends $(RAW_FILES) out/apk/res.apk
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
out/%.unsigned.apk:
 | 
					 | 
				
			||||||
	@mkdir -p $(dir $@) out/apk$(BUILD_TYPE)/bin/aarch64/ out/apk$(BUILD_TYPE)/bin/x86_64/
 | 
					 | 
				
			||||||
	@echo [aapt] $@
 | 
					 | 
				
			||||||
	@cp out/android$(BUILD_TYPE)/tildefriends out/apk$(BUILD_TYPE)/bin/aarch64/
 | 
					 | 
				
			||||||
	@cp out/android$(BUILD_TYPE)-x86_64/tildefriends out/apk$(BUILD_TYPE)/bin/x86_64/
 | 
					 | 
				
			||||||
	@$(ANDROID_NDK)/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-strip out/apk$(BUILD_TYPE)/bin/aarch64/tildefriends
 | 
					 | 
				
			||||||
	@$(ANDROID_NDK)/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-strip out/apk$(BUILD_TYPE)/bin/x86_64/tildefriends
 | 
					 | 
				
			||||||
	@cp out/apk/res.apk $@
 | 
					 | 
				
			||||||
	@cp out/apk/classes.dex out/apk$(BUILD_TYPE)/
 | 
					 | 
				
			||||||
	@cd out/apk$(BUILD_TYPE) && zip -u ../../$@ -q -9 -r . && cd ../../
 | 
					 | 
				
			||||||
	@zip -u $@ -q -9 -r $(PACKAGE_DIRS) $(RAW_FILES)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
out/%.apk: out/apk/%.unsigned.apk
 | 
					 | 
				
			||||||
	@echo [apksigner] $(notdir $@)
 | 
					 | 
				
			||||||
	@$(ANDROID_BUILD_TOOLS)/apksigner sign --ks keystore.jks --ks-key-alias androidKey --ks-pass pass:android --key-pass pass:android --out $@ $<
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
apk: out/TildeFriends-debug.apk
 | 
					 | 
				
			||||||
.PHONY: apk
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
apkgo: out/TildeFriends-debug.apk
 | 
					 | 
				
			||||||
	@adb install $<
 | 
					 | 
				
			||||||
	@adb shell am start com.unprompted.tildefriends/.MainActivity
 | 
					 | 
				
			||||||
.PHONY: apkgo
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
clean:
 | 
					 | 
				
			||||||
	rm -rf $(BUILD_DIR)
 | 
					 | 
				
			||||||
.PHONY: clean
 | 
					 | 
				
			||||||
							
								
								
									
										31
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										31
									
								
								README.md
									
									
									
									
									
								
							@@ -1,36 +1,49 @@
 | 
				
			|||||||
# Tilde Friends
 | 
					# Tilde Friends
 | 
				
			||||||
 | 
					
 | 
				
			||||||
Tilde Friends is a tool for making and sharing.
 | 
					Tilde Friends is a tool for making and sharing.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					A public instance lives at https://www.tildefriends.net/.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
It is both a peer-to-peer social network client, participating in Secure
 | 
					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.
 | 
					Scuttlebutt, as well as a platform for writing and running web applications.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
## Goals
 | 
					## Goals
 | 
				
			||||||
 | 
					
 | 
				
			||||||
1. Make it easy and fun to run all sorts of web applications.
 | 
					1. Make it easy and fun to run all sorts of web applications.
 | 
				
			||||||
2. Provide security that is easy to understand and protects your data.
 | 
					2. Provide security that is easy to understand and protects your data.
 | 
				
			||||||
3. Make creating and sharing web applications accessible to anyone with a
 | 
					3. Make creating and sharing web applications accessible to anyone with a
 | 
				
			||||||
   browser.
 | 
					   browser.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
## Building
 | 
					## Building
 | 
				
			||||||
1. Requires openssl (`libssl-dev`, in debian-speak).  All other dependencies
 | 
					
 | 
				
			||||||
 | 
					Builds on Linux (x86_64 and aarch64), MacOS, OpenBSD, and Haiku. Builds for
 | 
				
			||||||
 | 
					all of those host platforms plus mingw64, iOS, and android.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					1. Requires openssl (`libssl-dev`, in debian-speak). All other dependencies
 | 
				
			||||||
   are kept up to date in the tree.
 | 
					   are kept up to date in the tree.
 | 
				
			||||||
2. To build, run `make debug` or `make release`.  An executable will be
 | 
					2. To build, run `make debug` or `make release`. An executable will be
 | 
				
			||||||
   generated in a subdirectory of `out/`.
 | 
					   generated in a subdirectory of `out/`.
 | 
				
			||||||
3. `make windebug` or `make winrelease` will generate a windows executable
 | 
					3. It's possible to build for Android, iOS, and Windows on Linux, if you have
 | 
				
			||||||
   which might work.
 | 
					   the right dependencies in the right places. `make windebug winrelease
 | 
				
			||||||
 | 
					iosdebug-ipa iosrelease-ipa release-apk`.
 | 
				
			||||||
4. To build in docker, `docker build .`.
 | 
					4. To build in docker, `docker build .`.
 | 
				
			||||||
 | 
					5. `make format` will normalize formatting to the coding standard.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
## Running
 | 
					## Running
 | 
				
			||||||
 | 
					
 | 
				
			||||||
By default, running the built `tildefriends` executable will start a web server
 | 
					By default, running the built `tildefriends` executable will start a web server
 | 
				
			||||||
at <http://localhost:12345/>.  `tildefriends -h` lists further options.
 | 
					at <http://localhost:12345/>. `tildefriends -h` lists further options.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
The first user to create an account and log in will be granted administrative
 | 
					The first user to create an account and log in will be granted administrative
 | 
				
			||||||
privileges.  Further administration can be done at
 | 
					privileges. Further administration can be done at
 | 
				
			||||||
<http://localhost:12345/~core/admin/`>.
 | 
					<http://localhost:12345/~core/admin/>.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
## Documentation
 | 
					## Documentation
 | 
				
			||||||
There are the very beginnings of developer documentation in `apps/docs/`
 | 
					
 | 
				
			||||||
that can be read in-place or at <http://localhost:12345/~core/docs/>.
 | 
					Docs are a work in progress:
 | 
				
			||||||
 | 
					<https://www.tildefriends.net/~cory/wiki/#test-wiki/tf-app-quick-reference>.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
## License
 | 
					## License
 | 
				
			||||||
 | 
					
 | 
				
			||||||
All code unless otherwise noted in is provided under the
 | 
					All code unless otherwise noted in is provided under the
 | 
				
			||||||
[MIT](https://opensource.org/licenses/MIT) license.
 | 
					[MIT](https://opensource.org/licenses/MIT) license.
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,4 +1,5 @@
 | 
				
			|||||||
{
 | 
					{
 | 
				
			||||||
  "type": "tildefriends-app",
 | 
						"type": "tildefriends-app",
 | 
				
			||||||
  "emoji": "🎛"
 | 
						"emoji": "🎛",
 | 
				
			||||||
 | 
						"previous": "&vrpS/vE7n588iYv1p8HafDxHB+YDHTrtUbJiu9nGA9I=.sha256"
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@@ -9,14 +9,22 @@ tfrpc.register(function global_settings_set(key, value) {
 | 
				
			|||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
async function main() {
 | 
					async function main() {
 | 
				
			||||||
	let data = {
 | 
						try {
 | 
				
			||||||
		users: {},
 | 
							let data = {
 | 
				
			||||||
		granted: await core.allPermissionsGranted(),
 | 
								users: {},
 | 
				
			||||||
		settings: await core.globalSettingsDescriptions(),
 | 
								granted: await core.allPermissionsGranted(),
 | 
				
			||||||
	};
 | 
								settings: await core.globalSettingsDescriptions(),
 | 
				
			||||||
	for (let user of await core.users()) {
 | 
							};
 | 
				
			||||||
		data.users[user] = await core.permissionsForUser(user);
 | 
							for (let user of await core.users()) {
 | 
				
			||||||
 | 
								data.users[user] = await core.permissionsForUser(user);
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							await app.setDocument(
 | 
				
			||||||
 | 
								utf8Decode(getFile('index.html')).replace('$data', JSON.stringify(data))
 | 
				
			||||||
 | 
							);
 | 
				
			||||||
 | 
						} catch {
 | 
				
			||||||
 | 
							await app.setDocument(
 | 
				
			||||||
 | 
								'<span style="color: #f00">Only an administrator can modify these settings.</span>'
 | 
				
			||||||
 | 
							);
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	await app.setDocument(utf8Decode(getFile('index.html')).replace('$data', JSON.stringify(data)));
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
main();
 | 
					main();
 | 
				
			||||||
@@ -1,10 +1,41 @@
 | 
				
			|||||||
<!DOCTYPE html>
 | 
					<!doctype html>
 | 
				
			||||||
<html>
 | 
					<html style="width: 100%">
 | 
				
			||||||
	<head>
 | 
						<head>
 | 
				
			||||||
		<script>const g_data = $data;</script>
 | 
							<script>
 | 
				
			||||||
 | 
								const g_data = $data;
 | 
				
			||||||
 | 
							</script>
 | 
				
			||||||
 | 
							<link rel="stylesheet" href="w3.css" />
 | 
				
			||||||
 | 
							<!-- prettier-ignore -->
 | 
				
			||||||
 | 
							<style>
 | 
				
			||||||
 | 
								/* 2018 Valiant Poppy */
 | 
				
			||||||
 | 
								.w3-theme-l5 {color:#000 !important; background-color:#fbf3f3 !important}
 | 
				
			||||||
 | 
								.w3-theme-l4 {color:#000 !important; background-color:#f3d7d6 !important}
 | 
				
			||||||
 | 
								.w3-theme-l3 {color:#000 !important; background-color:#e6afae !important}
 | 
				
			||||||
 | 
								.w3-theme-l2 {color:#fff !important; background-color:#da8785 !important}
 | 
				
			||||||
 | 
								.w3-theme-l1 {color:#fff !important; background-color:#cd5f5d !important}
 | 
				
			||||||
 | 
								.w3-theme-d1 {color:#fff !important; background-color:#a93634 !important}
 | 
				
			||||||
 | 
								.w3-theme-d2 {color:#fff !important; background-color:#96302e !important}
 | 
				
			||||||
 | 
								.w3-theme-d3 {color:#fff !important; background-color:#832a28 !important}
 | 
				
			||||||
 | 
								.w3-theme-d4 {color:#fff !important; background-color:#702423 !important}
 | 
				
			||||||
 | 
								.w3-theme-d5 {color:#fff !important; background-color:#5e1e1d !important}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								.w3-theme-light {color:#000 !important; background-color:#fbf3f3 !important}
 | 
				
			||||||
 | 
								.w3-theme-dark {color:#fff !important; background-color:#5e1e1d !important}
 | 
				
			||||||
 | 
								.w3-theme-action {color:#fff !important; background-color:#5e1e1d !important}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								.w3-theme {color:#fff !important; background-color:#bd3d3a !important}
 | 
				
			||||||
 | 
								.w3-text-theme {color:#bd3d3a !important}
 | 
				
			||||||
 | 
								.w3-border-theme {border-color:#bd3d3a !important}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								.w3-hover-theme:hover {color:#fff !important; background-color:#bd3d3a !important}
 | 
				
			||||||
 | 
								.w3-hover-text-theme:hover {color:#bd3d3a !important}
 | 
				
			||||||
 | 
								.w3-hover-border-theme:hover {border-color:#bd3d3a !important}
 | 
				
			||||||
 | 
							</style>
 | 
				
			||||||
	</head>
 | 
						</head>
 | 
				
			||||||
	<body style="color: #fff">
 | 
						<body class="w3-theme-l4">
 | 
				
			||||||
		<h1>Tilde Friends Administration</h1>
 | 
							<header class="w3-row w3-padding w3-header w3-theme-l1">
 | 
				
			||||||
 | 
								<h1>Tilde Friends Administration</h1>
 | 
				
			||||||
 | 
							</header>
 | 
				
			||||||
	</body>
 | 
						</body>
 | 
				
			||||||
	<script type="module" src="script.js"></script>
 | 
						<script type="module" src="script.js"></script>
 | 
				
			||||||
</html>
 | 
					</html>
 | 
				
			||||||
@@ -3,76 +3,106 @@ import * as tfrpc from '/static/tfrpc.js';
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
function delete_user(user) {
 | 
					function delete_user(user) {
 | 
				
			||||||
	if (confirm(`Are you sure you want to delete the user "${user}"?`)) {
 | 
						if (confirm(`Are you sure you want to delete the user "${user}"?`)) {
 | 
				
			||||||
		tfrpc.rpc.delete_user(user).then(function() {
 | 
							tfrpc.rpc
 | 
				
			||||||
			alert(`User "${user}" deleted successfully.`);
 | 
								.delete_user(user)
 | 
				
			||||||
		}).catch(function(error) {
 | 
								.then(function () {
 | 
				
			||||||
			alert(`Failed to delete user "${user}": ${JSON.stringify(error, null, 2)}.`);
 | 
									alert(`User "${user}" deleted successfully.`);
 | 
				
			||||||
		});
 | 
								})
 | 
				
			||||||
 | 
								.catch(function (error) {
 | 
				
			||||||
 | 
									alert(
 | 
				
			||||||
 | 
										`Failed to delete user "${user}": ${JSON.stringify(error, null, 2)}.`
 | 
				
			||||||
 | 
									);
 | 
				
			||||||
 | 
								});
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function global_settings_set(key, value) {
 | 
					function global_settings_set(key, value) {
 | 
				
			||||||
	tfrpc.rpc.global_settings_set(key, value).then(function() {
 | 
						tfrpc.rpc
 | 
				
			||||||
		alert(`Set "${key}" to "${value}".`);
 | 
							.global_settings_set(key, value)
 | 
				
			||||||
	}).catch(function(error) {
 | 
							.then(function () {
 | 
				
			||||||
		alert(`Failed to set "${key}": ${JSON.stringify(error, null, 2)}.`);
 | 
								alert(`Set "${key}" to "${value}".`);
 | 
				
			||||||
	});
 | 
							})
 | 
				
			||||||
 | 
							.catch(function (error) {
 | 
				
			||||||
 | 
								alert(`Failed to set "${key}": ${JSON.stringify(error, null, 2)}.`);
 | 
				
			||||||
 | 
							});
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
window.addEventListener('load', function() {
 | 
					window.addEventListener('load', function () {
 | 
				
			||||||
	const permission_template = (permission) =>
 | 
						const permission_template = (permission) => html` <code>${permission}</code>`;
 | 
				
			||||||
		html` <code>${permission}</code>`;
 | 
					 | 
				
			||||||
	function input_template(key, description) {
 | 
						function input_template(key, description) {
 | 
				
			||||||
		if (description.type === 'boolean') {
 | 
							if (description.type === 'boolean') {
 | 
				
			||||||
			return html`
 | 
								return html`
 | 
				
			||||||
				<label ?for=${'gs_' + key} style="grid-column: 1">${key}: </label>
 | 
									<li class="w3-row">
 | 
				
			||||||
				<input type="checkbox" ?checked=${description.value} ?id=${'gs_' + key} style="grid-column: 2"></input>
 | 
										<label class="w3-quarter" for=${'gs_' + key} style="font-weight: bold">${key}</label>
 | 
				
			||||||
				<div style="grid-column: 3">
 | 
										<div class="w3-quarter w3-padding">${description.description}</div>
 | 
				
			||||||
					<button @click=${(e) => global_settings_set(key, e.srcElement.parentElement.previousElementSibling.checked)}>Set</button>
 | 
										<input class="w3-quarter w3-check" type="checkbox" ?checked=${description.value} id=${'gs_' + key}></input>
 | 
				
			||||||
					<span>${description.description}</span>
 | 
										<button class="w3-quarter w3-button w3-theme-action" @click=${(e) => global_settings_set(key, e.srcElement.previousElementSibling.checked)}>Set</button>
 | 
				
			||||||
				</div>
 | 
									</li>
 | 
				
			||||||
			`;
 | 
								`;
 | 
				
			||||||
		} else if (description.type === 'textarea') {
 | 
							} else if (description.type === 'textarea') {
 | 
				
			||||||
			return html`
 | 
								return html`
 | 
				
			||||||
				<label ?for=${'gs_' + key} style="grid-column: 1">${key}: </label>
 | 
									<li class="w3-row">
 | 
				
			||||||
				<textarea style="vertical-align: top" rows=20 cols=80 ?id=${'gs_' + key}>${description.value}</textarea>
 | 
										<label class="w3-quarter" for=${'gs_' + key} style="font-weight: bold"
 | 
				
			||||||
				<div style="grid-column: 3">
 | 
											>${key}</label
 | 
				
			||||||
					<button @click=${(e) => global_settings_set(key, e.srcElement.parentElement.previousElementSibling.value)}>Set</button>
 | 
										>
 | 
				
			||||||
					<span>${description.description}</span>
 | 
										<div class="w3-rest w3-padding">${description.description}</div>
 | 
				
			||||||
				</div>
 | 
										<textarea
 | 
				
			||||||
 | 
											class="w3-input"
 | 
				
			||||||
 | 
											style="vertical-align: top; resize: vertical"
 | 
				
			||||||
 | 
											id=${'gs_' + key}
 | 
				
			||||||
 | 
										>
 | 
				
			||||||
 | 
					${description.value}</textarea
 | 
				
			||||||
 | 
										>
 | 
				
			||||||
 | 
										<button
 | 
				
			||||||
 | 
											class="w3-button w3-right w3-quarter w3-theme-action"
 | 
				
			||||||
 | 
											@click=${(e) =>
 | 
				
			||||||
 | 
												global_settings_set(
 | 
				
			||||||
 | 
													key,
 | 
				
			||||||
 | 
													e.srcElement.previousElementSibling.value
 | 
				
			||||||
 | 
												)}
 | 
				
			||||||
 | 
										>
 | 
				
			||||||
 | 
											Set
 | 
				
			||||||
 | 
										</button>
 | 
				
			||||||
 | 
									</li>
 | 
				
			||||||
			`;
 | 
								`;
 | 
				
			||||||
		} else {
 | 
							} else {
 | 
				
			||||||
			return html`
 | 
								return html`
 | 
				
			||||||
				<label ?for=${'gs_' + key} style="grid-column: 1">${key}: </label>
 | 
									<li class="w3-row">
 | 
				
			||||||
				<input type="text" value="${description.value}" ?id=${'gs_' + key}></input>
 | 
										<label class="w3-quarter" for=${'gs_' + key} style="font-weight: bold">${key}</label>
 | 
				
			||||||
				<div style="grid-column: 3">
 | 
										<div class="w3-quarter w3-padding">${description.description}</div>
 | 
				
			||||||
					<button @click=${(e) => global_settings_set(key, e.srcElement.parentElement.previousElementSibling.value)}>Set</button>
 | 
										<input class="w3-input w3-quarter" type="text" value="${description.value}" id=${'gs_' + key}></input>
 | 
				
			||||||
					<span>${description.description}</span>
 | 
										<button class="w3-button w3-quarter w3-theme-action" @click=${(e) => global_settings_set(key, e.srcElement.previousElementSibling.value)}>Set</button>
 | 
				
			||||||
				</div>
 | 
									</li>
 | 
				
			||||||
			`;
 | 
								`;
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	const user_template = (user, permissions) => html`
 | 
						const user_template = (user, permissions) => html`
 | 
				
			||||||
		<li>
 | 
							<li class="w3-card w3-margin">
 | 
				
			||||||
			<button @click=${(e) => delete_user(user)}>
 | 
								<button
 | 
				
			||||||
 | 
									class="w3-button w3-theme-action"
 | 
				
			||||||
 | 
									@click=${(e) => delete_user(user)}
 | 
				
			||||||
 | 
								>
 | 
				
			||||||
				Delete
 | 
									Delete
 | 
				
			||||||
			</button>
 | 
								</button>
 | 
				
			||||||
			${user}:
 | 
								${user}: ${permissions.map((x) => permission_template(x))}
 | 
				
			||||||
			${permissions.map(x => permission_template(x))}
 | 
					 | 
				
			||||||
		</li>
 | 
							</li>
 | 
				
			||||||
	`;
 | 
						`;
 | 
				
			||||||
	const users_template = (users) =>
 | 
						const users_template = (users) =>
 | 
				
			||||||
		html`<h2>Users</h2>
 | 
							html` <header class="w3-container w3-theme-l2"><h2>Users</h2></header>
 | 
				
			||||||
		<ul>
 | 
								<ul class="w3-ul">
 | 
				
			||||||
			${Object.entries(users).map(u => user_template(u[0], u[1]))}
 | 
									${Object.entries(users).map((u) => user_template(u[0], u[1]))}
 | 
				
			||||||
		</ul>`;
 | 
								</ul>`;
 | 
				
			||||||
	const page_template = (data) =>
 | 
						const page_template = (data) =>
 | 
				
			||||||
		html`<div>
 | 
							html`<div style="padding: 0; margin: 0; width: 100%; max-width: 100%">
 | 
				
			||||||
		<h2>Global Settings</h2>
 | 
								<header class="w3-container w3-theme-l2"><h2>Global Settings</h2></header>
 | 
				
			||||||
		<div style="display: grid">
 | 
								<div class="w3-container">
 | 
				
			||||||
		${Object.keys(data.settings).sort().map(x => html`${input_template(x, data.settings[x])}`)}
 | 
									<ul class="w3-ul">
 | 
				
			||||||
		</div>
 | 
										${Object.keys(data.settings)
 | 
				
			||||||
		${users_template(data.users)}
 | 
											.sort()
 | 
				
			||||||
		</div>`;
 | 
											.map((x) => html`${input_template(x, data.settings[x])}`)}
 | 
				
			||||||
 | 
									</ul>
 | 
				
			||||||
 | 
								</div>
 | 
				
			||||||
 | 
								${users_template(data.users)}
 | 
				
			||||||
 | 
							</div> `;
 | 
				
			||||||
	render(page_template(g_data), document.body);
 | 
						render(page_template(g_data), document.body);
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
							
								
								
									
										235
									
								
								apps/admin/w3.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										235
									
								
								apps/admin/w3.css
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,235 @@
 | 
				
			|||||||
 | 
					/* W3.CSS 4.15 December 2020 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-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}
 | 
				
			||||||
 | 
					/* 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-pale-red,.w3-hover-pale-red:hover{color:#000!important;background-color:#ffdddd!important}
 | 
				
			||||||
 | 
					.w3-pale-green,.w3-hover-pale-green:hover{color:#000!important;background-color:#ddffdd!important}
 | 
				
			||||||
 | 
					.w3-pale-yellow,.w3-hover-pale-yellow:hover{color:#000!important;background-color:#ffffcc!important}
 | 
				
			||||||
 | 
					.w3-pale-blue,.w3-hover-pale-blue:hover{color:#000!important;background-color:#ddffff!important}
 | 
				
			||||||
 | 
					.w3-text-amber,.w3-hover-text-amber:hover{color:#ffc107!important}
 | 
				
			||||||
 | 
					.w3-text-aqua,.w3-hover-text-aqua:hover{color:#00ffff!important}
 | 
				
			||||||
 | 
					.w3-text-blue,.w3-hover-text-blue:hover{color:#2196F3!important}
 | 
				
			||||||
 | 
					.w3-text-light-blue,.w3-hover-text-light-blue:hover{color:#87CEEB!important}
 | 
				
			||||||
 | 
					.w3-text-brown,.w3-hover-text-brown:hover{color:#795548!important}
 | 
				
			||||||
 | 
					.w3-text-cyan,.w3-hover-text-cyan:hover{color:#00bcd4!important}
 | 
				
			||||||
 | 
					.w3-text-blue-grey,.w3-hover-text-blue-grey:hover,.w3-text-blue-gray,.w3-hover-text-blue-gray:hover{color:#607d8b!important}
 | 
				
			||||||
 | 
					.w3-text-green,.w3-hover-text-green:hover{color:#4CAF50!important}
 | 
				
			||||||
 | 
					.w3-text-light-green,.w3-hover-text-light-green:hover{color:#8bc34a!important}
 | 
				
			||||||
 | 
					.w3-text-indigo,.w3-hover-text-indigo:hover{color:#3f51b5!important}
 | 
				
			||||||
 | 
					.w3-text-khaki,.w3-hover-text-khaki:hover{color:#b4aa50!important}
 | 
				
			||||||
 | 
					.w3-text-lime,.w3-hover-text-lime:hover{color:#cddc39!important}
 | 
				
			||||||
 | 
					.w3-text-orange,.w3-hover-text-orange:hover{color:#ff9800!important}
 | 
				
			||||||
 | 
					.w3-text-deep-orange,.w3-hover-text-deep-orange:hover{color:#ff5722!important}
 | 
				
			||||||
 | 
					.w3-text-pink,.w3-hover-text-pink:hover{color:#e91e63!important}
 | 
				
			||||||
 | 
					.w3-text-purple,.w3-hover-text-purple:hover{color:#9c27b0!important}
 | 
				
			||||||
 | 
					.w3-text-deep-purple,.w3-hover-text-deep-purple:hover{color:#673ab7!important}
 | 
				
			||||||
 | 
					.w3-text-red,.w3-hover-text-red:hover{color:#f44336!important}
 | 
				
			||||||
 | 
					.w3-text-sand,.w3-hover-text-sand:hover{color:#fdf5e6!important}
 | 
				
			||||||
 | 
					.w3-text-teal,.w3-hover-text-teal:hover{color:#009688!important}
 | 
				
			||||||
 | 
					.w3-text-yellow,.w3-hover-text-yellow:hover{color:#d2be0e!important}
 | 
				
			||||||
 | 
					.w3-text-white,.w3-hover-text-white:hover{color:#fff!important}
 | 
				
			||||||
 | 
					.w3-text-black,.w3-hover-text-black:hover{color:#000!important}
 | 
				
			||||||
 | 
					.w3-text-grey,.w3-hover-text-grey:hover,.w3-text-gray,.w3-hover-text-gray:hover{color:#757575!important}
 | 
				
			||||||
 | 
					.w3-text-light-grey,.w3-hover-text-light-grey:hover,.w3-text-light-gray,.w3-hover-text-light-gray:hover{color:#f1f1f1!important}
 | 
				
			||||||
 | 
					.w3-text-dark-grey,.w3-hover-text-dark-grey:hover,.w3-text-dark-gray,.w3-hover-text-dark-gray:hover{color:#3a3a3a!important}
 | 
				
			||||||
 | 
					.w3-border-amber,.w3-hover-border-amber:hover{border-color:#ffc107!important}
 | 
				
			||||||
 | 
					.w3-border-aqua,.w3-hover-border-aqua:hover{border-color:#00ffff!important}
 | 
				
			||||||
 | 
					.w3-border-blue,.w3-hover-border-blue:hover{border-color:#2196F3!important}
 | 
				
			||||||
 | 
					.w3-border-light-blue,.w3-hover-border-light-blue:hover{border-color:#87CEEB!important}
 | 
				
			||||||
 | 
					.w3-border-brown,.w3-hover-border-brown:hover{border-color:#795548!important}
 | 
				
			||||||
 | 
					.w3-border-cyan,.w3-hover-border-cyan:hover{border-color:#00bcd4!important}
 | 
				
			||||||
 | 
					.w3-border-blue-grey,.w3-hover-border-blue-grey:hover,.w3-border-blue-gray,.w3-hover-border-blue-gray:hover{border-color:#607d8b!important}
 | 
				
			||||||
 | 
					.w3-border-green,.w3-hover-border-green:hover{border-color:#4CAF50!important}
 | 
				
			||||||
 | 
					.w3-border-light-green,.w3-hover-border-light-green:hover{border-color:#8bc34a!important}
 | 
				
			||||||
 | 
					.w3-border-indigo,.w3-hover-border-indigo:hover{border-color:#3f51b5!important}
 | 
				
			||||||
 | 
					.w3-border-khaki,.w3-hover-border-khaki:hover{border-color:#f0e68c!important}
 | 
				
			||||||
 | 
					.w3-border-lime,.w3-hover-border-lime:hover{border-color:#cddc39!important}
 | 
				
			||||||
 | 
					.w3-border-orange,.w3-hover-border-orange:hover{border-color:#ff9800!important}
 | 
				
			||||||
 | 
					.w3-border-deep-orange,.w3-hover-border-deep-orange:hover{border-color:#ff5722!important}
 | 
				
			||||||
 | 
					.w3-border-pink,.w3-hover-border-pink:hover{border-color:#e91e63!important}
 | 
				
			||||||
 | 
					.w3-border-purple,.w3-hover-border-purple:hover{border-color:#9c27b0!important}
 | 
				
			||||||
 | 
					.w3-border-deep-purple,.w3-hover-border-deep-purple:hover{border-color:#673ab7!important}
 | 
				
			||||||
 | 
					.w3-border-red,.w3-hover-border-red:hover{border-color:#f44336!important}
 | 
				
			||||||
 | 
					.w3-border-sand,.w3-hover-border-sand:hover{border-color:#fdf5e6!important}
 | 
				
			||||||
 | 
					.w3-border-teal,.w3-hover-border-teal:hover{border-color:#009688!important}
 | 
				
			||||||
 | 
					.w3-border-yellow,.w3-hover-border-yellow:hover{border-color:#ffeb3b!important}
 | 
				
			||||||
 | 
					.w3-border-white,.w3-hover-border-white:hover{border-color:#fff!important}
 | 
				
			||||||
 | 
					.w3-border-black,.w3-hover-border-black:hover{border-color:#000!important}
 | 
				
			||||||
 | 
					.w3-border-grey,.w3-hover-border-grey:hover,.w3-border-gray,.w3-hover-border-gray:hover{border-color:#9e9e9e!important}
 | 
				
			||||||
 | 
					.w3-border-light-grey,.w3-hover-border-light-grey:hover,.w3-border-light-gray,.w3-hover-border-light-gray:hover{border-color:#f1f1f1!important}
 | 
				
			||||||
 | 
					.w3-border-dark-grey,.w3-hover-border-dark-grey:hover,.w3-border-dark-gray,.w3-hover-border-dark-gray:hover{border-color:#616161!important}
 | 
				
			||||||
 | 
					.w3-border-pale-red,.w3-hover-border-pale-red:hover{border-color:#ffe7e7!important}.w3-border-pale-green,.w3-hover-border-pale-green:hover{border-color:#e7ffe7!important}
 | 
				
			||||||
 | 
					.w3-border-pale-yellow,.w3-hover-border-pale-yellow:hover{border-color:#ffffcc!important}.w3-border-pale-blue,.w3-hover-border-pale-blue:hover{border-color:#e7ffff!important}
 | 
				
			||||||
@@ -1,4 +1,5 @@
 | 
				
			|||||||
{
 | 
					{
 | 
				
			||||||
  "type": "tildefriends-app",
 | 
						"type": "tildefriends-app",
 | 
				
			||||||
  "emoji": "📜"
 | 
						"emoji": "📜",
 | 
				
			||||||
 | 
						"previous": "&miGORZ8BwjHg2YO0t4bms6SI28XWPYqnqOZ8u9zsbZc=.sha256"
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
							
								
								
									
										356
									
								
								apps/api/docs.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										356
									
								
								apps/api/docs.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,356 @@
 | 
				
			|||||||
 | 
					export const docs = {};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					docs.global = `# Tilde Friends API Documentation
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Welcome to the Tilde Friends API documentation.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 * [App Globals](#App_Globals)
 | 
				
			||||||
 | 
					 * [Database Interface](#Database)
 | 
				
			||||||
 | 
					 * [Remote Procedure Calls](#tfrpc)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<a id="App_Globals"></a>
 | 
				
			||||||
 | 
					## <span style="color: #aaf">App Globals</span>
 | 
				
			||||||
 | 
					The following are functions and values exposed to all apps in their \`app.js\` or \`handler.js\`.  Most
 | 
				
			||||||
 | 
					of these are asynchronous, returning a \`Promise\` that will be resolved when the call completes, unless
 | 
				
			||||||
 | 
					noted otherwise.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					This is all a work in progess. These are liable to change without warning.  Feedback is welcome.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					The exposed functions in this API balance multiple competing needs:
 | 
				
			||||||
 | 
					 * The surface area of the exposed API ought to be fairly minimal.  If something can be implemented entirely app-side, that is
 | 
				
			||||||
 | 
					   generally preferred over building it into the core.
 | 
				
			||||||
 | 
					 * Everything is built on this API.  Ideally the admin app, the SSB app, and the editor all use standard API exposed to all
 | 
				
			||||||
 | 
					   apps, with appropriate permission guards in place making it so that only trusted apps do potentially destructive operations.
 | 
				
			||||||
 | 
					   There will be some things here that aren't necessarily general use to support what's required.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					If you are looking at the [Tilde Friends source code](https://www.tildefriends.net/~cory/releases/),
 | 
				
			||||||
 | 
					the vast majority of these are implemented in \`src/*.js.c\` files, and exposed to apps via \`core/core.js\`.
 | 
				
			||||||
 | 
					`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					docs['core.user.credentials.session.name'] = `
 | 
				
			||||||
 | 
					*String* The name of the authenticated user.
 | 
				
			||||||
 | 
					`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					docs['app.setDocument()'] = `
 | 
				
			||||||
 | 
					Set the contents of the client <iframe/>.
 | 
				
			||||||
 | 
					### Parameters
 | 
				
			||||||
 | 
					 * *String* **html** The HTML contents.
 | 
				
			||||||
 | 
					`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					docs['core.apps()'] = `
 | 
				
			||||||
 | 
					Gets a list of apps owned by the current user.
 | 
				
			||||||
 | 
					### Returns
 | 
				
			||||||
 | 
					*Array* An array of string names of the apps owned by the current user.
 | 
				
			||||||
 | 
					`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					docs['core.url'] = `
 | 
				
			||||||
 | 
					The url by which the running app is being invoked.
 | 
				
			||||||
 | 
					`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					docs['app.localStorageSet()'] = `
 | 
				
			||||||
 | 
					Set a value in browser local storage.
 | 
				
			||||||
 | 
					### Parameters
 | 
				
			||||||
 | 
					*String* **key** The localStorage key to set.
 | 
				
			||||||
 | 
					*String* **value** The localStorage value to set.
 | 
				
			||||||
 | 
					`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					docs['app.localStorageGet()'] = `
 | 
				
			||||||
 | 
					Gets a value from browser local storage.
 | 
				
			||||||
 | 
					### Parameters
 | 
				
			||||||
 | 
					*String* **key** The key with which the value was set.
 | 
				
			||||||
 | 
					### Returns
 | 
				
			||||||
 | 
					*String* The value, or undefined.
 | 
				
			||||||
 | 
					`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					docs['app.print()'] = `
 | 
				
			||||||
 | 
					Log information for debugging purposes to the server and to the connected browser console.
 | 
				
			||||||
 | 
					### Parameters
 | 
				
			||||||
 | 
					 * ... Any args to print.
 | 
				
			||||||
 | 
					`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					docs['ssb.createIdentity()'] = `
 | 
				
			||||||
 | 
					Create a new SSB identity.
 | 
				
			||||||
 | 
					### Returns
 | 
				
			||||||
 | 
					*String* The created identity public key.
 | 
				
			||||||
 | 
					`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					docs['ssb.getIdentities()'] = `
 | 
				
			||||||
 | 
					Get all SSB identities owned by the current user.
 | 
				
			||||||
 | 
					### Returns
 | 
				
			||||||
 | 
					*Array* An array of public key strings.
 | 
				
			||||||
 | 
					`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					docs['ssb.sqlAsync()'] = `
 | 
				
			||||||
 | 
					Run an SQL query against the sqlite database.
 | 
				
			||||||
 | 
					### Parameters
 | 
				
			||||||
 | 
					 * *String* **query** The sqlite query.
 | 
				
			||||||
 | 
					 * *Array* **args** The query arguments to bind.
 | 
				
			||||||
 | 
					 * *Function* **callback** Callback called for each row result.
 | 
				
			||||||
 | 
					`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					docs['ssb.appendMessageWithIdentity()'] = `
 | 
				
			||||||
 | 
					Signs and stores a message in the SSB database.
 | 
				
			||||||
 | 
					### Parameters
 | 
				
			||||||
 | 
					 * *String* **id** The public key of an SSB identity owned by the authenticated user.
 | 
				
			||||||
 | 
					 * *Object* **message** The unsigned message.
 | 
				
			||||||
 | 
					`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					docs['ssb.storeMessage()'] = `
 | 
				
			||||||
 | 
					Verifies and stores a signed message in the SSB database.
 | 
				
			||||||
 | 
					### Parameters
 | 
				
			||||||
 | 
					 * *Object* **message** The valid, signed message to store.
 | 
				
			||||||
 | 
					`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					docs['ssb.blobStore()'] = `
 | 
				
			||||||
 | 
					Store a blob in the SSB database.
 | 
				
			||||||
 | 
					### Parameters
 | 
				
			||||||
 | 
					 * *String*/*Uint8Array* **blob** The blob contents to store
 | 
				
			||||||
 | 
					### Returns
 | 
				
			||||||
 | 
					*String* The stored blob ID.
 | 
				
			||||||
 | 
					`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					docs['ssb.blobGet()'] = `
 | 
				
			||||||
 | 
					Fetches a blob from the database.
 | 
				
			||||||
 | 
					### Parameters
 | 
				
			||||||
 | 
					 * *String* **blob_id** The blob identifier to fetch (\`&....sha256\`).
 | 
				
			||||||
 | 
					### Returns
 | 
				
			||||||
 | 
					*ArrayBuffer* The blob data.
 | 
				
			||||||
 | 
					`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					docs['print()'] = `
 | 
				
			||||||
 | 
					Log debug information both to the server's console and to the visiting user's browser console when possible.
 | 
				
			||||||
 | 
					### Parameters
 | 
				
			||||||
 | 
					 * **...** Whatever you want to log.  Will be joined with spaces.
 | 
				
			||||||
 | 
					`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					docs['database()'] = `
 | 
				
			||||||
 | 
					Returns a database instance that is specific to the authenticated user and the given key.
 | 
				
			||||||
 | 
					### Parameters
 | 
				
			||||||
 | 
					 * *String* **key** The database key.
 | 
				
			||||||
 | 
					### Returns
 | 
				
			||||||
 | 
					 *Database* A database.
 | 
				
			||||||
 | 
					`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					docs['my_shared_database()'] = `
 | 
				
			||||||
 | 
					Returns a database instance that is specific to the authenticated user and the given key.
 | 
				
			||||||
 | 
					### Parameters
 | 
				
			||||||
 | 
					 * *String* **package_name** The database package name.
 | 
				
			||||||
 | 
					 * *String* **key** The database key.
 | 
				
			||||||
 | 
					### Returns
 | 
				
			||||||
 | 
					*Database* A database.
 | 
				
			||||||
 | 
					`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					docs['shared_database()'] = `
 | 
				
			||||||
 | 
					Returns a database instance that is shared between all users of the app, determined by its owner and app name.
 | 
				
			||||||
 | 
					### Parameters
 | 
				
			||||||
 | 
					 * *String* **key** The database key.
 | 
				
			||||||
 | 
					### Returns
 | 
				
			||||||
 | 
					*Database* A database.
 | 
				
			||||||
 | 
					`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					docs['base64Decode()'] = `
 | 
				
			||||||
 | 
					Decode a base64 string to bytes.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Completes synchronously.
 | 
				
			||||||
 | 
					### Parameters
 | 
				
			||||||
 | 
					 * *String* value The base64-encoded string.
 | 
				
			||||||
 | 
					### Returns
 | 
				
			||||||
 | 
					*Uint8Array* The decoded bytes.
 | 
				
			||||||
 | 
					`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					docs['base64Encode()'] = `
 | 
				
			||||||
 | 
					Encode bytes to a base64 string.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Completes synchronously.
 | 
				
			||||||
 | 
					### Parameters
 | 
				
			||||||
 | 
					 * *Uint8Array* The bytes to encode.
 | 
				
			||||||
 | 
					### Returns
 | 
				
			||||||
 | 
					*String* The base64-encoded string.
 | 
				
			||||||
 | 
					`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					docs['utf8Decode()'] = `
 | 
				
			||||||
 | 
					Decode UTF-8 bytes to a string.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Completes synchronously.
 | 
				
			||||||
 | 
					### Parameters
 | 
				
			||||||
 | 
					 * *Uint8Array* **value** The value to decode.
 | 
				
			||||||
 | 
					### Returns
 | 
				
			||||||
 | 
					*String* The value as a string.
 | 
				
			||||||
 | 
					`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					docs['utf8Encode()'] = `
 | 
				
			||||||
 | 
					Encodes a string to UTF-8 bytes.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Completes synchronously.
 | 
				
			||||||
 | 
					### Parameters
 | 
				
			||||||
 | 
					 * *String* **value** The value to encode.
 | 
				
			||||||
 | 
					### Returns
 | 
				
			||||||
 | 
					*Uint8Array* The encoded \`value\`.
 | 
				
			||||||
 | 
					`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					docs['setTimeout()'] = `
 | 
				
			||||||
 | 
					Call a function after some delay.
 | 
				
			||||||
 | 
					### Parameters
 | 
				
			||||||
 | 
					 * *Function* **callback** The function to call.
 | 
				
			||||||
 | 
					 * *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?
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Completes synchronously.
 | 
				
			||||||
 | 
					### Parameters
 | 
				
			||||||
 | 
					 * *Integer* **exit_code** System exit code.
 | 
				
			||||||
 | 
					`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					docs['version()'] = `
 | 
				
			||||||
 | 
					Gets version information for the running server.
 | 
				
			||||||
 | 
					### Returns
 | 
				
			||||||
 | 
					*Object* Keys are things like \`name\` and \`number\` for the server itself and \`libuv\` and \`openssl\` for
 | 
				
			||||||
 | 
					dependencies.  Values are *String* version numbers.
 | 
				
			||||||
 | 
					`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					docs['platform()'] = `
 | 
				
			||||||
 | 
					Gets the host operating system platform of the running server.
 | 
				
			||||||
 | 
					### Returns
 | 
				
			||||||
 | 
					*String* The platform, one of \`windows\`, \`android\`, \`linux\`, or \`other\`.
 | 
				
			||||||
 | 
					`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					docs['getFile()'] = `
 | 
				
			||||||
 | 
					Gets a file from the running app.
 | 
				
			||||||
 | 
					### Parameters
 | 
				
			||||||
 | 
					 * *String* **name** Name of the file to retrieve.
 | 
				
			||||||
 | 
					### Returns
 | 
				
			||||||
 | 
					*Uint8Array* The contents of a file from the app with the given name, or *undefined*.
 | 
				
			||||||
 | 
					`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					docs.database = `
 | 
				
			||||||
 | 
					# <span style="color: #aaf">Database</span>
 | 
				
			||||||
 | 
					Local-only storage is provided by a \`Database\` type representing a key-value store.
 | 
				
			||||||
 | 
					`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					docs['database.get()'] = `
 | 
				
			||||||
 | 
					Gets a value from the database.
 | 
				
			||||||
 | 
					### Parameters
 | 
				
			||||||
 | 
					 * *String* **key** The key.
 | 
				
			||||||
 | 
					### Returns
 | 
				
			||||||
 | 
					*String* The value from the database or undefined if not found.
 | 
				
			||||||
 | 
					`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					docs['database.getAll()'] = `
 | 
				
			||||||
 | 
					Gets all keys from the database.
 | 
				
			||||||
 | 
					### Returns
 | 
				
			||||||
 | 
					*Array* An array of *String* key names for all keys in the given database.
 | 
				
			||||||
 | 
					`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					docs['database.getLike()'] = `
 | 
				
			||||||
 | 
					Gets all keys and values from the database matching a pattern.
 | 
				
			||||||
 | 
					### Parameters
 | 
				
			||||||
 | 
					 * *String* **pattern** An sqlite \`LIKE\` pattern to match keys against.
 | 
				
			||||||
 | 
					### Returns
 | 
				
			||||||
 | 
					*Object* An object whose keys are the database keys and values are the database values that match the given pattern.
 | 
				
			||||||
 | 
					`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					docs['database.set()'] = `
 | 
				
			||||||
 | 
					Sets a value in the database, creating a new entry or replacing an existing entry.
 | 
				
			||||||
 | 
					### Parameters
 | 
				
			||||||
 | 
					 * *String* **key** The key.
 | 
				
			||||||
 | 
					 * *String* **value** The value.
 | 
				
			||||||
 | 
					`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					docs['database.exchange()'] = `
 | 
				
			||||||
 | 
					Performs an atomic compare and exchange operation, setting a value in the database only if its current value matches what is expected.
 | 
				
			||||||
 | 
					### Parameters
 | 
				
			||||||
 | 
					 * *String* **key** The key.
 | 
				
			||||||
 | 
					 * *String* **expected** The expected value.
 | 
				
			||||||
 | 
					 * *String* **value** The new value.
 | 
				
			||||||
 | 
					 ### Returns
 | 
				
			||||||
 | 
					 *Boolean* true if the value is now the given value.
 | 
				
			||||||
 | 
					`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					docs['database.remove()'] = `
 | 
				
			||||||
 | 
					Removes an entry from the database if it exists.
 | 
				
			||||||
 | 
					### Parameters
 | 
				
			||||||
 | 
					 * *String* **key** The key.
 | 
				
			||||||
 | 
					`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					docs.tfrpc = `
 | 
				
			||||||
 | 
					# <span style="color: #aaf" id="tfrpc">tfrpc</span>
 | 
				
			||||||
 | 
					\`tfrpc.js\` is a small helper script that is available to be used to facilitate communication between parts of an application.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					\`tfrpc.js\` can be used to asynchronously make calls between the app code running in a sandboxed iframe in the browser
 | 
				
			||||||
 | 
					and the app process on the server.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					From \`app.js\`:
 | 
				
			||||||
 | 
					\`\`\`
 | 
				
			||||||
 | 
					import * as tfrpc from '/tfrpc.js';
 | 
				
			||||||
 | 
					\`\`\`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					From script running in the browser:
 | 
				
			||||||
 | 
					\`\`\`
 | 
				
			||||||
 | 
					import * as tfrpc from '/static/tfrpc.js';
 | 
				
			||||||
 | 
					\`\`\`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Either side can register or call functions, though they must be registered before they can be called.  Arguments and return
 | 
				
			||||||
 | 
					values are ultimately serialized by means that attempt to preserve most JSON-serializable values as well as functions themselves.
 | 
				
			||||||
 | 
					`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					docs['tfrpc.register()'] = `
 | 
				
			||||||
 | 
					Register a function, allowing it to be called remotely.
 | 
				
			||||||
 | 
					### Parameters
 | 
				
			||||||
 | 
					 * *Function* **function** The function to register.  Its name will be how it will be called.
 | 
				
			||||||
 | 
					`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					docs['tfrpc.rpc.*()'] = `
 | 
				
			||||||
 | 
					Call a remote function.
 | 
				
			||||||
 | 
					### Parameters
 | 
				
			||||||
 | 
					 * **...** Parameters to pass to the function.
 | 
				
			||||||
 | 
					### Returns
 | 
				
			||||||
 | 
					The return value of the called function.
 | 
				
			||||||
 | 
					`;
 | 
				
			||||||
@@ -1,4 +1,5 @@
 | 
				
			|||||||
{
 | 
					{
 | 
				
			||||||
  "type": "tildefriends-app",
 | 
						"type": "tildefriends-app",
 | 
				
			||||||
  "emoji": "💻"
 | 
						"emoji": "💻",
 | 
				
			||||||
 | 
						"previous": "&icsPplXHgmpkbNWyo/WTvRdT/A8BXxW4lJixOtP4ueQ=.sha256"
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
							
								
								
									
										185
									
								
								apps/apps/app.js
									
									
									
									
									
								
							
							
						
						
									
										185
									
								
								apps/apps/app.js
									
									
									
									
									
								
							@@ -1,25 +1,87 @@
 | 
				
			|||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * Fetches information about the applications
 | 
				
			||||||
 | 
					 * @param apps Record<appName, blobId>
 | 
				
			||||||
 | 
					 * @returns an object including the apps' name, emoji, and blobs ids
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
async function fetch_info(apps) {
 | 
					async function fetch_info(apps) {
 | 
				
			||||||
	let result = {};
 | 
						let result = {};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// For each app
 | 
				
			||||||
	for (let [key, value] of Object.entries(apps)) {
 | 
						for (let [key, value] of Object.entries(apps)) {
 | 
				
			||||||
 | 
							// Get it's blob and parse it
 | 
				
			||||||
		let blob = await ssb.blobGet(value);
 | 
							let blob = await ssb.blobGet(value);
 | 
				
			||||||
		blob = blob ? utf8Decode(blob) : '{}';
 | 
							blob = blob ? utf8Decode(blob) : '{}';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// Add it to the result object
 | 
				
			||||||
		result[key] = JSON.parse(blob);
 | 
							result[key] = JSON.parse(blob);
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return result;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 *
 | 
				
			||||||
 | 
					 *
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					async function fetch_shared_apps() {
 | 
				
			||||||
 | 
						let messages = {};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						await ssb.sqlAsync(
 | 
				
			||||||
 | 
							`
 | 
				
			||||||
 | 
									SELECT messages.id, messages.previous, messages.author, messages.sequence, messages.timestamp, messages.hash, json(messages.content) AS content, messages.signature
 | 
				
			||||||
 | 
									FROM messages_fts('"application/tildefriends"')
 | 
				
			||||||
 | 
									JOIN messages ON messages.rowid = messages_fts.rowid
 | 
				
			||||||
 | 
									ORDER BY messages.timestamp
 | 
				
			||||||
 | 
							`,
 | 
				
			||||||
 | 
							[],
 | 
				
			||||||
 | 
							function (row) {
 | 
				
			||||||
 | 
								let content = JSON.parse(row.content);
 | 
				
			||||||
 | 
								for (let mention of content.mentions) {
 | 
				
			||||||
 | 
									if (mention?.type === 'application/tildefriends') {
 | 
				
			||||||
 | 
										messages[JSON.stringify([row.author, mention.name])] = {
 | 
				
			||||||
 | 
											message: row,
 | 
				
			||||||
 | 
											blob: mention.link,
 | 
				
			||||||
 | 
											name: mention.name,
 | 
				
			||||||
 | 
										};
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						let result = {};
 | 
				
			||||||
 | 
						for (let app of Object.values(messages).sort(
 | 
				
			||||||
 | 
							(x, y) => y.message.timestamp - x.message.timestamp
 | 
				
			||||||
 | 
						)) {
 | 
				
			||||||
 | 
							let app_object = JSON.parse(utf8Decode(await ssb.blobGet(app.blob)));
 | 
				
			||||||
 | 
							if (app_object) {
 | 
				
			||||||
 | 
								app_object.blob_id = app.blob;
 | 
				
			||||||
 | 
								result[app.name] = app_object;
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
	return result;
 | 
						return result;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
async function main() {
 | 
					async function main() {
 | 
				
			||||||
	var apps = await fetch_info(await core.apps());
 | 
						const apps = await fetch_info(await core.apps());
 | 
				
			||||||
	var core_apps = await fetch_info(await core.apps('core'));
 | 
						const core_apps = await fetch_info(await core.apps('core'));
 | 
				
			||||||
	var doc = `<!DOCTYPE html>
 | 
						const shared_apps = await fetch_shared_apps();
 | 
				
			||||||
<html>
 | 
					
 | 
				
			||||||
<head>
 | 
						const stylesheet = `
 | 
				
			||||||
	<style>
 | 
							body {
 | 
				
			||||||
 | 
								color: whitesmoke;
 | 
				
			||||||
 | 
								font-family: sans-serif;
 | 
				
			||||||
 | 
								margin: 16px;
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
		.container {
 | 
							.container {
 | 
				
			||||||
			display: grid;
 | 
								display: grid;
 | 
				
			||||||
			grid-template-columns: repeat(auto-fill, 64px);
 | 
								grid-template-columns: repeat(auto-fill, 64px);
 | 
				
			||||||
 | 
								gap: 1em;
 | 
				
			||||||
			justify-content: space-around;
 | 
								justify-content: space-around;
 | 
				
			||||||
 | 
								background-color: #ffffff10;
 | 
				
			||||||
 | 
								border: 2px solid #073642;
 | 
				
			||||||
 | 
								border-radius: 8px;
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		.app {
 | 
							.app {
 | 
				
			||||||
			height: 96px;
 | 
								height: 96px;
 | 
				
			||||||
			width: 64px;
 | 
								width: 64px;
 | 
				
			||||||
@@ -34,44 +96,87 @@ async function main() {
 | 
				
			|||||||
			max-width: 64px;
 | 
								max-width: 64px;
 | 
				
			||||||
			text-overflow: ellipsis ellipsis;
 | 
								text-overflow: ellipsis ellipsis;
 | 
				
			||||||
			overflow: hidden;
 | 
								overflow: hidden;
 | 
				
			||||||
 | 
								color: whitesmoke;
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	</style>
 | 
						`;
 | 
				
			||||||
</head>
 | 
					 | 
				
			||||||
<body style="background: #888">
 | 
					 | 
				
			||||||
<h1 id="apps_title">Apps</h1>
 | 
					 | 
				
			||||||
<div id="apps" class="container"></div>
 | 
					 | 
				
			||||||
<h1>Core Apps</h1>
 | 
					 | 
				
			||||||
<div id="core_apps" class="container"></div>
 | 
					 | 
				
			||||||
</body>
 | 
					 | 
				
			||||||
<script>
 | 
					 | 
				
			||||||
	function populate_apps(id, name, apps) {
 | 
					 | 
				
			||||||
		var list = document.getElementById(id);
 | 
					 | 
				
			||||||
		for (let app of Object.keys(apps).sort()) {
 | 
					 | 
				
			||||||
			let div = list.appendChild(document.createElement('div'));
 | 
					 | 
				
			||||||
			div.classList.add('app');
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
			let icon_a = document.createElement('a');
 | 
						const body = `
 | 
				
			||||||
			let icon = document.createElement('div');
 | 
							<h1 style="text-shadow: #808080 0 0 10px;">Welcome to Tilde Friends.</h1>
 | 
				
			||||||
			icon.appendChild(document.createTextNode(apps[app].emoji || '📦'));
 | 
					 | 
				
			||||||
			icon.style.fontSize = 'xxx-large';
 | 
					 | 
				
			||||||
			icon_a.appendChild(icon);
 | 
					 | 
				
			||||||
			icon_a.href = '/~' + name + '/' + app + '/';
 | 
					 | 
				
			||||||
			icon_a.target = '_top';
 | 
					 | 
				
			||||||
			div.appendChild(icon_a);
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
			let a = document.createElement('a');
 | 
							<h2>your apps</h2>
 | 
				
			||||||
			a.appendChild(document.createTextNode(app));
 | 
							<div id="apps" class="container"></div>
 | 
				
			||||||
			a.href = '/~' + name + '/' + app + '/';
 | 
					
 | 
				
			||||||
			a.target = '_top';
 | 
							<h2>shared apps</h2>
 | 
				
			||||||
			div.appendChild(a);
 | 
							<div id="shared_apps" class="container"></div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							<h2>core apps</h2>
 | 
				
			||||||
 | 
							<div id="core_apps" class="container"></div>
 | 
				
			||||||
 | 
						`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						const script = `
 | 
				
			||||||
 | 
							/*
 | 
				
			||||||
 | 
							 * Creates a list of apps
 | 
				
			||||||
 | 
							 * @param id the id of the element to populate
 | 
				
			||||||
 | 
							 * @param name (a username, 'core' or undefined)
 | 
				
			||||||
 | 
							 * @param apps Object, a list of apps
 | 
				
			||||||
 | 
							 */
 | 
				
			||||||
 | 
							function populate_apps(id, name, apps) {
 | 
				
			||||||
 | 
								// Our target
 | 
				
			||||||
 | 
								var list = document.getElementById(id);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								// 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'));
 | 
				
			||||||
 | 
									div.classList.add('app');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
									// The app's icon
 | 
				
			||||||
 | 
									let href = name ? '/~' + name + '/' + app + '/' : ('/' + apps[app].blob_id + '/');
 | 
				
			||||||
 | 
									let icon_a = document.createElement('a');
 | 
				
			||||||
 | 
									let icon = document.createElement('div');
 | 
				
			||||||
 | 
									icon.appendChild(document.createTextNode(apps[app].emoji || '📦'));
 | 
				
			||||||
 | 
									icon.style.fontSize = 'xxx-large';
 | 
				
			||||||
 | 
									icon_a.appendChild(icon);
 | 
				
			||||||
 | 
									icon_a.href = href;
 | 
				
			||||||
 | 
									icon_a.target = '_top';
 | 
				
			||||||
 | 
									div.appendChild(icon_a);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
									// The app's name
 | 
				
			||||||
 | 
									let a = document.createElement('a');
 | 
				
			||||||
 | 
									a.appendChild(document.createTextNode(app));
 | 
				
			||||||
 | 
									a.href = href;
 | 
				
			||||||
 | 
									a.target = '_top';
 | 
				
			||||||
 | 
									div.appendChild(a);
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	}
 | 
					
 | 
				
			||||||
	document.getElementById('apps_title').innerText = "~${escape(core.user.credentials?.session?.name || 'guest')}'s Apps";
 | 
							populate_apps('apps', '${core.user.credentials?.session?.name}', ${JSON.stringify(apps)});
 | 
				
			||||||
	populate_apps('apps', '${core.user.credentials?.session?.name}', ${JSON.stringify(apps)});
 | 
							populate_apps('core_apps', 'core', ${JSON.stringify(core_apps)});
 | 
				
			||||||
	populate_apps('core_apps', 'core', ${JSON.stringify(core_apps)});
 | 
							populate_apps('shared_apps', undefined, ${JSON.stringify(shared_apps)});
 | 
				
			||||||
</script>
 | 
						`;
 | 
				
			||||||
</html>`
 | 
					
 | 
				
			||||||
	app.setDocument(doc);
 | 
						// Build the document
 | 
				
			||||||
 | 
						const document = `
 | 
				
			||||||
 | 
						<!DOCTYPE html>
 | 
				
			||||||
 | 
						<html>
 | 
				
			||||||
 | 
							<head>
 | 
				
			||||||
 | 
								<style>
 | 
				
			||||||
 | 
									${stylesheet}
 | 
				
			||||||
 | 
								</style>
 | 
				
			||||||
 | 
							</head>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							<body>
 | 
				
			||||||
 | 
								${body}
 | 
				
			||||||
 | 
							</body>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							<script>
 | 
				
			||||||
 | 
								${script}
 | 
				
			||||||
 | 
							</script>
 | 
				
			||||||
 | 
						</html>`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Send it to the browser
 | 
				
			||||||
 | 
						app.setDocument(document);
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
main();
 | 
					main();
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,3 +0,0 @@
 | 
				
			|||||||
{
 | 
					 | 
				
			||||||
  "type": "tildefriends-app"
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@@ -1,55 +0,0 @@
 | 
				
			|||||||
async function get_apps() {
 | 
					 | 
				
			||||||
	let results = {};
 | 
					 | 
				
			||||||
	await ssb.sqlStream(`
 | 
					 | 
				
			||||||
				SELECT messages.*
 | 
					 | 
				
			||||||
				FROM messages_fts('"application/tildefriends"')
 | 
					 | 
				
			||||||
				JOIN messages ON messages.rowid = messages_fts.rowid
 | 
					 | 
				
			||||||
				ORDER BY timestamp
 | 
					 | 
				
			||||||
		`,
 | 
					 | 
				
			||||||
		[],
 | 
					 | 
				
			||||||
		function(row) {
 | 
					 | 
				
			||||||
			let content = JSON.parse(row.content);
 | 
					 | 
				
			||||||
			for (let mention of content.mentions) {
 | 
					 | 
				
			||||||
				if (mention?.type === 'application/tildefriends') {
 | 
					 | 
				
			||||||
					results[JSON.stringify([row.author, mention.name])] = {
 | 
					 | 
				
			||||||
						message: row,
 | 
					 | 
				
			||||||
						blob: mention.link,
 | 
					 | 
				
			||||||
						name: mention.name,
 | 
					 | 
				
			||||||
					};
 | 
					 | 
				
			||||||
				}
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
		});
 | 
					 | 
				
			||||||
	return Object.values(results).sort((x, y) => y.message.timestamp - x.message.timestamp);
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
function render_app(app) {
 | 
					 | 
				
			||||||
	return `
 | 
					 | 
				
			||||||
		<div style="border: 2px solid white; display: inline-block; margin: 8px; padding: 8px">
 | 
					 | 
				
			||||||
			<a href="/~cory/ssb/#${app.message.author}">@</a>
 | 
					 | 
				
			||||||
			<a href="/~cory/ssb/#${app.message.id}">%</a>
 | 
					 | 
				
			||||||
			<a href="/${app.blob}/">${app.name}</a>
 | 
					 | 
				
			||||||
		</div>
 | 
					 | 
				
			||||||
	`;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
async function main() {
 | 
					 | 
				
			||||||
	let apps = await get_apps();
 | 
					 | 
				
			||||||
	app.setDocument(`
 | 
					 | 
				
			||||||
		<html>
 | 
					 | 
				
			||||||
			<head>
 | 
					 | 
				
			||||||
				<base target="_top">
 | 
					 | 
				
			||||||
				<style>
 | 
					 | 
				
			||||||
					a:link { color: #bbf; }
 | 
					 | 
				
			||||||
					a:visited { color: #ddd; }
 | 
					 | 
				
			||||||
					a:hover { color: #ddf; }
 | 
					 | 
				
			||||||
				</style>
 | 
					 | 
				
			||||||
			</head>
 | 
					 | 
				
			||||||
			<body style="color: #fff">
 | 
					 | 
				
			||||||
				<h1>${apps.length} apps</h1>
 | 
					 | 
				
			||||||
				${apps.map(render_app).join('\n')}
 | 
					 | 
				
			||||||
			</body>
 | 
					 | 
				
			||||||
		</html>
 | 
					 | 
				
			||||||
	`);
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
main();
 | 
					 | 
				
			||||||
							
								
								
									
										5
									
								
								apps/blog.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								apps/blog.json
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,5 @@
 | 
				
			|||||||
 | 
					{
 | 
				
			||||||
 | 
						"type": "tildefriends-app",
 | 
				
			||||||
 | 
						"emoji": "🪵",
 | 
				
			||||||
 | 
						"previous": "&TIrBnpN3iz3O9L9MCCteAcVJZjA83EKdcfu4SCM76VE=.sha256"
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										8
									
								
								apps/blog/app.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								apps/blog/app.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,8 @@
 | 
				
			|||||||
 | 
					import * as blog from './blog.js';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					async function main() {
 | 
				
			||||||
 | 
						let blogs = await blog.get_posts();
 | 
				
			||||||
 | 
						await app.setDocument(blog.render_html(blogs));
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					main();
 | 
				
			||||||
							
								
								
									
										207
									
								
								apps/blog/blog.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										207
									
								
								apps/blog/blog.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,207 @@
 | 
				
			|||||||
 | 
					import * as commonmark from './commonmark.min.js';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function escape(text) {
 | 
				
			||||||
 | 
						return (text ?? '')
 | 
				
			||||||
 | 
							.replaceAll('&', '&')
 | 
				
			||||||
 | 
							.replaceAll('<', '<')
 | 
				
			||||||
 | 
							.replaceAll('>', '>');
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function escapeAttribute(text) {
 | 
				
			||||||
 | 
						return (text ?? '')
 | 
				
			||||||
 | 
							.replaceAll('&', '&')
 | 
				
			||||||
 | 
							.replaceAll('<', '<')
 | 
				
			||||||
 | 
							.replaceAll('>', '>')
 | 
				
			||||||
 | 
							.replaceAll('"', '"')
 | 
				
			||||||
 | 
							.replaceAll("'", ''');
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export async function get_blog_message(id) {
 | 
				
			||||||
 | 
						let message;
 | 
				
			||||||
 | 
						await ssb.sqlAsync(
 | 
				
			||||||
 | 
							'SELECT author, timestamp, content FROM messages WHERE id = ?',
 | 
				
			||||||
 | 
							[id],
 | 
				
			||||||
 | 
							function (row) {
 | 
				
			||||||
 | 
								let content = JSON.parse(row.content);
 | 
				
			||||||
 | 
								message = {
 | 
				
			||||||
 | 
									author: row.author,
 | 
				
			||||||
 | 
									timestamp: row.timestamp,
 | 
				
			||||||
 | 
									blog: content?.blog,
 | 
				
			||||||
 | 
									title: content?.title,
 | 
				
			||||||
 | 
								};
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						);
 | 
				
			||||||
 | 
						if (message) {
 | 
				
			||||||
 | 
							await ssb.sqlAsync(
 | 
				
			||||||
 | 
								`
 | 
				
			||||||
 | 
									SELECT json_extract(content, '$.name') AS name
 | 
				
			||||||
 | 
									FROM messages
 | 
				
			||||||
 | 
									WHERE author = ?
 | 
				
			||||||
 | 
									AND json_extract(content, '$.type') = 'about'
 | 
				
			||||||
 | 
									AND json_extract(content, '$.about') = author
 | 
				
			||||||
 | 
									AND name IS NOT NULL
 | 
				
			||||||
 | 
									ORDER BY sequence DESC LIMIT 1
 | 
				
			||||||
 | 
								`,
 | 
				
			||||||
 | 
								[message.author],
 | 
				
			||||||
 | 
								function (row) {
 | 
				
			||||||
 | 
									message.name = row.name;
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							);
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return message;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function markdown(md) {
 | 
				
			||||||
 | 
						let reader = new commonmark.Parser({safe: true});
 | 
				
			||||||
 | 
						let writer = new commonmark.HtmlRenderer();
 | 
				
			||||||
 | 
						let parsed = reader.parse(md || '');
 | 
				
			||||||
 | 
						let walker = parsed.walker();
 | 
				
			||||||
 | 
						let event, node;
 | 
				
			||||||
 | 
						while ((event = walker.next())) {
 | 
				
			||||||
 | 
							node = event.node;
 | 
				
			||||||
 | 
							if (event.entering) {
 | 
				
			||||||
 | 
								if (node.destination?.startsWith('&')) {
 | 
				
			||||||
 | 
									node.destination =
 | 
				
			||||||
 | 
										'/' + node.destination + '/view?filename=' + node.firstChild?.literal;
 | 
				
			||||||
 | 
								} else if (
 | 
				
			||||||
 | 
									node.destination?.startsWith('@') ||
 | 
				
			||||||
 | 
									node.destination?.startsWith('%')
 | 
				
			||||||
 | 
								) {
 | 
				
			||||||
 | 
									node.destination = '/~core/ssb/#' + escape(node.destination);
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return writer.render(parsed);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export async function render_blog_post_html(blog_post) {
 | 
				
			||||||
 | 
						let blob = utf8Decode(await ssb.blobGet(blog_post.blog));
 | 
				
			||||||
 | 
						return `<!DOCTYPE html>
 | 
				
			||||||
 | 
							<html>
 | 
				
			||||||
 | 
								<head>
 | 
				
			||||||
 | 
									<title>🪵Tilde Friends Blog - ${markdown(blog_post.title)}</title>
 | 
				
			||||||
 | 
									<base target="_top">
 | 
				
			||||||
 | 
								</head>
 | 
				
			||||||
 | 
								<body>
 | 
				
			||||||
 | 
									<h1><a href="./">🪵Tilde Friends Blog</a></h1>
 | 
				
			||||||
 | 
									<div>
 | 
				
			||||||
 | 
										<div><a href="../ssb/#${escapeAttribute(blog_post.author)}">${escape(blog_post.name)}</a> ${escape(new Date(blog_post.timestamp).toString())}</div>
 | 
				
			||||||
 | 
										<div>${markdown(blob)}</div>
 | 
				
			||||||
 | 
									</div>
 | 
				
			||||||
 | 
								</body>
 | 
				
			||||||
 | 
							</html>
 | 
				
			||||||
 | 
						`;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function render_blog_post(blog_post) {
 | 
				
			||||||
 | 
						return `
 | 
				
			||||||
 | 
							<div>
 | 
				
			||||||
 | 
								<h2><a href="/~${core.app.owner}/${core.app.name}/${escapeAttribute(blog_post.id)}">${escape(blog_post.title)}</a></h2>
 | 
				
			||||||
 | 
								<div><a href="../ssb/#${escapeAttribute(blog_post.author)}">${escape(blog_post.name)}</a> ${escape(new Date(blog_post.timestamp).toString())}</div>
 | 
				
			||||||
 | 
								<div>${markdown(blog_post.summary)}</div>
 | 
				
			||||||
 | 
							</div>
 | 
				
			||||||
 | 
						`;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function render_html(blogs) {
 | 
				
			||||||
 | 
						return `<!DOCTYPE html>
 | 
				
			||||||
 | 
							<html>
 | 
				
			||||||
 | 
								<head>
 | 
				
			||||||
 | 
									<title>🪵Tilde Friends Blog</title>
 | 
				
			||||||
 | 
									<link href="./atom" type="application/atom+xml" rel="alternate" title="🪵Tilde Blog"/>
 | 
				
			||||||
 | 
									<style>
 | 
				
			||||||
 | 
										html {
 | 
				
			||||||
 | 
											background-color: #ccc;
 | 
				
			||||||
 | 
										}
 | 
				
			||||||
 | 
									</style>
 | 
				
			||||||
 | 
									<base target="_top">
 | 
				
			||||||
 | 
								</head>
 | 
				
			||||||
 | 
								<body>
 | 
				
			||||||
 | 
									<div style="display: flex; flex-direction: row; align-items: center; gap: 1em">
 | 
				
			||||||
 | 
										<h1>🪵Tilde Friends Blog</h1>
 | 
				
			||||||
 | 
										<div style="font-size: xx-small; vertical-align: middle"><a href="/~cory/blog/atom">atom feed</a></div>
 | 
				
			||||||
 | 
									</div>
 | 
				
			||||||
 | 
									${blogs.map((blog_post) => render_blog_post(blog_post)).join('\n')}
 | 
				
			||||||
 | 
								</body>
 | 
				
			||||||
 | 
							</html>`;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function render_blog_post_atom(blog_post) {
 | 
				
			||||||
 | 
						return `<entry>
 | 
				
			||||||
 | 
							<title>${escape(blog_post.title)}</title>
 | 
				
			||||||
 | 
							<link href="/~cory/ssb/#${blog_post.id}" />
 | 
				
			||||||
 | 
							<id>${blog_post.id}</id>
 | 
				
			||||||
 | 
							<published>${escape(new Date(blog_post.timestamp).toString())}</published>
 | 
				
			||||||
 | 
							<summary>${escape(blog_post.summary)}</summary>
 | 
				
			||||||
 | 
							<author>
 | 
				
			||||||
 | 
								<name>${escape(blog_post.name)}</name>
 | 
				
			||||||
 | 
								<feed>${escape(blog_post.author)}</feed>
 | 
				
			||||||
 | 
							</author>
 | 
				
			||||||
 | 
						</entry>`;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function render_atom(blogs) {
 | 
				
			||||||
 | 
						return `<?xml version="1.0" encoding="utf-8"?>
 | 
				
			||||||
 | 
					<feed xmlns="http://www.w3.org/2005/Atom">
 | 
				
			||||||
 | 
						<title>🪵Tilde Blog</title>
 | 
				
			||||||
 | 
						<subtitle>A subtitle.</subtitle>
 | 
				
			||||||
 | 
						<link href="${core.url}/atom" rel="self"/>
 | 
				
			||||||
 | 
						<link href="${core.url}"/>
 | 
				
			||||||
 | 
						<id>${core.url}</id>
 | 
				
			||||||
 | 
						<updated>${new Date().toString()}</updated>
 | 
				
			||||||
 | 
						${blogs.map((blog_post) => render_blog_post_atom(blog_post)).join('\n')}
 | 
				
			||||||
 | 
					</feed>`;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export async function get_posts() {
 | 
				
			||||||
 | 
						let blogs = [];
 | 
				
			||||||
 | 
						let ids = await ssb.getIdentities();
 | 
				
			||||||
 | 
						await ssb.sqlAsync(
 | 
				
			||||||
 | 
							`
 | 
				
			||||||
 | 
							WITH
 | 
				
			||||||
 | 
								blogs AS (
 | 
				
			||||||
 | 
									SELECT
 | 
				
			||||||
 | 
										messages.author,
 | 
				
			||||||
 | 
										messages.id,
 | 
				
			||||||
 | 
										json_extract(messages.content, '$.title') AS title,
 | 
				
			||||||
 | 
										json_extract(messages.content, '$.summary') AS summary,
 | 
				
			||||||
 | 
										json_extract(messages.content, '$.blog') AS blog,
 | 
				
			||||||
 | 
										messages.timestamp
 | 
				
			||||||
 | 
									FROM messages_fts('blog')
 | 
				
			||||||
 | 
									JOIN messages ON messages.rowid = messages_fts.rowid
 | 
				
			||||||
 | 
									WHERE json_extract(messages.content, '$.type') = 'blog'),
 | 
				
			||||||
 | 
								public AS (
 | 
				
			||||||
 | 
									SELECT author FROM (
 | 
				
			||||||
 | 
										SELECT
 | 
				
			||||||
 | 
											messages.author,
 | 
				
			||||||
 | 
											RANK() OVER (PARTITION BY messages.author ORDER BY messages.sequence DESC) AS author_rank,
 | 
				
			||||||
 | 
											json_extract(messages.content, '$.publicWebHosting') AS is_public
 | 
				
			||||||
 | 
										FROM messages_fts('about')
 | 
				
			||||||
 | 
										JOIN messages ON messages.rowid = messages_fts.rowid
 | 
				
			||||||
 | 
										WHERE json_extract(messages.content, '$.type') = 'about' AND is_public IS NOT NULL)
 | 
				
			||||||
 | 
									WHERE author_rank = 1 AND is_public),
 | 
				
			||||||
 | 
								names AS (
 | 
				
			||||||
 | 
									SELECT author, name FROM (
 | 
				
			||||||
 | 
										SELECT
 | 
				
			||||||
 | 
											messages.author,
 | 
				
			||||||
 | 
											RANK() OVER (PARTITION BY messages.author ORDER BY messages.sequence DESC) AS author_rank,
 | 
				
			||||||
 | 
											json_extract(messages.content, '$.name') AS name
 | 
				
			||||||
 | 
										FROM messages_fts('about')
 | 
				
			||||||
 | 
										JOIN messages ON messages.rowid = messages_fts.rowid
 | 
				
			||||||
 | 
										WHERE json_extract(messages.content, '$.type') = 'about' AND
 | 
				
			||||||
 | 
											json_extract(messages.content, '$.about') = messages.author AND
 | 
				
			||||||
 | 
											name IS NOT NULL)
 | 
				
			||||||
 | 
									WHERE author_rank = 1)
 | 
				
			||||||
 | 
							SELECT blogs.*, names.name FROM blogs
 | 
				
			||||||
 | 
							JOIN json_each(?) AS self ON self.value = blogs.author
 | 
				
			||||||
 | 
							JOIN public ON public.author = blogs.author
 | 
				
			||||||
 | 
							LEFT OUTER JOIN names ON names.author = blogs.author
 | 
				
			||||||
 | 
							ORDER BY blogs.timestamp DESC LIMIT 20
 | 
				
			||||||
 | 
						`,
 | 
				
			||||||
 | 
							[JSON.stringify(ids)],
 | 
				
			||||||
 | 
							function (row) {
 | 
				
			||||||
 | 
								blogs.push(row);
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						);
 | 
				
			||||||
 | 
						return blogs;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										1
									
								
								apps/blog/commonmark.min.js
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								apps/blog/commonmark.min.js
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
							
								
								
									
										51
									
								
								apps/blog/handler.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										51
									
								
								apps/blog/handler.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,51 @@
 | 
				
			|||||||
 | 
					import * as blog from './blog.js';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					async function main() {
 | 
				
			||||||
 | 
						if (request.path.startsWith('%') && request.path.endsWith('.sha256')) {
 | 
				
			||||||
 | 
							let id = request.path.startsWith('%25')
 | 
				
			||||||
 | 
								? '%' + request.path.substring(3)
 | 
				
			||||||
 | 
								: request.path;
 | 
				
			||||||
 | 
							let message = await blog.get_blog_message(id);
 | 
				
			||||||
 | 
							if (message) {
 | 
				
			||||||
 | 
								respond({
 | 
				
			||||||
 | 
									data: await blog.render_blog_post_html(message),
 | 
				
			||||||
 | 
									content_type: 'text/html; charset=utf-8',
 | 
				
			||||||
 | 
								});
 | 
				
			||||||
 | 
							} else {
 | 
				
			||||||
 | 
								respond({
 | 
				
			||||||
 | 
									data: `Message ${id} not found.`,
 | 
				
			||||||
 | 
									content_type: 'text/html; charset=utf-8',
 | 
				
			||||||
 | 
								});
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						} else if (request.path == 'atom') {
 | 
				
			||||||
 | 
							let blogs = await blog.get_posts();
 | 
				
			||||||
 | 
							respond({
 | 
				
			||||||
 | 
								data: blog.render_atom(blogs),
 | 
				
			||||||
 | 
								content_type: 'application/atom+xml',
 | 
				
			||||||
 | 
							});
 | 
				
			||||||
 | 
						} else {
 | 
				
			||||||
 | 
							let blogs = await blog.get_posts();
 | 
				
			||||||
 | 
							for (let blog_post of blogs) {
 | 
				
			||||||
 | 
								let title = (blog_post.title || '').replaceAll(/\W/g, '_').toLowerCase();
 | 
				
			||||||
 | 
								if (request.path === title) {
 | 
				
			||||||
 | 
									respond({
 | 
				
			||||||
 | 
										data: await blog.render_blog_post_html(blog_post),
 | 
				
			||||||
 | 
										content_type: 'text/html; charset=utf-8',
 | 
				
			||||||
 | 
									});
 | 
				
			||||||
 | 
									return;
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							respond({
 | 
				
			||||||
 | 
								data: blog.render_html(blogs),
 | 
				
			||||||
 | 
								content_type: 'text/html; charset=utf-8',
 | 
				
			||||||
 | 
							});
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					main().catch(function (error) {
 | 
				
			||||||
 | 
						respond({
 | 
				
			||||||
 | 
							data: `<!DOCTYPE html>
 | 
				
			||||||
 | 
						<pre style="color: #f00">${error.message}\n${error.stack}</pre>`,
 | 
				
			||||||
 | 
							content_type: 'text/html',
 | 
				
			||||||
 | 
						});
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
							
								
								
									
										120
									
								
								apps/blog/lit-all.min.js
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										120
									
								
								apps/blog/lit-all.min.js
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
							
								
								
									
										1
									
								
								apps/blog/lit-all.min.js.map
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								apps/blog/lit-all.min.js.map
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							@@ -1,4 +1,4 @@
 | 
				
			|||||||
{
 | 
					{
 | 
				
			||||||
  "type": "tildefriends-app",
 | 
						"type": "tildefriends-app",
 | 
				
			||||||
  "emoji": "💽"
 | 
						"emoji": "💽"
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@@ -20,7 +20,7 @@ async function database_list() {
 | 
				
			|||||||
	}
 | 
						}
 | 
				
			||||||
	populate_dbs('dbs', ${JSON.stringify(dbs)});
 | 
						populate_dbs('dbs', ${JSON.stringify(dbs)});
 | 
				
			||||||
</script>
 | 
					</script>
 | 
				
			||||||
</html>`
 | 
					</html>`;
 | 
				
			||||||
	app.setDocument(doc);
 | 
						app.setDocument(doc);
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -47,11 +47,11 @@ async function key_list(db) {
 | 
				
			|||||||
	}
 | 
						}
 | 
				
			||||||
	populate_dbs('keys', ${JSON.stringify(object)});
 | 
						populate_dbs('keys', ${JSON.stringify(object)});
 | 
				
			||||||
</script>
 | 
					</script>
 | 
				
			||||||
</html>`
 | 
					</html>`;
 | 
				
			||||||
	app.setDocument(doc);
 | 
						app.setDocument(doc);
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
core.register('message', async function(message) {
 | 
					core.register('message', async function (message) {
 | 
				
			||||||
	if (message.event == 'hashChange') {
 | 
						if (message.event == 'hashChange') {
 | 
				
			||||||
		let hash = message.hash.substring(1);
 | 
							let hash = message.hash.substring(1);
 | 
				
			||||||
		if (hash.startsWith(':shared:')) {
 | 
							if (hash.startsWith(':shared:')) {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,4 +0,0 @@
 | 
				
			|||||||
{
 | 
					 | 
				
			||||||
  "type": "tildefriends-app",
 | 
					 | 
				
			||||||
  "emoji": "📚"
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							@@ -1,16 +0,0 @@
 | 
				
			|||||||
# Tilde Friends Developer's Guide
 | 
					 | 
				
			||||||
[Back to index](#index)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
A Tilde Friends application runs on the server.  To make an interesting
 | 
					 | 
				
			||||||
application that interacts with the client, it's necessary to understand
 | 
					 | 
				
			||||||
how the parts work together.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
## Hello, world!
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
A simple starting point.  Presents `Hello, world!` in the browser when
 | 
					 | 
				
			||||||
visited.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
**app.js**:
 | 
					 | 
				
			||||||
```
 | 
					 | 
				
			||||||
app.setDocument('<h1>Hello, world!</h1>');
 | 
					 | 
				
			||||||
```
 | 
					 | 
				
			||||||
@@ -1,12 +0,0 @@
 | 
				
			|||||||
# Tilde Friends Documentation
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
Tilde Friends is a participating member of a greater social
 | 
					 | 
				
			||||||
network, [Secure Scuttlebutt](https://scuttlebutt.nz/),
 | 
					 | 
				
			||||||
adding a way to safely and securely write, share,
 | 
					 | 
				
			||||||
and run code in the form of server-side web applications.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
- [Tilde Friends Vision](#vision)
 | 
					 | 
				
			||||||
- [Secure Scuttlebutt from Scratch](#ssb)
 | 
					 | 
				
			||||||
- [Structure](#structure)
 | 
					 | 
				
			||||||
- [Guide](#guide)
 | 
					 | 
				
			||||||
- [TODO](#todo)
 | 
					 | 
				
			||||||
@@ -1,41 +0,0 @@
 | 
				
			|||||||
# Secure Scuttlebutt from Scratch
 | 
					 | 
				
			||||||
[Back to index](#index)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
This aims to be the missing reference for those who wish to create a Secure
 | 
					 | 
				
			||||||
Scuttlebutt client from scratch.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
## Discovery
 | 
					 | 
				
			||||||
A good way to get started is to participate in local network discovery with a known working
 | 
					 | 
				
			||||||
client on the same network.  The
 | 
					 | 
				
			||||||
[Scuttlebutt Programming Guide](https://ssbc.github.io/scuttlebutt-protocol-guide/#local-network)
 | 
					 | 
				
			||||||
is a good start, here, with a few things to note:
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
1. Some clients advertise multiple addresses separated by semicolons (`;`).
 | 
					 | 
				
			||||||
2. Some clients advertise alternative protocols than `shs` and use hostnames instead of
 | 
					 | 
				
			||||||
IPv4 addresses.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
So be prepared to accept variations.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
There also an undocumented "new" style of discovery message.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
## Secret Handshake, Box Stream, and RPC Protocol
 | 
					 | 
				
			||||||
Now that two clients are aware of eachother, they need to complete a secret handshake.
 | 
					 | 
				
			||||||
The [programming guide](https://ssbc.github.io/scuttlebutt-protocol-guide/#handshake)
 | 
					 | 
				
			||||||
is once again a good reference.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
The box stream and RPC protocol can both be implemented from the
 | 
					 | 
				
			||||||
[same documentation](https://ssbc.github.io/scuttlebutt-protocol-guide/#box-stream)
 | 
					 | 
				
			||||||
without surprises.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
## Synchronizing Data
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
... `ebt.replicate` or `createHistoryStream` ...
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
## Rooms
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
TODO
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
## References
 | 
					 | 
				
			||||||
* [https://ssbc.github.io/scuttlebutt-protocol-guide/](https://ssbc.github.io/scuttlebutt-protocol-guide/)
 | 
					 | 
				
			||||||
* [https://dev.planetary.social/](https://dev.planetary.social/)
 | 
					 | 
				
			||||||
* [https://dev.scuttlebutt.nz/#/golang/?id=muxrpc-endpoints](https://dev.scuttlebutt.nz/#/golang/?id=muxrpc-endpoints)
 | 
					 | 
				
			||||||
@@ -1,65 +0,0 @@
 | 
				
			|||||||
# Tilde Friends Structure
 | 
					 | 
				
			||||||
[Back to index](#index)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
Tilde Friends is a mostly-self-contained executable written in C.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
In combines the following key components:
 | 
					 | 
				
			||||||
- A Secure Scuttlebutt (SSB) client/server.  This talks with other SSB
 | 
					 | 
				
			||||||
  instances, storing messages and blobs for anyone visible to local
 | 
					 | 
				
			||||||
  users as they are encountered and sharing anything published locally
 | 
					 | 
				
			||||||
  as appropriate.
 | 
					 | 
				
			||||||
- An sqlite database.  This is where the SSB instance stores its data.
 | 
					 | 
				
			||||||
  The general schema involves a `messages` table, storing mostly JSON,
 | 
					 | 
				
			||||||
  a `blobs` table storing arbitrary blob data, and a `properties` table,
 | 
					 | 
				
			||||||
  storing arbitrary state gleaned from `messages` and `blobs`, generally
 | 
					 | 
				
			||||||
  updated on demand and incrementally.
 | 
					 | 
				
			||||||
- A QuickJS runtime.  The core process runs stock scripts and has access
 | 
					 | 
				
			||||||
  and permission to use all resources.  All other processes, which
 | 
					 | 
				
			||||||
  includes everything which runs untrusted code created by Tilde Friends
 | 
					 | 
				
			||||||
  users, are strictly sandboxed in ways similar to how web browsers run
 | 
					 | 
				
			||||||
  untrusted code.  All attempts to access potentially sensitive resources
 | 
					 | 
				
			||||||
  are mediated through the core process.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
When run with no arguments, it starts a web server on
 | 
					 | 
				
			||||||
[http://localhost:12345/](http://localhost:12345/) and an SSB node.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
## Web Interface
 | 
					 | 
				
			||||||
The Tilde Friends web server provides access to Tilde Friends applications,
 | 
					 | 
				
			||||||
which are arbitrary user-defined web applications.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
At the top left, in addition to some basic navigation links, is an `edit`
 | 
					 | 
				
			||||||
link.  Anyone can view, modify, and run in-place the code to any Tilde
 | 
					 | 
				
			||||||
Friends application by using the in-browser editor.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
At the top right, one can `login` (to save work in their own space)
 | 
					 | 
				
			||||||
or `logout` (proceeding as a guest).
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
The rest of the page is an iframe belonging to the application.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
## Special Paths
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
- `/~user/app/` - Tilde Friends application paths take the form `/~user/app/`, where `user`
 | 
					 | 
				
			||||||
is a username of a Tilde Friends account, and `app` is an arbitrary name
 | 
					 | 
				
			||||||
of an application saved by the given user.
 | 
					 | 
				
			||||||
- `/~user/app/file` - A raw file in an app.
 | 
					 | 
				
			||||||
- `/&blobid.ed25519` - A raw blob.  Content-Type is inferred for at least
 | 
					 | 
				
			||||||
	a few common image types.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
## Communication Channels
 | 
					 | 
				
			||||||
Web Browser <-> Core <-> Sandbox
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
Visiting an application path delivers stock HTML and JavaScript which
 | 
					 | 
				
			||||||
establishes a WebSocket connection back to the server.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
At this point, a new sandbox process is started in Tilde Friends, much
 | 
					 | 
				
			||||||
as a new sandboxed process might be started for a new tab in a web
 | 
					 | 
				
			||||||
browser.  This process has a custom RPC connection to the core process
 | 
					 | 
				
			||||||
which holds the WebSocket connection to the browser.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
The custom RPC communication between the sandbox process and the core
 | 
					 | 
				
			||||||
process facilitates passing and calling functions remotely.  Calling a
 | 
					 | 
				
			||||||
function in another process returns a `Promise`.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
An application will typically call `app.setDocument()` at startup to
 | 
					 | 
				
			||||||
populate the app's iframe in the web browser with its own client web
 | 
					 | 
				
			||||||
application resources.
 | 
					 | 
				
			||||||
@@ -1,63 +0,0 @@
 | 
				
			|||||||
# Tilde Friends TODO
 | 
					 | 
				
			||||||
[Back to index](#index)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
## MVP3
 | 
					 | 
				
			||||||
- Sync status (problem feeds, messages/seconds stats, ...)
 | 
					 | 
				
			||||||
- app: wiki
 | 
					 | 
				
			||||||
- app: public blog
 | 
					 | 
				
			||||||
- Content-Disposition: download
 | 
					 | 
				
			||||||
- remove SSB credentials
 | 
					 | 
				
			||||||
- export SSB credentials
 | 
					 | 
				
			||||||
- initial: better empty news screen
 | 
					 | 
				
			||||||
- initial: remembered wrong user across login/logout
 | 
					 | 
				
			||||||
- initial: bad experience when following nobody
 | 
					 | 
				
			||||||
- make a cool independent app
 | 
					 | 
				
			||||||
- indicate when workspace differs from installed
 | 
					 | 
				
			||||||
- / => Something good.
 | 
					 | 
				
			||||||
- update docs
 | 
					 | 
				
			||||||
- audit + document API exposed to apps
 | 
					 | 
				
			||||||
- sqlStream => sqlExec or something
 | 
					 | 
				
			||||||
- fix weird HTTP warnings
 | 
					 | 
				
			||||||
- ssb from child process?
 | 
					 | 
				
			||||||
- channels
 | 
					 | 
				
			||||||
- placeholder/missing images
 | 
					 | 
				
			||||||
- no denial of service
 | 
					 | 
				
			||||||
- package standalone executable
 | 
					 | 
				
			||||||
- editor without app iframe
 | 
					 | 
				
			||||||
- sequence_before_author -> flags
 | 
					 | 
				
			||||||
- linkify ssb: links
 | 
					 | 
				
			||||||
- perfect rooms support
 | 
					 | 
				
			||||||
- connections 2.0
 | 
					 | 
				
			||||||
- make a better connections API
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
## Maybe Done
 | 
					 | 
				
			||||||
- blob_wants 2.0
 | 
					 | 
				
			||||||
- image downsample
 | 
					 | 
				
			||||||
- app: todo
 | 
					 | 
				
			||||||
- app: build archive
 | 
					 | 
				
			||||||
- update README
 | 
					 | 
				
			||||||
- administrators config
 | 
					 | 
				
			||||||
- apps name characters
 | 
					 | 
				
			||||||
- initial: can't switch to account when there is only one
 | 
					 | 
				
			||||||
- get tarball under 5MB
 | 
					 | 
				
			||||||
- rooms
 | 
					 | 
				
			||||||
- initial: doesn't refresh when create identity
 | 
					 | 
				
			||||||
- tf account timeout why
 | 
					 | 
				
			||||||
- ssb don't overflow boxes
 | 
					 | 
				
			||||||
- jwt for session tokens
 | 
					 | 
				
			||||||
- linkify https://...
 | 
					 | 
				
			||||||
- emoji reaction picker
 | 
					 | 
				
			||||||
- expose loads of stats
 | 
					 | 
				
			||||||
- confirm posting all new messages
 | 
					 | 
				
			||||||
- multiple identities per user, in database
 | 
					 | 
				
			||||||
- auto-populate data on initial launch
 | 
					 | 
				
			||||||
- make the docker image good / test it / use it
 | 
					 | 
				
			||||||
- leaking imports / exports
 | 
					 | 
				
			||||||
- file upload widget
 | 
					 | 
				
			||||||
- keep working on good error feedback
 | 
					 | 
				
			||||||
- build for windows
 | 
					 | 
				
			||||||
- installable apps (bring back an app message?)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
## Done
 | 
					 | 
				
			||||||
- update LICENSE
 | 
					 | 
				
			||||||
- logging to browser
 | 
					 | 
				
			||||||
@@ -1,62 +0,0 @@
 | 
				
			|||||||
# Tilde Friends Vision
 | 
					 | 
				
			||||||
[Back to index](#index)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
Tilde Friends is a tool for making and sharing.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
It is both a peer-to-peer social network client, participating in Secure
 | 
					 | 
				
			||||||
Scuttlebutt, and an environment for creating and running web applications.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
## Why
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
This is a thing that I wanted to exist and wanted to work on.  No other reason.
 | 
					 | 
				
			||||||
There is not a business model.  I believe it is interesting and unique.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
## 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.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
## Ways to Use Tilde Friends
 | 
					 | 
				
			||||||
1. **Social Network User**: This is a social network first.  You are just here,
 | 
					 | 
				
			||||||
   because your friends are.  Or you like how we limit your message length or
 | 
					 | 
				
			||||||
   short videos or whatever the trend is.  If you are ambitious, you click links
 | 
					 | 
				
			||||||
   and see interactive experiences (apps) that you wouldn't see elsewhere.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
2. **Web Visitor**: You get links from a friend to meeting invites, polls, games,
 | 
					 | 
				
			||||||
   lists, wiki pages, ..., and you interact with them as though they were
 | 
					 | 
				
			||||||
   cloud-hosted by a megacorporation.  They just work, and you don't think twice.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
3. **Group leader**: You host or use a small public instance, installing apps for
 | 
					 | 
				
			||||||
   a group of friends to use as web visitors.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
4. **Developer**: You like to write code and make or improve apps for fun or to
 | 
					 | 
				
			||||||
   solve problems.  When you encounter a Tilde Friends app on a strange server,
 | 
					 | 
				
			||||||
   you know you can trivially modify it or download it to your own instance.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
## Future Goals / Endgame
 | 
					 | 
				
			||||||
1. Mobile apps.  This can run on your old phone.  Maybe you won't be hosting
 | 
					 | 
				
			||||||
   the web interface publicly, but you can sync, install and edit apps, and
 | 
					 | 
				
			||||||
   otherwise get the full experience from a tiny touch screen.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
2. The universal application runtime.  The web browser is the universal
 | 
					 | 
				
			||||||
   platform, but even for the simplest application that you might want to host
 | 
					 | 
				
			||||||
   for your friends, cloud hosting, containers, and complicated dependencies might
 | 
					 | 
				
			||||||
   all enter the mix.  Tilde Friends, though it is yet another thing to host,
 | 
					 | 
				
			||||||
   includes everything you need out of the box to run a vast variety of interesting
 | 
					 | 
				
			||||||
   apps.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
   Tilde Friends will be built out, gradually providing safe access to host
 | 
					 | 
				
			||||||
   resources and client resources the same way web browsers extended access to
 | 
					 | 
				
			||||||
   resources like GPU, persistent storage, cameras, ... over the years.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
   Not much effort has been put forward yet to having a robust, long-lasting API,
 | 
					 | 
				
			||||||
   but since the client side longevity is already handled by web browsers, it
 | 
					 | 
				
			||||||
   seems possible that the server-side API can be managed in a similar way.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
3. An awesome development environment.  Right now it runs JavaScript from the
 | 
					 | 
				
			||||||
   first embeddable text editor I could poorly configure enough to edit code,
 | 
					 | 
				
			||||||
   but it could incorporate a debugger, source control integration a la ssb-git,
 | 
					 | 
				
			||||||
   merge tools, and transpiling from all sorts of different languages.
 | 
					 | 
				
			||||||
@@ -1,4 +1,4 @@
 | 
				
			|||||||
{
 | 
					{
 | 
				
			||||||
  "type": "tildefriends-app",
 | 
						"type": "tildefriends-app",
 | 
				
			||||||
  "emoji": "➡️"
 | 
						"emoji": "➡️"
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@@ -1,107 +1,233 @@
 | 
				
			|||||||
"use strict";
 | 
					let g_about_cache = {};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
var g_following_cache = {};
 | 
					async function query(sql, args) {
 | 
				
			||||||
var g_following_deep_cache = {};
 | 
						let result = [];
 | 
				
			||||||
var g_about_cache = {};
 | 
						await ssb.sqlAsync(sql, args, function (row) {
 | 
				
			||||||
 | 
							result.push(row);
 | 
				
			||||||
async function following(db, id) {
 | 
						});
 | 
				
			||||||
	if (g_following_cache[id]) {
 | 
						return result;
 | 
				
			||||||
		return g_following_cache[id];
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	var o = await db.get(id + ":following");
 | 
					 | 
				
			||||||
	const k_version = 5;
 | 
					 | 
				
			||||||
	var f = o ? JSON.parse(o) : o;
 | 
					 | 
				
			||||||
	if (!f || f.version != k_version) {
 | 
					 | 
				
			||||||
		f = {users: [], sequence: 0, version: k_version};
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	f.users = new Set(f.users);
 | 
					 | 
				
			||||||
	await ssb.sqlAsync(
 | 
					 | 
				
			||||||
		"SELECT "+
 | 
					 | 
				
			||||||
		"  sequence, "+
 | 
					 | 
				
			||||||
		"  json_extract(content, '$.contact') AS contact, "+
 | 
					 | 
				
			||||||
		"  json_extract(content, '$.following') AS following "+
 | 
					 | 
				
			||||||
		"FROM messages "+
 | 
					 | 
				
			||||||
		"WHERE "+
 | 
					 | 
				
			||||||
		"  author = ?1 AND "+
 | 
					 | 
				
			||||||
		"  sequence > ?2 AND "+
 | 
					 | 
				
			||||||
		"  json_extract(content, '$.type') = 'contact' "+
 | 
					 | 
				
			||||||
		"UNION SELECT MAX(sequence) AS sequence, NULL, NULL FROM messages WHERE author = ?1 "+
 | 
					 | 
				
			||||||
		"ORDER BY sequence",
 | 
					 | 
				
			||||||
		[id, f.sequence],
 | 
					 | 
				
			||||||
		function(row) {
 | 
					 | 
				
			||||||
			if (row.following) {
 | 
					 | 
				
			||||||
				f.users.add(row.contact);
 | 
					 | 
				
			||||||
			} else {
 | 
					 | 
				
			||||||
				f.users.delete(row.contact);
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
			f.sequence = row.sequence;
 | 
					 | 
				
			||||||
		});
 | 
					 | 
				
			||||||
	var as_set = f.users;
 | 
					 | 
				
			||||||
	f.users = Array.from(f.users).sort();
 | 
					 | 
				
			||||||
	var j = JSON.stringify(f);
 | 
					 | 
				
			||||||
	if (o != j) {
 | 
					 | 
				
			||||||
		await db.set(id + ":following", j);
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	f.users = as_set;
 | 
					 | 
				
			||||||
	g_following_cache[id] = f.users;
 | 
					 | 
				
			||||||
	return f.users;
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
async function followingDeep(db, seed_ids, depth) {
 | 
					async function contacts_internal(id, last_row_id, following, max_row_id) {
 | 
				
			||||||
	if (depth <= 0) {
 | 
						let result = Object.assign({}, following[id] || {});
 | 
				
			||||||
		return seed_ids;
 | 
						result.following = result.following || {};
 | 
				
			||||||
 | 
						result.blocking = result.blocking || {};
 | 
				
			||||||
 | 
						let contacts = await query(
 | 
				
			||||||
 | 
							`
 | 
				
			||||||
 | 
									SELECT content FROM messages
 | 
				
			||||||
 | 
									WHERE author = ? AND
 | 
				
			||||||
 | 
									rowid > ? AND
 | 
				
			||||||
 | 
									rowid <= ? AND
 | 
				
			||||||
 | 
									json_extract(content, '$.type') = 'contact'
 | 
				
			||||||
 | 
									ORDER BY sequence
 | 
				
			||||||
 | 
								`,
 | 
				
			||||||
 | 
							[id, last_row_id, max_row_id]
 | 
				
			||||||
 | 
						);
 | 
				
			||||||
 | 
						for (let row of contacts) {
 | 
				
			||||||
 | 
							let contact = JSON.parse(row.content);
 | 
				
			||||||
 | 
							if (contact.following === true) {
 | 
				
			||||||
 | 
								result.following[contact.contact] = true;
 | 
				
			||||||
 | 
							} else if (contact.following === false) {
 | 
				
			||||||
 | 
								delete result.following[contact.contact];
 | 
				
			||||||
 | 
							} else if (contact.blocking === true) {
 | 
				
			||||||
 | 
								result.blocking[contact.contact] = true;
 | 
				
			||||||
 | 
							} else if (contact.blocking === false) {
 | 
				
			||||||
 | 
								delete result.blocking[contact.contact];
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	var key = JSON.stringify([seed_ids, depth]);
 | 
						following[id] = result;
 | 
				
			||||||
	if (g_following_deep_cache[key]) {
 | 
						return result;
 | 
				
			||||||
		return g_following_deep_cache[key];
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					async function contact(id, last_row_id, following, max_row_id) {
 | 
				
			||||||
 | 
						return await contacts_internal(id, last_row_id, following, max_row_id);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					async function following_deep_internal(
 | 
				
			||||||
 | 
						ids,
 | 
				
			||||||
 | 
						depth,
 | 
				
			||||||
 | 
						blocking,
 | 
				
			||||||
 | 
						last_row_id,
 | 
				
			||||||
 | 
						following,
 | 
				
			||||||
 | 
						max_row_id
 | 
				
			||||||
 | 
					) {
 | 
				
			||||||
 | 
						let contacts = await Promise.all(
 | 
				
			||||||
 | 
							[...new Set(ids)].map((x) => contact(x, last_row_id, following, max_row_id))
 | 
				
			||||||
 | 
						);
 | 
				
			||||||
 | 
						let result = {};
 | 
				
			||||||
 | 
						for (let i = 0; i < ids.length; i++) {
 | 
				
			||||||
 | 
							let id = ids[i];
 | 
				
			||||||
 | 
							let contact = contacts[i];
 | 
				
			||||||
 | 
							let all_blocking = Object.assign({}, contact.blocking, blocking);
 | 
				
			||||||
 | 
							let found = Object.keys(contact.following).filter((y) => !all_blocking[y]);
 | 
				
			||||||
 | 
							let deeper =
 | 
				
			||||||
 | 
								depth > 1
 | 
				
			||||||
 | 
									? await following_deep_internal(
 | 
				
			||||||
 | 
											found,
 | 
				
			||||||
 | 
											depth - 1,
 | 
				
			||||||
 | 
											all_blocking,
 | 
				
			||||||
 | 
											last_row_id,
 | 
				
			||||||
 | 
											following,
 | 
				
			||||||
 | 
											max_row_id
 | 
				
			||||||
 | 
										)
 | 
				
			||||||
 | 
									: [];
 | 
				
			||||||
 | 
							result[id] = [id, ...found, ...deeper];
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	var f = await Promise.all(seed_ids.map(x => following(db, x).then(x => [...x])));
 | 
						return [...new Set(Object.values(result).flat())];
 | 
				
			||||||
	var ids = [].concat(...f);
 | 
					}
 | 
				
			||||||
	var x = await followingDeep(db, [...new Set(ids)].sort(), depth - 1);
 | 
					
 | 
				
			||||||
	x = [...new Set([].concat(...x, ...seed_ids))].sort();
 | 
					async function following_deep(ids, depth, blocking) {
 | 
				
			||||||
	g_following_deep_cache[key] = x;
 | 
						let db = await database('cache');
 | 
				
			||||||
	return x;
 | 
						const k_cache_version = 5;
 | 
				
			||||||
 | 
						let cache = await db.get('following');
 | 
				
			||||||
 | 
						cache = cache ? JSON.parse(cache) : {};
 | 
				
			||||||
 | 
						if (cache.version !== k_cache_version) {
 | 
				
			||||||
 | 
							cache = {
 | 
				
			||||||
 | 
								version: k_cache_version,
 | 
				
			||||||
 | 
								following: {},
 | 
				
			||||||
 | 
								last_row_id: 0,
 | 
				
			||||||
 | 
							};
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						let max_row_id = (
 | 
				
			||||||
 | 
							await query(
 | 
				
			||||||
 | 
								`
 | 
				
			||||||
 | 
								SELECT MAX(rowid) AS max_row_id FROM messages
 | 
				
			||||||
 | 
							`,
 | 
				
			||||||
 | 
								[]
 | 
				
			||||||
 | 
							)
 | 
				
			||||||
 | 
						)[0].max_row_id;
 | 
				
			||||||
 | 
						let result = await following_deep_internal(
 | 
				
			||||||
 | 
							ids,
 | 
				
			||||||
 | 
							depth,
 | 
				
			||||||
 | 
							blocking,
 | 
				
			||||||
 | 
							cache.last_row_id,
 | 
				
			||||||
 | 
							cache.following,
 | 
				
			||||||
 | 
							max_row_id
 | 
				
			||||||
 | 
						);
 | 
				
			||||||
 | 
						cache.last_row_id = max_row_id;
 | 
				
			||||||
 | 
						let store = JSON.stringify(cache);
 | 
				
			||||||
 | 
						await db.set('following', store);
 | 
				
			||||||
 | 
						return result;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					async function fetch_about(db, ids, users) {
 | 
				
			||||||
 | 
						const k_cache_version = 1;
 | 
				
			||||||
 | 
						let cache = await db.get('about');
 | 
				
			||||||
 | 
						cache = cache ? JSON.parse(cache) : {};
 | 
				
			||||||
 | 
						if (cache.version !== k_cache_version) {
 | 
				
			||||||
 | 
							cache = {
 | 
				
			||||||
 | 
								version: k_cache_version,
 | 
				
			||||||
 | 
								about: {},
 | 
				
			||||||
 | 
								last_row_id: 0,
 | 
				
			||||||
 | 
							};
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						let max_row_id = 0;
 | 
				
			||||||
 | 
						await ssb.sqlAsync(
 | 
				
			||||||
 | 
							`
 | 
				
			||||||
 | 
								SELECT MAX(rowid) AS max_row_id FROM messages
 | 
				
			||||||
 | 
							`,
 | 
				
			||||||
 | 
							[],
 | 
				
			||||||
 | 
							function (row) {
 | 
				
			||||||
 | 
								max_row_id = row.max_row_id;
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						);
 | 
				
			||||||
 | 
						for (let id of Object.keys(cache.about)) {
 | 
				
			||||||
 | 
							if (ids.indexOf(id) == -1) {
 | 
				
			||||||
 | 
								delete cache.about[id];
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						let abouts = [];
 | 
				
			||||||
 | 
						await ssb.sqlAsync(
 | 
				
			||||||
 | 
							`
 | 
				
			||||||
 | 
									SELECT
 | 
				
			||||||
 | 
										messages.*
 | 
				
			||||||
 | 
									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.*
 | 
				
			||||||
 | 
									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,
 | 
				
			||||||
 | 
							]
 | 
				
			||||||
 | 
						);
 | 
				
			||||||
 | 
						for (let about of abouts) {
 | 
				
			||||||
 | 
							let content = JSON.parse(about.content);
 | 
				
			||||||
 | 
							if (content.about === about.author) {
 | 
				
			||||||
 | 
								delete content.type;
 | 
				
			||||||
 | 
								delete content.about;
 | 
				
			||||||
 | 
								cache.about[about.author] = Object.assign(
 | 
				
			||||||
 | 
									cache.about[about.author] || {},
 | 
				
			||||||
 | 
									content
 | 
				
			||||||
 | 
								);
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						cache.last_row_id = max_row_id;
 | 
				
			||||||
 | 
						await db.set('about', JSON.stringify(cache));
 | 
				
			||||||
 | 
						users = users || {};
 | 
				
			||||||
 | 
						for (let id of Object.keys(cache.about)) {
 | 
				
			||||||
 | 
							users[id] = Object.assign(users[id] || {}, cache.about[id]);
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return Object.assign({}, users);
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
async function getAbout(db, id) {
 | 
					async function getAbout(db, id) {
 | 
				
			||||||
	if (g_about_cache[id]) {
 | 
						if (g_about_cache[id]) {
 | 
				
			||||||
		return g_about_cache[id];
 | 
							return g_about_cache[id];
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	var o = await db.get(id + ":about");
 | 
						let o = await db.get(id + ':about');
 | 
				
			||||||
	const k_version = 4;
 | 
						const k_version = 4;
 | 
				
			||||||
	var f = o ? JSON.parse(o) : o;
 | 
						let f = o ? JSON.parse(o) : o;
 | 
				
			||||||
	if (!f || f.version != k_version) {
 | 
						if (!f || f.version != k_version) {
 | 
				
			||||||
		f = {about: {}, sequence: 0, version: k_version};
 | 
							f = {about: {}, sequence: 0, version: k_version};
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	await ssb.sqlAsync(
 | 
						await ssb.sqlAsync(
 | 
				
			||||||
		"SELECT "+
 | 
							'SELECT ' +
 | 
				
			||||||
		"  sequence, "+
 | 
								'  sequence, ' +
 | 
				
			||||||
		"  content "+
 | 
								'  content ' +
 | 
				
			||||||
		"FROM messages "+
 | 
								'FROM messages ' +
 | 
				
			||||||
		"WHERE "+
 | 
								'WHERE ' +
 | 
				
			||||||
		"  author = ?1 AND "+
 | 
								'  author = ?1 AND ' +
 | 
				
			||||||
		"  sequence > ?2 AND "+
 | 
								'  sequence > ?2 AND ' +
 | 
				
			||||||
		"  json_extract(content, '$.type') = 'about' AND "+
 | 
								"  json_extract(content, '$.type') = 'about' AND " +
 | 
				
			||||||
		"  json_extract(content, '$.about') = ?1 "+
 | 
								"  json_extract(content, '$.about') = ?1 " +
 | 
				
			||||||
		"UNION SELECT MAX(sequence) as sequence, NULL FROM messages WHERE author = ?1 "+
 | 
								'UNION SELECT MAX(sequence) as sequence, NULL FROM messages WHERE author = ?1 ' +
 | 
				
			||||||
		"ORDER BY sequence",
 | 
								'ORDER BY sequence',
 | 
				
			||||||
		[id, f.sequence],
 | 
							[id, f.sequence],
 | 
				
			||||||
		function(row) {
 | 
							function (row) {
 | 
				
			||||||
			f.sequence = row.sequence;
 | 
								f.sequence = row.sequence;
 | 
				
			||||||
			if (row.content) {
 | 
								if (row.content) {
 | 
				
			||||||
				var about = {};
 | 
									let about = {};
 | 
				
			||||||
				try {
 | 
									try {
 | 
				
			||||||
					about = JSON.parse(row.content);
 | 
										about = JSON.parse(row.content);
 | 
				
			||||||
				} catch {
 | 
									} catch {}
 | 
				
			||||||
				}
 | 
					 | 
				
			||||||
				delete about.about;
 | 
									delete about.about;
 | 
				
			||||||
				delete about.type;
 | 
									delete about.type;
 | 
				
			||||||
				f.about = Object.assign(f.about, about);
 | 
									f.about = Object.assign(f.about, about);
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
		});
 | 
							}
 | 
				
			||||||
	var j = JSON.stringify(f);
 | 
						);
 | 
				
			||||||
 | 
						let j = JSON.stringify(f);
 | 
				
			||||||
	if (o != j) {
 | 
						if (o != j) {
 | 
				
			||||||
		await db.set(id + ":about", j);
 | 
							await db.set(id + ':about', j);
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	g_about_cache[id] = f.about;
 | 
						g_about_cache[id] = f.about;
 | 
				
			||||||
	return f.about;
 | 
						return f.about;
 | 
				
			||||||
@@ -110,14 +236,34 @@ async function getAbout(db, id) {
 | 
				
			|||||||
async function getSize(db, id) {
 | 
					async function getSize(db, id) {
 | 
				
			||||||
	let size = 0;
 | 
						let size = 0;
 | 
				
			||||||
	await ssb.sqlAsync(
 | 
						await ssb.sqlAsync(
 | 
				
			||||||
		"SELECT (SUM(LENGTH(content)) + SUM(LENGTH(author)) + SUM(LENGTH(id))) AS size FROM messages WHERE author = ?1",
 | 
							'SELECT (LENGTH(author) * COUNT(*) + SUM(LENGTH(content)) + SUM(LENGTH(id))) AS size FROM messages WHERE author = ?1',
 | 
				
			||||||
		[id],
 | 
							[id],
 | 
				
			||||||
		function (row) {
 | 
							function (row) {
 | 
				
			||||||
			size += row.size;
 | 
								size += row.size;
 | 
				
			||||||
		});
 | 
							}
 | 
				
			||||||
 | 
						);
 | 
				
			||||||
	return size;
 | 
						return size;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					async function getSizes(ids) {
 | 
				
			||||||
 | 
						let sizes = {};
 | 
				
			||||||
 | 
						await ssb.sqlAsync(
 | 
				
			||||||
 | 
							`
 | 
				
			||||||
 | 
								SELECT
 | 
				
			||||||
 | 
									author,
 | 
				
			||||||
 | 
									(SUM(LENGTH(content)) + SUM(LENGTH(author)) + SUM(LENGTH(messages.id))) AS size
 | 
				
			||||||
 | 
								FROM messages
 | 
				
			||||||
 | 
								JOIN json_each(?) AS ids ON author = ids.value
 | 
				
			||||||
 | 
								GROUP BY author
 | 
				
			||||||
 | 
							`,
 | 
				
			||||||
 | 
							[JSON.stringify(ids)],
 | 
				
			||||||
 | 
							function (row) {
 | 
				
			||||||
 | 
								sizes[row.author] = row.size;
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						);
 | 
				
			||||||
 | 
						return sizes;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function niceSize(bytes) {
 | 
					function niceSize(bytes) {
 | 
				
			||||||
	let value = bytes;
 | 
						let value = bytes;
 | 
				
			||||||
	let unit = 'B';
 | 
						let unit = 'B';
 | 
				
			||||||
@@ -133,27 +279,39 @@ function niceSize(bytes) {
 | 
				
			|||||||
	return Math.round(value * 10) / 10 + ' ' + unit;
 | 
						return Math.round(value * 10) / 10 + ' ' + unit;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
async function buildTree(db, root, indent, depth) {
 | 
					function escape(value) {
 | 
				
			||||||
	var f = await following(db, root);
 | 
						return value
 | 
				
			||||||
	var result = indent + '[' + f.size + '] ' + '<a target="_top" href="../index/#' + root + '">' + ((await getAbout(db, root)).name || root) + '</a> ' + niceSize(await getSize(db, root)) + '\n';
 | 
							.replaceAll('&', '&')
 | 
				
			||||||
	if (depth > 0) {
 | 
							.replaceAll('<', '<')
 | 
				
			||||||
		for (let next of f) {
 | 
							.replaceAll('>', '>');
 | 
				
			||||||
			result += await buildTree(db, next, indent + '  ', depth - 1);
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	return result;
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
async function main() {
 | 
					async function main() {
 | 
				
			||||||
	await app.setDocument('<pre style="color: #fff">building...</pre>');
 | 
						await app.setDocument('<pre style="color: #fff">building...</pre>');
 | 
				
			||||||
	var db = await database('ssb');
 | 
						let db = await database('ssb');
 | 
				
			||||||
	var whoami = await ssb.getIdentities();
 | 
						let whoami = await ssb.getIdentities();
 | 
				
			||||||
	var tree = '';
 | 
						let tree = '';
 | 
				
			||||||
	for (let id of whoami) {
 | 
						await app.setDocument(
 | 
				
			||||||
		await app.setDocument(`<pre style="color: #fff">building... ${id}</pre>`);
 | 
							`<pre style="color: #fff">Enumerating followed users...</pre>`
 | 
				
			||||||
		tree += await buildTree(db, id, '', 2);
 | 
						);
 | 
				
			||||||
 | 
						let following = await following_deep(whoami, 2, {});
 | 
				
			||||||
 | 
						await app.setDocument(
 | 
				
			||||||
 | 
							`<pre style="color: #fff">Getting names and sizes...</pre>`
 | 
				
			||||||
 | 
						);
 | 
				
			||||||
 | 
						let [about, sizes] = await Promise.all([
 | 
				
			||||||
 | 
							fetch_about(db, following, {}),
 | 
				
			||||||
 | 
							getSizes(following),
 | 
				
			||||||
 | 
						]);
 | 
				
			||||||
 | 
						await app.setDocument(`<pre style="color: #fff">Finishing...</pre>`);
 | 
				
			||||||
 | 
						following.sort((a, b) => (sizes[b] ?? 0) - (sizes[a] ?? 0));
 | 
				
			||||||
 | 
						for (let id of following) {
 | 
				
			||||||
 | 
							tree += `<li><a href="/~core/ssb/#${id}">${escape(about[id]?.name ?? id)}</a> ${niceSize(sizes[id] ?? 0)}</li>\n`;
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	await app.setDocument('<pre style="color: #fff">FOLLOWING:\n' + tree + '</pre>');
 | 
						await app.setDocument(
 | 
				
			||||||
 | 
							'<!DOCTYPE html>\n<html>\n<body style="color: #fff"><h1>Following</h1>\n<ul>' +
 | 
				
			||||||
 | 
								tree +
 | 
				
			||||||
 | 
								'</ul>\n</body>\n</html>'
 | 
				
			||||||
 | 
						);
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
main();
 | 
					main();
 | 
				
			||||||
							
								
								
									
										5
									
								
								apps/identity.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								apps/identity.json
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,5 @@
 | 
				
			|||||||
 | 
					{
 | 
				
			||||||
 | 
						"type": "tildefriends-app",
 | 
				
			||||||
 | 
						"emoji": "🪪",
 | 
				
			||||||
 | 
						"previous": "&de7q4A59auHP/34bXgeNH05JZoxsGr5TjwXPvehWH30=.sha256"
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										136
									
								
								apps/identity/app.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										136
									
								
								apps/identity/app.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,136 @@
 | 
				
			|||||||
 | 
					import * as tfrpc from '/tfrpc.js';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					tfrpc.register(async function get_private_key(id) {
 | 
				
			||||||
 | 
						return bip39Words(await ssb.getPrivateKey(id));
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					tfrpc.register(async function create_id(id) {
 | 
				
			||||||
 | 
						return await ssb.createIdentity();
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					tfrpc.register(async function add_id(id) {
 | 
				
			||||||
 | 
						return await ssb.addIdentity(bip39Bytes(id));
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					tfrpc.register(async function delete_id(id) {
 | 
				
			||||||
 | 
						return await ssb.deleteIdentity(id);
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					tfrpc.register(async function reload() {
 | 
				
			||||||
 | 
						await main();
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					async function main() {
 | 
				
			||||||
 | 
						let ids = await ssb.getIdentities();
 | 
				
			||||||
 | 
						await app.setDocument(
 | 
				
			||||||
 | 
							`
 | 
				
			||||||
 | 
							<head>
 | 
				
			||||||
 | 
								<link rel="stylesheet" href="w3.css"></link>
 | 
				
			||||||
 | 
								<style>
 | 
				
			||||||
 | 
									/* "2018 Sargasso Sea" */
 | 
				
			||||||
 | 
									.w3-theme-l5 {color:#000 !important; background-color:#f3f4f7 !important}
 | 
				
			||||||
 | 
									.w3-theme-l4 {color:#000 !important; background-color:#d7dbe3 !important}
 | 
				
			||||||
 | 
									.w3-theme-l3 {color:#000 !important; background-color:#b0b6c8 !important}
 | 
				
			||||||
 | 
									.w3-theme-l2 {color:#fff !important; background-color:#8892ac !important}
 | 
				
			||||||
 | 
									.w3-theme-l1 {color:#fff !important; background-color:#636f8e !important}
 | 
				
			||||||
 | 
									.w3-theme-d1 {color:#fff !important; background-color:#40485c !important}
 | 
				
			||||||
 | 
									.w3-theme-d2 {color:#fff !important; background-color:#394052 !important}
 | 
				
			||||||
 | 
									.w3-theme-d3 {color:#fff !important; background-color:#323848 !important}
 | 
				
			||||||
 | 
									.w3-theme-d4 {color:#fff !important; background-color:#2b303d !important}
 | 
				
			||||||
 | 
									.w3-theme-d5 {color:#fff !important; background-color:#242833 !important}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
									.w3-theme-light {color:#000 !important; background-color:#f3f4f7 !important}
 | 
				
			||||||
 | 
									.w3-theme-dark {color:#fff !important; background-color:#242833 !important}
 | 
				
			||||||
 | 
									.w3-theme-action {color:#fff !important; background-color:#242833 !important}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
									.w3-theme {color:#fff !important; background-color:#485167 !important}
 | 
				
			||||||
 | 
									.w3-text-theme {color:#485167 !important}
 | 
				
			||||||
 | 
									.w3-border-theme {border-color:#485167 !important}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
									.w3-hover-theme:hover {color:#fff !important; background-color:#485167 !important}
 | 
				
			||||||
 | 
									.w3-hover-text-theme:hover {color:#485167 !important}
 | 
				
			||||||
 | 
									.w3-hover-border-theme:hover {border-color:#485167 !important}
 | 
				
			||||||
 | 
								</style>
 | 
				
			||||||
 | 
							</head>
 | 
				
			||||||
 | 
							<body class="w3-theme-l3">
 | 
				
			||||||
 | 
							<script>const handler = {};</script>
 | 
				
			||||||
 | 
							<script type="module">
 | 
				
			||||||
 | 
								import * as tfrpc from '/static/tfrpc.js';
 | 
				
			||||||
 | 
								handler.export_id = async function export_id(event) {
 | 
				
			||||||
 | 
									let id = event.srcElement.dataset.id;
 | 
				
			||||||
 | 
									let element = document.createElement('textarea');
 | 
				
			||||||
 | 
									element.value = await tfrpc.rpc.get_private_key(id);
 | 
				
			||||||
 | 
									element.style = 'width: 100%; height: auto; read-only: true; resize: none';
 | 
				
			||||||
 | 
									element.classList.add('w3-input');
 | 
				
			||||||
 | 
									element.readOnly = true;
 | 
				
			||||||
 | 
									event.srcElement.parentElement.appendChild(element);
 | 
				
			||||||
 | 
									event.srcElement.onclick = event => handler.hide_id(event, element);
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								handler.add_id = async function add_id(event) {
 | 
				
			||||||
 | 
									let id = document.getElementById('add_id').value;
 | 
				
			||||||
 | 
									try {
 | 
				
			||||||
 | 
										let new_id = await tfrpc.rpc.add_id(id);
 | 
				
			||||||
 | 
										alert('Successfully imported: ' + new_id);
 | 
				
			||||||
 | 
										await tfrpc.rpc.reload();
 | 
				
			||||||
 | 
									} catch (e) {
 | 
				
			||||||
 | 
										alert('Error importing identity: ' + e);
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								handler.create_id = async function create_id(event) {
 | 
				
			||||||
 | 
									try {
 | 
				
			||||||
 | 
										let id = await tfrpc.rpc.create_id();
 | 
				
			||||||
 | 
										alert('Successfully created: ' + id);
 | 
				
			||||||
 | 
										await tfrpc.rpc.reload();
 | 
				
			||||||
 | 
									} catch (e) {
 | 
				
			||||||
 | 
										alert('Error creating identity: ' + e.message);
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								handler.hide_id = function hide_id(event, element) {
 | 
				
			||||||
 | 
									element.parentNode.removeChild(element);
 | 
				
			||||||
 | 
									event.srcElement.onclick = handler.export_id;
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								handler.delete_id = async function delete_id(event) {
 | 
				
			||||||
 | 
									let id = event.srcElement.dataset.id;
 | 
				
			||||||
 | 
									try {
 | 
				
			||||||
 | 
										if (prompt('Are you sure you want to delete "' + id + '"?  It cannot be recovered without the exported phrase.\\n\\nEnter the word "DELETE" to confirm you wish to delete it.') === 'DELETE') {
 | 
				
			||||||
 | 
											if (await tfrpc.rpc.delete_id(id)) {
 | 
				
			||||||
 | 
												alert('Successfully deleted ID: ' + id);
 | 
				
			||||||
 | 
											}
 | 
				
			||||||
 | 
											await tfrpc.rpc.reload();
 | 
				
			||||||
 | 
										}
 | 
				
			||||||
 | 
									} catch (e) {
 | 
				
			||||||
 | 
										alert('Error deleting ID: ' + e);
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							</script>
 | 
				
			||||||
 | 
							<header class="w3-theme w3-padding"><h1>SSB Identity Management</h1></header>
 | 
				
			||||||
 | 
							<div class="w3-card-4 w3-margin">
 | 
				
			||||||
 | 
								<header class="w3-container w3-theme-l2"><h2>Create a new identity</h2></header>
 | 
				
			||||||
 | 
								<footer class="w3-padding">
 | 
				
			||||||
 | 
									<button id="create_id" onclick="handler.create_id()" class="w3-button w3-theme">Create Identity</button>
 | 
				
			||||||
 | 
								</footer>
 | 
				
			||||||
 | 
							</div>
 | 
				
			||||||
 | 
							<div class="w3-card-4 w3-margin">
 | 
				
			||||||
 | 
								<header class="w3-container w3-theme-l2"><h2>Import an SSB Identity from 12 BIP39 English Words</h2></header>
 | 
				
			||||||
 | 
								<textarea id="add_id" style="width: 100%" rows="4" class="w3-input"></textarea>
 | 
				
			||||||
 | 
								<footer class="w3-padding">
 | 
				
			||||||
 | 
									<button id="add" onclick="handler.add_id(event)" class="w3-button w3-theme">Import Identity</button>
 | 
				
			||||||
 | 
								</footer>
 | 
				
			||||||
 | 
							</div>
 | 
				
			||||||
 | 
							<div class="w3-card-4 w3-margin">
 | 
				
			||||||
 | 
								<header class="w3-container w3-theme-l2"><h2>Identities</h2></header>
 | 
				
			||||||
 | 
								<ul class="w3-ul">` +
 | 
				
			||||||
 | 
								ids
 | 
				
			||||||
 | 
									.map(
 | 
				
			||||||
 | 
										(
 | 
				
			||||||
 | 
											id
 | 
				
			||||||
 | 
										) => `<li style="overflow: hidden; text-wrap: nowrap; text-overflow: ellipsis">
 | 
				
			||||||
 | 
									<button onclick="handler.export_id(event)" data-id="${id}" class="w3-button w3-theme">Export Identity</button>
 | 
				
			||||||
 | 
									<button onclick="handler.delete_id(event)" data-id="${id}" class="w3-button w3-theme">Delete Identity</button>
 | 
				
			||||||
 | 
									${id}
 | 
				
			||||||
 | 
								</li>`
 | 
				
			||||||
 | 
									)
 | 
				
			||||||
 | 
									.join('\n') +
 | 
				
			||||||
 | 
								`	</ul>
 | 
				
			||||||
 | 
							</div>
 | 
				
			||||||
 | 
						</body>`
 | 
				
			||||||
 | 
						);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					main();
 | 
				
			||||||
							
								
								
									
										235
									
								
								apps/identity/w3.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										235
									
								
								apps/identity/w3.css
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,235 @@
 | 
				
			|||||||
 | 
					/* W3.CSS 4.15 December 2020 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-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}
 | 
				
			||||||
 | 
					/* 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-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}
 | 
				
			||||||
							
								
								
									
										5
									
								
								apps/issues.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								apps/issues.json
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,5 @@
 | 
				
			|||||||
 | 
					{
 | 
				
			||||||
 | 
						"type": "tildefriends-app",
 | 
				
			||||||
 | 
						"emoji": "🦟",
 | 
				
			||||||
 | 
						"previous": "&cUqvSDUls3jn0haD85LPFAGdkc8wFuy347TtATNcJgg=.sha256"
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										107
									
								
								apps/issues/app.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										107
									
								
								apps/issues/app.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,107 @@
 | 
				
			|||||||
 | 
					import * as tfrpc from '/tfrpc.js';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					let g_database;
 | 
				
			||||||
 | 
					let g_hash;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					tfrpc.register(async function localStorageGet(key) {
 | 
				
			||||||
 | 
						return app.localStorageGet(key);
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					tfrpc.register(async function localStorageSet(key, value) {
 | 
				
			||||||
 | 
						return app.localStorageSet(key, value);
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					tfrpc.register(async function databaseGet(key) {
 | 
				
			||||||
 | 
						return g_database ? g_database.get(key) : undefined;
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					tfrpc.register(async function databaseSet(key, value) {
 | 
				
			||||||
 | 
						return g_database ? g_database.set(key, value) : undefined;
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					tfrpc.register(async function createIdentity() {
 | 
				
			||||||
 | 
						return ssb.createIdentity();
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					tfrpc.register(async function getIdentities() {
 | 
				
			||||||
 | 
						return ssb.getIdentities();
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					tfrpc.register(async function getAllIdentities() {
 | 
				
			||||||
 | 
						return ssb.getAllIdentities();
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					tfrpc.register(async function getBroadcasts() {
 | 
				
			||||||
 | 
						return ssb.getBroadcasts();
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					tfrpc.register(async function getConnections() {
 | 
				
			||||||
 | 
						return ssb.connections();
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					tfrpc.register(async function getStoredConnections() {
 | 
				
			||||||
 | 
						return ssb.storedConnections();
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					tfrpc.register(async function forgetStoredConnection(connection) {
 | 
				
			||||||
 | 
						return ssb.forgetStoredConnection(connection);
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					tfrpc.register(async function createTunnel(portal, target) {
 | 
				
			||||||
 | 
						return ssb.createTunnel(portal, target);
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					tfrpc.register(async function connect(token) {
 | 
				
			||||||
 | 
						await ssb.connect(token);
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					tfrpc.register(async function closeConnection(id) {
 | 
				
			||||||
 | 
						await ssb.closeConnection(id);
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					tfrpc.register(async function query(sql, args) {
 | 
				
			||||||
 | 
						let result = [];
 | 
				
			||||||
 | 
						await ssb.sqlAsync(sql, args, function callback(row) {
 | 
				
			||||||
 | 
							result.push(row);
 | 
				
			||||||
 | 
						});
 | 
				
			||||||
 | 
						return result;
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					tfrpc.register(async function appendMessage(id, message) {
 | 
				
			||||||
 | 
						return ssb.appendMessageWithIdentity(id, message);
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					core.register('message', async function message_handler(message) {
 | 
				
			||||||
 | 
						if (message.event == 'hashChange') {
 | 
				
			||||||
 | 
							g_hash = message.hash;
 | 
				
			||||||
 | 
							await tfrpc.rpc.hashChanged(message.hash);
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					tfrpc.register(function getHash(id, message) {
 | 
				
			||||||
 | 
						return g_hash;
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					tfrpc.register(function setHash(hash) {
 | 
				
			||||||
 | 
						return app.setHash(hash);
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					tfrpc.register(async function store_blob(blob) {
 | 
				
			||||||
 | 
						if (Array.isArray(blob)) {
 | 
				
			||||||
 | 
							blob = Uint8Array.from(blob);
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return await ssb.blobStore(blob);
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					tfrpc.register(async function get_blob(id) {
 | 
				
			||||||
 | 
						return utf8Decode(await ssb.blobGet(id));
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					tfrpc.register(async function store_message(message) {
 | 
				
			||||||
 | 
						return await ssb.storeMessage(message);
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					tfrpc.register(function apps() {
 | 
				
			||||||
 | 
						return core.apps();
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					tfrpc.register(function getActiveIdentity() {
 | 
				
			||||||
 | 
						return ssb.getActiveIdentity();
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					tfrpc.register(async function try_decrypt(id, content) {
 | 
				
			||||||
 | 
						return await ssb.privateMessageDecrypt(id, content);
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					core.register('onMessage', async function (id) {
 | 
				
			||||||
 | 
						await tfrpc.rpc.notifyNewMessage(id);
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					core.register('onBroadcastsChanged', async function () {
 | 
				
			||||||
 | 
						await tfrpc.rpc.set('broadcasts', await ssb.getBroadcasts());
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					core.register('onConnectionsChanged', async function () {
 | 
				
			||||||
 | 
						await tfrpc.rpc.set('connections', await ssb.connections());
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					async function main() {
 | 
				
			||||||
 | 
						if (typeof database !== 'undefined') {
 | 
				
			||||||
 | 
							g_database = await database('ssb');
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						await app.setDocument(utf8Decode(await getFile('index.html')));
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					main();
 | 
				
			||||||
							
								
								
									
										1
									
								
								apps/issues/commonmark.min.js
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								apps/issues/commonmark.min.js
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
							
								
								
									
										16
									
								
								apps/issues/index.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								apps/issues/index.html
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,16 @@
 | 
				
			|||||||
 | 
					<!doctype html>
 | 
				
			||||||
 | 
					<html style="color: #fff">
 | 
				
			||||||
 | 
						<head>
 | 
				
			||||||
 | 
							<title>Tilde Friends</title>
 | 
				
			||||||
 | 
							<base target="_top" />
 | 
				
			||||||
 | 
						</head>
 | 
				
			||||||
 | 
						<body>
 | 
				
			||||||
 | 
							<tf-issues-app />
 | 
				
			||||||
 | 
							<script>
 | 
				
			||||||
 | 
								window.litDisableBundleWarning = true;
 | 
				
			||||||
 | 
							</script>
 | 
				
			||||||
 | 
							<script src="commonmark.min.js"></script>
 | 
				
			||||||
 | 
							<script src="commonmark-linkify.js" type="module"></script>
 | 
				
			||||||
 | 
							<script src="script.js" type="module"></script>
 | 
				
			||||||
 | 
						</body>
 | 
				
			||||||
 | 
					</html>
 | 
				
			||||||
							
								
								
									
										120
									
								
								apps/issues/lit-all.min.js
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										120
									
								
								apps/issues/lit-all.min.js
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
							
								
								
									
										1
									
								
								apps/issues/lit-all.min.js.map
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								apps/issues/lit-all.min.js.map
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
							
								
								
									
										248
									
								
								apps/issues/script.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										248
									
								
								apps/issues/script.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,248 @@
 | 
				
			|||||||
 | 
					import {LitElement, html, unsafeHTML} from './lit-all.min.js';
 | 
				
			||||||
 | 
					import * as tfrpc from '/static/tfrpc.js';
 | 
				
			||||||
 | 
					import * as tfutils from './tf-utils.js';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const k_project = '%Hr+4xEVtjplidSKBlRWi4Aw/0Tfw7B+1OR9BzlDKmOI=.sha256';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class TfComposeElement extends LitElement {
 | 
				
			||||||
 | 
						static get properties() {
 | 
				
			||||||
 | 
							return {
 | 
				
			||||||
 | 
								value: {type: String},
 | 
				
			||||||
 | 
							};
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						input() {
 | 
				
			||||||
 | 
							let input = this.renderRoot.getElementById('input');
 | 
				
			||||||
 | 
							let preview = this.renderRoot.getElementById('preview');
 | 
				
			||||||
 | 
							if (input && preview) {
 | 
				
			||||||
 | 
								preview.innerHTML = tfutils.markdown(input.value);
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						submit() {
 | 
				
			||||||
 | 
							this.dispatchEvent(
 | 
				
			||||||
 | 
								new CustomEvent('tf-submit', {
 | 
				
			||||||
 | 
									bubbles: true,
 | 
				
			||||||
 | 
									composed: true,
 | 
				
			||||||
 | 
									detail: {
 | 
				
			||||||
 | 
										value: this.renderRoot.getElementById('input').value,
 | 
				
			||||||
 | 
									},
 | 
				
			||||||
 | 
								})
 | 
				
			||||||
 | 
							);
 | 
				
			||||||
 | 
							this.renderRoot.getElementById('input').value = '';
 | 
				
			||||||
 | 
							this.input();
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						render() {
 | 
				
			||||||
 | 
							return html`
 | 
				
			||||||
 | 
								<div style="display: flex; flex-direction: row">
 | 
				
			||||||
 | 
									<textarea id="input" @input=${this.input} style="flex: 1 1">${this.value}</textarea>
 | 
				
			||||||
 | 
									<div id="preview" style="flex: 1 1"></div>
 | 
				
			||||||
 | 
								</div>
 | 
				
			||||||
 | 
								<input type="submit" value="Submit" @click=${this.submit}></input>
 | 
				
			||||||
 | 
							`;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					customElements.define('tf-compose', TfComposeElement);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class TfIssuesAppElement extends LitElement {
 | 
				
			||||||
 | 
						static get properties() {
 | 
				
			||||||
 | 
							return {
 | 
				
			||||||
 | 
								issues: {type: Array},
 | 
				
			||||||
 | 
								selected: {type: Object},
 | 
				
			||||||
 | 
							};
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						constructor() {
 | 
				
			||||||
 | 
							super();
 | 
				
			||||||
 | 
							this.issues = [];
 | 
				
			||||||
 | 
							this.load();
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						async load() {
 | 
				
			||||||
 | 
							let issues = {};
 | 
				
			||||||
 | 
							let messages = await tfrpc.rpc.query(
 | 
				
			||||||
 | 
								`
 | 
				
			||||||
 | 
								WITH issues AS (SELECT messages.id, json(messages.content) AS content, messages.author, messages.timestamp FROM messages_refs JOIN messages ON
 | 
				
			||||||
 | 
									messages.id = messages_refs.message
 | 
				
			||||||
 | 
									WHERE messages_refs.ref = ? AND json_extract(messages.content, '$.type') = 'issue'),
 | 
				
			||||||
 | 
								edits AS (SELECT messages.id, json(messages.content) AS content, messages.author, messages.timestamp FROM issues JOIN messages_refs ON
 | 
				
			||||||
 | 
									issues.id = messages_refs.ref JOIN messages ON
 | 
				
			||||||
 | 
									messages.id = messages_refs.message
 | 
				
			||||||
 | 
									WHERE json_extract(messages.content, '$.type') IN ('issue-edit', 'post'))
 | 
				
			||||||
 | 
								SELECT * FROM issues
 | 
				
			||||||
 | 
								UNION
 | 
				
			||||||
 | 
								SELECT * FROM edits ORDER BY timestamp
 | 
				
			||||||
 | 
							`,
 | 
				
			||||||
 | 
								[k_project]
 | 
				
			||||||
 | 
							);
 | 
				
			||||||
 | 
							for (let message of messages) {
 | 
				
			||||||
 | 
								let content = JSON.parse(message.content);
 | 
				
			||||||
 | 
								switch (content.type) {
 | 
				
			||||||
 | 
									case 'issue':
 | 
				
			||||||
 | 
										issues[message.id] = {
 | 
				
			||||||
 | 
											id: message.id,
 | 
				
			||||||
 | 
											author: message.author,
 | 
				
			||||||
 | 
											text: content.text,
 | 
				
			||||||
 | 
											updates: [],
 | 
				
			||||||
 | 
											created: message.timestamp,
 | 
				
			||||||
 | 
											open: true,
 | 
				
			||||||
 | 
										};
 | 
				
			||||||
 | 
										break;
 | 
				
			||||||
 | 
									case 'issue-edit':
 | 
				
			||||||
 | 
									case 'post':
 | 
				
			||||||
 | 
										for (let issue of content.issues || []) {
 | 
				
			||||||
 | 
											if (issues[issue.link]) {
 | 
				
			||||||
 | 
												if (issue.open !== undefined) {
 | 
				
			||||||
 | 
													issues[issue.link].open = issue.open;
 | 
				
			||||||
 | 
													message.open = issue.open;
 | 
				
			||||||
 | 
												}
 | 
				
			||||||
 | 
												issues[issue.link].updates.push(message);
 | 
				
			||||||
 | 
												issues[issue.link].updated = message.timestamp;
 | 
				
			||||||
 | 
											}
 | 
				
			||||||
 | 
										}
 | 
				
			||||||
 | 
										break;
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							this.issues = Object.values(issues).sort(
 | 
				
			||||||
 | 
								(x, y) => y.open - x.open || y.created - x.created
 | 
				
			||||||
 | 
							);
 | 
				
			||||||
 | 
							if (this.selected) {
 | 
				
			||||||
 | 
								for (let issue of this.issues) {
 | 
				
			||||||
 | 
									if (issue.id == this.selected.id) {
 | 
				
			||||||
 | 
										this.selected = issue;
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						render_issue_table_row(issue) {
 | 
				
			||||||
 | 
							return html`
 | 
				
			||||||
 | 
								<tr>
 | 
				
			||||||
 | 
									<td>${issue.open ? '☐ open' : '☑ closed'}</td>
 | 
				
			||||||
 | 
									<td
 | 
				
			||||||
 | 
										style="max-width: 8em; overflow: hidden; white-space: nowrap; text-overflow: ellipsis"
 | 
				
			||||||
 | 
									>
 | 
				
			||||||
 | 
										${issue.author}
 | 
				
			||||||
 | 
									</td>
 | 
				
			||||||
 | 
									<td
 | 
				
			||||||
 | 
										style="max-width: 40em; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; cursor: pointer"
 | 
				
			||||||
 | 
										@click=${() => (this.selected = issue)}
 | 
				
			||||||
 | 
									>
 | 
				
			||||||
 | 
										${issue.text.split('\n')?.[0]}
 | 
				
			||||||
 | 
									</td>
 | 
				
			||||||
 | 
									<td>
 | 
				
			||||||
 | 
										${new Date(issue.updated ?? issue.created).toLocaleDateString()}
 | 
				
			||||||
 | 
									</td>
 | 
				
			||||||
 | 
								</tr>
 | 
				
			||||||
 | 
							`;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						render_update(update) {
 | 
				
			||||||
 | 
							let content = JSON.parse(update.content);
 | 
				
			||||||
 | 
							let message;
 | 
				
			||||||
 | 
							if (content.text) {
 | 
				
			||||||
 | 
								message = unsafeHTML(tfutils.markdown(content.text));
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							return html`
 | 
				
			||||||
 | 
								<div style="border-left: 2px solid #fff; padding-left: 8px">
 | 
				
			||||||
 | 
									<div>${new Date(update.timestamp).toLocaleString()}</div>
 | 
				
			||||||
 | 
									<div>${update.author}</div>
 | 
				
			||||||
 | 
									<div>${message}</div>
 | 
				
			||||||
 | 
									<div>
 | 
				
			||||||
 | 
										${update.open !== undefined
 | 
				
			||||||
 | 
											? update.open
 | 
				
			||||||
 | 
												? 'issue opened'
 | 
				
			||||||
 | 
												: 'issue closed'
 | 
				
			||||||
 | 
											: undefined}
 | 
				
			||||||
 | 
									</div>
 | 
				
			||||||
 | 
								</div>
 | 
				
			||||||
 | 
							`;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						async set_open(id, open) {
 | 
				
			||||||
 | 
							if (
 | 
				
			||||||
 | 
								confirm(`Are you sure you want to ${open ? 'open' : 'close'} this issue?`)
 | 
				
			||||||
 | 
							) {
 | 
				
			||||||
 | 
								let whoami = await tfrpc.rpc.getActiveIdentity();
 | 
				
			||||||
 | 
								await tfrpc.rpc.appendMessage(whoami, {
 | 
				
			||||||
 | 
									type: 'issue-edit',
 | 
				
			||||||
 | 
									issues: [
 | 
				
			||||||
 | 
										{
 | 
				
			||||||
 | 
											link: id,
 | 
				
			||||||
 | 
											open: open,
 | 
				
			||||||
 | 
										},
 | 
				
			||||||
 | 
									],
 | 
				
			||||||
 | 
								});
 | 
				
			||||||
 | 
								await this.load();
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						async create_issue(event) {
 | 
				
			||||||
 | 
							let whoami = await tfrpc.rpc.getActiveIdentity();
 | 
				
			||||||
 | 
							await tfrpc.rpc.appendMessage(whoami, {
 | 
				
			||||||
 | 
								type: 'issue',
 | 
				
			||||||
 | 
								project: k_project,
 | 
				
			||||||
 | 
								text: event.detail.value,
 | 
				
			||||||
 | 
							});
 | 
				
			||||||
 | 
							await this.load();
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						async reply_to_issue(event) {
 | 
				
			||||||
 | 
							let whoami = await tfrpc.rpc.getActiveIdentity();
 | 
				
			||||||
 | 
							await tfrpc.rpc.appendMessage(whoami, {
 | 
				
			||||||
 | 
								type: 'post',
 | 
				
			||||||
 | 
								text: event.detail.value,
 | 
				
			||||||
 | 
								root: this.selected.id,
 | 
				
			||||||
 | 
								branch: this.selected.updates.length
 | 
				
			||||||
 | 
									? this.selected.updates[this.selected.updates.length - 1].id
 | 
				
			||||||
 | 
									: this.selected.id,
 | 
				
			||||||
 | 
								issues: [
 | 
				
			||||||
 | 
									{
 | 
				
			||||||
 | 
										link: this.selected.id,
 | 
				
			||||||
 | 
									},
 | 
				
			||||||
 | 
								],
 | 
				
			||||||
 | 
							});
 | 
				
			||||||
 | 
							await this.load();
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						render() {
 | 
				
			||||||
 | 
							let header = html` <h1>Tilde Friends Issues</h1> `;
 | 
				
			||||||
 | 
							if (this.selected) {
 | 
				
			||||||
 | 
								return html`
 | 
				
			||||||
 | 
									${header}
 | 
				
			||||||
 | 
									<div>
 | 
				
			||||||
 | 
										<input type="button" value="Back" @click=${() => (this.selected = undefined)}></input>
 | 
				
			||||||
 | 
										${
 | 
				
			||||||
 | 
											this.selected.open
 | 
				
			||||||
 | 
												? html`<input type="button" value="Close Issue" @click=${() => this.set_open(this.selected.id, false)}></input>`
 | 
				
			||||||
 | 
												: html`<input type="button" value="Reopen Issue" @click=${() => this.set_open(this.selected.id, true)}></input>`
 | 
				
			||||||
 | 
										}
 | 
				
			||||||
 | 
									</div>
 | 
				
			||||||
 | 
									<div>${new Date(this.selected.created).toLocaleString()}</div>
 | 
				
			||||||
 | 
									<div>${this.selected.author}</div>
 | 
				
			||||||
 | 
									<div>${this.selected.id}</div>
 | 
				
			||||||
 | 
									<div>${unsafeHTML(tfutils.markdown(this.selected.text))}</div>
 | 
				
			||||||
 | 
									${this.selected.updates.map((x) => this.render_update(x))}
 | 
				
			||||||
 | 
									<tf-compose @tf-submit=${this.reply_to_issue}></tf-compose>
 | 
				
			||||||
 | 
								`;
 | 
				
			||||||
 | 
							} else {
 | 
				
			||||||
 | 
								return html`
 | 
				
			||||||
 | 
									${header}
 | 
				
			||||||
 | 
									<h2>New Issue</h2>
 | 
				
			||||||
 | 
									<tf-compose @tf-submit=${this.create_issue}></tf-compose>
 | 
				
			||||||
 | 
									<table>
 | 
				
			||||||
 | 
										<tr>
 | 
				
			||||||
 | 
											<th>Status</th>
 | 
				
			||||||
 | 
											<th>Author</th>
 | 
				
			||||||
 | 
											<th>Title</th>
 | 
				
			||||||
 | 
											<th>Date</th>
 | 
				
			||||||
 | 
										</tr>
 | 
				
			||||||
 | 
										${this.issues.map((x) => this.render_issue_table_row(x))}
 | 
				
			||||||
 | 
									</table>
 | 
				
			||||||
 | 
								`;
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					customElements.define('tf-issues-app', TfIssuesAppElement);
 | 
				
			||||||
							
								
								
									
										113
									
								
								apps/issues/tf-utils.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										113
									
								
								apps/issues/tf-utils.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,113 @@
 | 
				
			|||||||
 | 
					import * as linkify from './commonmark-linkify.js';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function image(node, entering) {
 | 
				
			||||||
 | 
						if (
 | 
				
			||||||
 | 
							node.firstChild?.type === 'text' &&
 | 
				
			||||||
 | 
							node.firstChild.literal.startsWith('video:')
 | 
				
			||||||
 | 
						) {
 | 
				
			||||||
 | 
							if (entering) {
 | 
				
			||||||
 | 
								this.lit(
 | 
				
			||||||
 | 
									'<video style="max-width: 100%; max-height: 480px" title="' +
 | 
				
			||||||
 | 
										this.esc(node.firstChild?.literal) +
 | 
				
			||||||
 | 
										'" controls>'
 | 
				
			||||||
 | 
								);
 | 
				
			||||||
 | 
								this.lit('<source src="' + this.esc(node.destination) + '"></source>');
 | 
				
			||||||
 | 
								this.disableTags += 1;
 | 
				
			||||||
 | 
							} else {
 | 
				
			||||||
 | 
								this.disableTags -= 1;
 | 
				
			||||||
 | 
								this.lit('</video>');
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						} else if (
 | 
				
			||||||
 | 
							node.firstChild?.type === 'text' &&
 | 
				
			||||||
 | 
							node.firstChild.literal.startsWith('audio:')
 | 
				
			||||||
 | 
						) {
 | 
				
			||||||
 | 
							if (entering) {
 | 
				
			||||||
 | 
								this.lit(
 | 
				
			||||||
 | 
									'<audio style="height: 32px; max-width: 100%" title="' +
 | 
				
			||||||
 | 
										this.esc(node.firstChild?.literal) +
 | 
				
			||||||
 | 
										'" controls>'
 | 
				
			||||||
 | 
								);
 | 
				
			||||||
 | 
								this.lit('<source src="' + this.esc(node.destination) + '"></source>');
 | 
				
			||||||
 | 
								this.disableTags += 1;
 | 
				
			||||||
 | 
							} else {
 | 
				
			||||||
 | 
								this.disableTags -= 1;
 | 
				
			||||||
 | 
								this.lit('</audio>');
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						} else {
 | 
				
			||||||
 | 
							if (entering) {
 | 
				
			||||||
 | 
								if (this.disableTags === 0) {
 | 
				
			||||||
 | 
									this.lit(
 | 
				
			||||||
 | 
										'<div class="img_caption">' +
 | 
				
			||||||
 | 
											this.esc(node.firstChild?.literal || node.destination) +
 | 
				
			||||||
 | 
											'</div>'
 | 
				
			||||||
 | 
									);
 | 
				
			||||||
 | 
									if (this.options.safe && potentiallyUnsafe(node.destination)) {
 | 
				
			||||||
 | 
										this.lit('<img src="" alt="');
 | 
				
			||||||
 | 
									} else {
 | 
				
			||||||
 | 
										this.lit('<img src="' + this.esc(node.destination) + '" alt="');
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								this.disableTags += 1;
 | 
				
			||||||
 | 
							} else {
 | 
				
			||||||
 | 
								this.disableTags -= 1;
 | 
				
			||||||
 | 
								if (this.disableTags === 0) {
 | 
				
			||||||
 | 
									if (node.title) {
 | 
				
			||||||
 | 
										this.lit('" title="' + this.esc(node.title));
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
									this.lit('" />');
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function markdown(md) {
 | 
				
			||||||
 | 
						var reader = new commonmark.Parser({safe: true});
 | 
				
			||||||
 | 
						var writer = new commonmark.HtmlRenderer();
 | 
				
			||||||
 | 
						writer.image = image;
 | 
				
			||||||
 | 
						var parsed = reader.parse(md || '');
 | 
				
			||||||
 | 
						parsed = linkify.transform(parsed);
 | 
				
			||||||
 | 
						var walker = parsed.walker();
 | 
				
			||||||
 | 
						var event, node;
 | 
				
			||||||
 | 
						while ((event = walker.next())) {
 | 
				
			||||||
 | 
							node = event.node;
 | 
				
			||||||
 | 
							if (event.entering) {
 | 
				
			||||||
 | 
								if (node.type == 'link') {
 | 
				
			||||||
 | 
									if (
 | 
				
			||||||
 | 
										node.destination.startsWith('@') &&
 | 
				
			||||||
 | 
										node.destination.endsWith('.ed25519')
 | 
				
			||||||
 | 
									) {
 | 
				
			||||||
 | 
										node.destination = '#' + node.destination;
 | 
				
			||||||
 | 
									} else if (
 | 
				
			||||||
 | 
										node.destination.startsWith('%') &&
 | 
				
			||||||
 | 
										node.destination.endsWith('.sha256')
 | 
				
			||||||
 | 
									) {
 | 
				
			||||||
 | 
										node.destination = '#' + node.destination;
 | 
				
			||||||
 | 
									} else if (
 | 
				
			||||||
 | 
										node.destination.startsWith('&') &&
 | 
				
			||||||
 | 
										node.destination.endsWith('.sha256')
 | 
				
			||||||
 | 
									) {
 | 
				
			||||||
 | 
										node.destination = '/' + node.destination + '/view';
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
								} else if (node.type == 'image') {
 | 
				
			||||||
 | 
									if (node.destination.startsWith('&')) {
 | 
				
			||||||
 | 
										node.destination = '/' + node.destination + '/view';
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return writer.render(parsed);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function human_readable_size(bytes) {
 | 
				
			||||||
 | 
						let v = bytes;
 | 
				
			||||||
 | 
						let u = 'B';
 | 
				
			||||||
 | 
						for (let unit of ['kB', 'MB', 'GB']) {
 | 
				
			||||||
 | 
							if (v > 1024) {
 | 
				
			||||||
 | 
								v /= 1024;
 | 
				
			||||||
 | 
								u = unit;
 | 
				
			||||||
 | 
							} else {
 | 
				
			||||||
 | 
								break;
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return `${Math.round(v * 10) / 10} ${u}`;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										5
									
								
								apps/journal.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								apps/journal.json
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,5 @@
 | 
				
			|||||||
 | 
					{
 | 
				
			||||||
 | 
						"type": "tildefriends-app",
 | 
				
			||||||
 | 
						"emoji": "📝",
 | 
				
			||||||
 | 
						"previous": "&b//KqE4Vx6kOSBRODK1p/8wjOLKZJ+CBB5IkaBt5YsM=.sha256"
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										185
									
								
								apps/journal/app.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										185
									
								
								apps/journal/app.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,185 @@
 | 
				
			|||||||
 | 
					import * as tfrpc from '/tfrpc.js';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					let g_hash;
 | 
				
			||||||
 | 
					let g_collection_notifies = {};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					tfrpc.register(async function getOwnerIdentities() {
 | 
				
			||||||
 | 
						return ssb.getOwnerIdentities();
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					tfrpc.register(async function getIdentities() {
 | 
				
			||||||
 | 
						return ssb.getIdentities();
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					tfrpc.register(async function query(sql, args) {
 | 
				
			||||||
 | 
						let result = [];
 | 
				
			||||||
 | 
						await ssb.sqlAsync(sql, args, function callback(row) {
 | 
				
			||||||
 | 
							result.push(row);
 | 
				
			||||||
 | 
						});
 | 
				
			||||||
 | 
						return result;
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					tfrpc.register(async function localStorageGet(key) {
 | 
				
			||||||
 | 
						return app.localStorageGet(key);
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					tfrpc.register(async function localStorageSet(key, value) {
 | 
				
			||||||
 | 
						return app.localStorageSet(key, value);
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					tfrpc.register(async function following(ids, depth) {
 | 
				
			||||||
 | 
						return ssb.following(ids, depth);
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					tfrpc.register(async function appendMessage(id, message) {
 | 
				
			||||||
 | 
						return ssb.appendMessageWithIdentity(id, message);
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					tfrpc.register(async function store_blob(blob) {
 | 
				
			||||||
 | 
						if (Array.isArray(blob)) {
 | 
				
			||||||
 | 
							blob = Uint8Array.from(blob);
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return await ssb.blobStore(blob);
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					tfrpc.register(async function get_blob(id) {
 | 
				
			||||||
 | 
						return utf8Decode(await ssb.blobGet(id));
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					let g_new_message_resolve;
 | 
				
			||||||
 | 
					let g_new_message_promise = new Promise(function (resolve, reject) {
 | 
				
			||||||
 | 
						g_new_message_resolve = resolve;
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function new_message() {
 | 
				
			||||||
 | 
						return g_new_message_promise;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					core.register('onMessage', function (id) {
 | 
				
			||||||
 | 
						let resolve = g_new_message_resolve;
 | 
				
			||||||
 | 
						g_new_message_promise = new Promise(function (resolve, reject) {
 | 
				
			||||||
 | 
							g_new_message_resolve = resolve;
 | 
				
			||||||
 | 
						});
 | 
				
			||||||
 | 
						if (resolve) {
 | 
				
			||||||
 | 
							resolve();
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					core.register('message', async function message_handler(message) {
 | 
				
			||||||
 | 
						if (message.event == 'hashChange') {
 | 
				
			||||||
 | 
							print('hash change', message.hash);
 | 
				
			||||||
 | 
							g_hash = message.hash;
 | 
				
			||||||
 | 
							await tfrpc.rpc.hash_changed(message.hash);
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					tfrpc.register(function set_hash(hash) {
 | 
				
			||||||
 | 
						if (g_hash != hash) {
 | 
				
			||||||
 | 
							return app.setHash(hash);
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					tfrpc.register(function get_hash(id, message) {
 | 
				
			||||||
 | 
						return g_hash;
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					tfrpc.register(async function try_decrypt(id, content) {
 | 
				
			||||||
 | 
						return await ssb.privateMessageDecrypt(id, content);
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					tfrpc.register(async function encrypt(id, recipients, content) {
 | 
				
			||||||
 | 
						return await ssb.privateMessageEncrypt(id, recipients, content);
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					async function process_message(whoami, collection, message, kind, parent) {
 | 
				
			||||||
 | 
						let content = JSON.parse(message.content);
 | 
				
			||||||
 | 
						if (typeof content == 'string') {
 | 
				
			||||||
 | 
							let x;
 | 
				
			||||||
 | 
							for (let id of whoami) {
 | 
				
			||||||
 | 
								x = await ssb.privateMessageDecrypt(id, content);
 | 
				
			||||||
 | 
								if (x) {
 | 
				
			||||||
 | 
									content = JSON.parse(x);
 | 
				
			||||||
 | 
									break;
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							if (!x) {
 | 
				
			||||||
 | 
								return;
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							if (content.type !== kind || (parent && content.parent !== parent)) {
 | 
				
			||||||
 | 
								return;
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if (content?.key) {
 | 
				
			||||||
 | 
							if (content?.tombstone) {
 | 
				
			||||||
 | 
								delete collection[content.key];
 | 
				
			||||||
 | 
							} else {
 | 
				
			||||||
 | 
								collection[content.key] = Object.assign(
 | 
				
			||||||
 | 
									collection[content.key] || {},
 | 
				
			||||||
 | 
									content
 | 
				
			||||||
 | 
								);
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						} else {
 | 
				
			||||||
 | 
							collection[message.id] = Object.assign(content, {id: message.id});
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return true;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					tfrpc.register(async function collection(ids, kind, parent, max_rowid, data) {
 | 
				
			||||||
 | 
						let whoami = await ssb.getIdentities();
 | 
				
			||||||
 | 
						data = data ?? {};
 | 
				
			||||||
 | 
						let rowid = 0;
 | 
				
			||||||
 | 
						await ssb.sqlAsync(
 | 
				
			||||||
 | 
							'SELECT MAX(rowid) AS rowid FROM messages',
 | 
				
			||||||
 | 
							[],
 | 
				
			||||||
 | 
							function (row) {
 | 
				
			||||||
 | 
								rowid = row.rowid;
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						);
 | 
				
			||||||
 | 
						while (true) {
 | 
				
			||||||
 | 
							if (rowid == max_rowid) {
 | 
				
			||||||
 | 
								await new_message();
 | 
				
			||||||
 | 
								await ssb.sqlAsync(
 | 
				
			||||||
 | 
									'SELECT MAX(rowid) AS rowid FROM messages',
 | 
				
			||||||
 | 
									[],
 | 
				
			||||||
 | 
									function (row) {
 | 
				
			||||||
 | 
										rowid = row.rowid;
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
								);
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							let modified = false;
 | 
				
			||||||
 | 
							let rows = [];
 | 
				
			||||||
 | 
							await ssb.sqlAsync(
 | 
				
			||||||
 | 
								`
 | 
				
			||||||
 | 
								SELECT messages.id, author, content, timestamp
 | 
				
			||||||
 | 
								FROM messages
 | 
				
			||||||
 | 
								JOIN json_each(?1) AS id ON messages.author = id.value
 | 
				
			||||||
 | 
								WHERE
 | 
				
			||||||
 | 
									messages.rowid > ?2 AND
 | 
				
			||||||
 | 
									messages.rowid <= ?3 AND
 | 
				
			||||||
 | 
									((json_extract(messages.content, '$.type') = ?4 AND
 | 
				
			||||||
 | 
									(?5 IS NULL OR json_extract(messages.content, '$.parent') = ?5)) OR
 | 
				
			||||||
 | 
									content LIKE '"%')
 | 
				
			||||||
 | 
								`,
 | 
				
			||||||
 | 
								[JSON.stringify(ids), max_rowid ?? -1, rowid, kind, parent],
 | 
				
			||||||
 | 
								function (row) {
 | 
				
			||||||
 | 
									rows.push(row);
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							);
 | 
				
			||||||
 | 
							max_rowid = rowid;
 | 
				
			||||||
 | 
							for (let row of rows) {
 | 
				
			||||||
 | 
								if (await process_message(whoami, data, row, kind, parent)) {
 | 
				
			||||||
 | 
									modified = true;
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							if (modified) {
 | 
				
			||||||
 | 
								break;
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return [rowid, data];
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					async function main() {
 | 
				
			||||||
 | 
						await app.setDocument(utf8Decode(await getFile('index.html')));
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					main();
 | 
				
			||||||
							
								
								
									
										1
									
								
								apps/journal/commonmark.min.js
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								apps/journal/commonmark.min.js
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
							
								
								
									
										16
									
								
								apps/journal/index.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								apps/journal/index.html
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,16 @@
 | 
				
			|||||||
 | 
					<!doctype html>
 | 
				
			||||||
 | 
					<html>
 | 
				
			||||||
 | 
						<head>
 | 
				
			||||||
 | 
							<base target="_top" />
 | 
				
			||||||
 | 
						</head>
 | 
				
			||||||
 | 
						<body style="color: #fff">
 | 
				
			||||||
 | 
							<tf-journal-app></tf-journal-app>
 | 
				
			||||||
 | 
							<script src="commonmark.min.js"></script>
 | 
				
			||||||
 | 
							<script>
 | 
				
			||||||
 | 
								window.litDisableBundleWarning = true;
 | 
				
			||||||
 | 
							</script>
 | 
				
			||||||
 | 
							<script src="tf-journal-app.js" type="module"></script>
 | 
				
			||||||
 | 
							<script src="tf-journal-entry.js" type="module"></script>
 | 
				
			||||||
 | 
							<script src="tf-id-picker.js" type="module"></script>
 | 
				
			||||||
 | 
						</body>
 | 
				
			||||||
 | 
					</html>
 | 
				
			||||||
							
								
								
									
										120
									
								
								apps/journal/lit-all.min.js
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										120
									
								
								apps/journal/lit-all.min.js
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
							
								
								
									
										1
									
								
								apps/journal/lit-all.min.js.map
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								apps/journal/lit-all.min.js.map
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
							
								
								
									
										43
									
								
								apps/journal/tf-id-picker.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										43
									
								
								apps/journal/tf-id-picker.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,43 @@
 | 
				
			|||||||
 | 
					import {LitElement, html} from './lit-all.min.js';
 | 
				
			||||||
 | 
					import * as tfrpc from '/static/tfrpc.js';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/*
 | 
				
			||||||
 | 
					 ** Provide a list of IDs, and this lets the user pick one.
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					class TfIdentityPickerElement extends LitElement {
 | 
				
			||||||
 | 
						static get properties() {
 | 
				
			||||||
 | 
							return {
 | 
				
			||||||
 | 
								ids: {type: Array},
 | 
				
			||||||
 | 
								selected: {type: String},
 | 
				
			||||||
 | 
							};
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						constructor() {
 | 
				
			||||||
 | 
							super();
 | 
				
			||||||
 | 
							this.ids = [];
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						changed(event) {
 | 
				
			||||||
 | 
							this.selected = event.srcElement.value;
 | 
				
			||||||
 | 
							this.dispatchEvent(
 | 
				
			||||||
 | 
								new Event('change', {
 | 
				
			||||||
 | 
									srcElement: this,
 | 
				
			||||||
 | 
								})
 | 
				
			||||||
 | 
							);
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						render() {
 | 
				
			||||||
 | 
							return html`
 | 
				
			||||||
 | 
								<select @change=${this.changed} style="max-width: 100%">
 | 
				
			||||||
 | 
									${(this.ids ?? []).map(
 | 
				
			||||||
 | 
										(id) =>
 | 
				
			||||||
 | 
											html`<option ?selected=${id == this.selected} value=${id}>
 | 
				
			||||||
 | 
												${id}
 | 
				
			||||||
 | 
											</option>`
 | 
				
			||||||
 | 
									)}
 | 
				
			||||||
 | 
								</select>
 | 
				
			||||||
 | 
							`;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					customElements.define('tf-id-picker', TfIdentityPickerElement);
 | 
				
			||||||
							
								
								
									
										89
									
								
								apps/journal/tf-journal-app.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										89
									
								
								apps/journal/tf-journal-app.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,89 @@
 | 
				
			|||||||
 | 
					import {LitElement, html, keyed, live} from './lit-all.min.js';
 | 
				
			||||||
 | 
					import * as tfrpc from '/static/tfrpc.js';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class TfJournalAppElement extends LitElement {
 | 
				
			||||||
 | 
						static get properties() {
 | 
				
			||||||
 | 
							return {
 | 
				
			||||||
 | 
								ids: {type: Array},
 | 
				
			||||||
 | 
								owner_ids: {type: Array},
 | 
				
			||||||
 | 
								whoami: {type: String},
 | 
				
			||||||
 | 
								journals: {type: Object},
 | 
				
			||||||
 | 
							};
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						constructor() {
 | 
				
			||||||
 | 
							super();
 | 
				
			||||||
 | 
							this.ids = [];
 | 
				
			||||||
 | 
							this.owner_ids = [];
 | 
				
			||||||
 | 
							this.journals = {};
 | 
				
			||||||
 | 
							this.load();
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						async load() {
 | 
				
			||||||
 | 
							this.ids = await tfrpc.rpc.getIdentities();
 | 
				
			||||||
 | 
							this.whoami = await tfrpc.rpc.localStorageGet('journal_whoami');
 | 
				
			||||||
 | 
							await this.read_journals();
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						async read_journals() {
 | 
				
			||||||
 | 
							let max_rowid;
 | 
				
			||||||
 | 
							let journals;
 | 
				
			||||||
 | 
							while (true) {
 | 
				
			||||||
 | 
								[max_rowid, journals] = await tfrpc.rpc.collection(
 | 
				
			||||||
 | 
									[this.whoami],
 | 
				
			||||||
 | 
									'journal-entry',
 | 
				
			||||||
 | 
									undefined,
 | 
				
			||||||
 | 
									max_rowid,
 | 
				
			||||||
 | 
									journals
 | 
				
			||||||
 | 
								);
 | 
				
			||||||
 | 
								this.journals = Object.assign({}, journals);
 | 
				
			||||||
 | 
								console.log('JOURNALS', this.journals);
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						async on_whoami_changed(event) {
 | 
				
			||||||
 | 
							let new_id = event.srcElement.selected;
 | 
				
			||||||
 | 
							await tfrpc.rpc.localStorageSet('journal_whoami', new_id);
 | 
				
			||||||
 | 
							this.whoami = new_id;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						async on_journal_publish(event) {
 | 
				
			||||||
 | 
							let key = event.detail.key;
 | 
				
			||||||
 | 
							let text = event.detail.text;
 | 
				
			||||||
 | 
							let message = {
 | 
				
			||||||
 | 
								type: 'journal-entry',
 | 
				
			||||||
 | 
								key: key,
 | 
				
			||||||
 | 
								text: text,
 | 
				
			||||||
 | 
							};
 | 
				
			||||||
 | 
							message.recps = [this.whoami];
 | 
				
			||||||
 | 
							print(message);
 | 
				
			||||||
 | 
							message = await tfrpc.rpc.encrypt(
 | 
				
			||||||
 | 
								this.whoami,
 | 
				
			||||||
 | 
								message.recps,
 | 
				
			||||||
 | 
								JSON.stringify(message)
 | 
				
			||||||
 | 
							);
 | 
				
			||||||
 | 
							print(message);
 | 
				
			||||||
 | 
							await tfrpc.rpc.appendMessage(this.whoami, message);
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						render() {
 | 
				
			||||||
 | 
							console.log('RENDER APP', this.journals);
 | 
				
			||||||
 | 
							let self = this;
 | 
				
			||||||
 | 
							return html`
 | 
				
			||||||
 | 
								<div>
 | 
				
			||||||
 | 
									<tf-id-picker
 | 
				
			||||||
 | 
										.ids=${this.ids}
 | 
				
			||||||
 | 
										selected=${this.whoami}
 | 
				
			||||||
 | 
										@change=${this.on_whoami_changed}
 | 
				
			||||||
 | 
									></tf-id-picker>
 | 
				
			||||||
 | 
								</div>
 | 
				
			||||||
 | 
								<tf-journal-entry
 | 
				
			||||||
 | 
									whoami=${this.whoami}
 | 
				
			||||||
 | 
									.journals=${this.journals}
 | 
				
			||||||
 | 
									@publish=${this.on_journal_publish}
 | 
				
			||||||
 | 
								></tf-journal-entry>
 | 
				
			||||||
 | 
							`;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					customElements.define('tf-journal-app', TfJournalAppElement);
 | 
				
			||||||
							
								
								
									
										97
									
								
								apps/journal/tf-journal-entry.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										97
									
								
								apps/journal/tf-journal-entry.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,97 @@
 | 
				
			|||||||
 | 
					import {LitElement, html, unsafeHTML, range} from './lit-all.min.js';
 | 
				
			||||||
 | 
					import * as tfrpc from '/static/tfrpc.js';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class TfJournalEntryElement extends LitElement {
 | 
				
			||||||
 | 
						static get properties() {
 | 
				
			||||||
 | 
							return {
 | 
				
			||||||
 | 
								whoami: {type: String},
 | 
				
			||||||
 | 
								key: {type: String},
 | 
				
			||||||
 | 
								journals: {type: Object},
 | 
				
			||||||
 | 
								text: {type: String},
 | 
				
			||||||
 | 
							};
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						constructor() {
 | 
				
			||||||
 | 
							super();
 | 
				
			||||||
 | 
							this.journals = {};
 | 
				
			||||||
 | 
							this.key = new Date().toISOString().split('T')[0];
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						markdown(md) {
 | 
				
			||||||
 | 
							var reader = new commonmark.Parser({safe: true});
 | 
				
			||||||
 | 
							var writer = new commonmark.HtmlRenderer();
 | 
				
			||||||
 | 
							var parsed = reader.parse(md || '');
 | 
				
			||||||
 | 
							return writer.render(parsed);
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						on_discard(event) {
 | 
				
			||||||
 | 
							this.text = undefined;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						async on_publish() {
 | 
				
			||||||
 | 
							console.log('publish', this.text);
 | 
				
			||||||
 | 
							this.dispatchEvent(
 | 
				
			||||||
 | 
								new CustomEvent('publish', {
 | 
				
			||||||
 | 
									bubbles: true,
 | 
				
			||||||
 | 
									detail: {
 | 
				
			||||||
 | 
										key: this.shadowRoot.getElementById('date_picker').value,
 | 
				
			||||||
 | 
										text: this.text,
 | 
				
			||||||
 | 
									},
 | 
				
			||||||
 | 
								})
 | 
				
			||||||
 | 
							);
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						back_dates(count) {
 | 
				
			||||||
 | 
							let now = new Date();
 | 
				
			||||||
 | 
							let result = [];
 | 
				
			||||||
 | 
							for (let i = 0; i < count; i++) {
 | 
				
			||||||
 | 
								let next = new Date(now);
 | 
				
			||||||
 | 
								next.setDate(now.getDate() - i);
 | 
				
			||||||
 | 
								result.push(next.toISOString().split('T')[0]);
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							return result;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						on_edit(event) {
 | 
				
			||||||
 | 
							this.text = event.srcElement.value;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						on_date_change(event) {
 | 
				
			||||||
 | 
							this.key = event.srcElement.value;
 | 
				
			||||||
 | 
							this.text = this.journals[this.key]?.text;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						render() {
 | 
				
			||||||
 | 
							console.log('RENDER ENTRY', this.key, this.journals?.[this.key]);
 | 
				
			||||||
 | 
							return html`
 | 
				
			||||||
 | 
								<select id="date_picker" @change=${this.on_date_change}>
 | 
				
			||||||
 | 
									${this.back_dates(10).map(
 | 
				
			||||||
 | 
										(x) => html` <option value=${x}>${x}</option> `
 | 
				
			||||||
 | 
									)}
 | 
				
			||||||
 | 
								</select>
 | 
				
			||||||
 | 
								<div style="display: inline-flex; flex-direction: row">
 | 
				
			||||||
 | 
									<button
 | 
				
			||||||
 | 
										?disabled=${this.text == this.journals?.[this.key]?.text}
 | 
				
			||||||
 | 
										@click=${this.on_publish}
 | 
				
			||||||
 | 
									>
 | 
				
			||||||
 | 
										Publish
 | 
				
			||||||
 | 
									</button>
 | 
				
			||||||
 | 
									<button @click=${this.on_discard}>Discard</button>
 | 
				
			||||||
 | 
								</div>
 | 
				
			||||||
 | 
								<div style="display: flex; flex-direction: row">
 | 
				
			||||||
 | 
									<textarea
 | 
				
			||||||
 | 
										style="flex: 1 1; min-height: 10em"
 | 
				
			||||||
 | 
										@input=${this.on_edit}
 | 
				
			||||||
 | 
										.value=${this.text ?? this.journals?.[this.key]?.text ?? ''}
 | 
				
			||||||
 | 
									></textarea>
 | 
				
			||||||
 | 
									<div style="flex: 1 1">
 | 
				
			||||||
 | 
										${unsafeHTML(
 | 
				
			||||||
 | 
											this.markdown(this.text ?? this.journals?.[this.key]?.text)
 | 
				
			||||||
 | 
										)}
 | 
				
			||||||
 | 
									</div>
 | 
				
			||||||
 | 
								</div>
 | 
				
			||||||
 | 
							`;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					customElements.define('tf-journal-entry', TfJournalEntryElement);
 | 
				
			||||||
							
								
								
									
										5
									
								
								apps/room.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								apps/room.json
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,5 @@
 | 
				
			|||||||
 | 
					{
 | 
				
			||||||
 | 
						"type": "tildefriends-app",
 | 
				
			||||||
 | 
						"emoji": "🚪",
 | 
				
			||||||
 | 
						"previous": "&HXCdDG8gGYXElTyEFbg85jqa6lDXNL2ENPIA9UoJNbI=.sha256"
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										13
									
								
								apps/room/app.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								apps/room/app.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,13 @@
 | 
				
			|||||||
 | 
					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=`;
 | 
				
			||||||
 | 
						await app.setDocument(`
 | 
				
			||||||
 | 
							<body style="color: #fff">
 | 
				
			||||||
 | 
								<h1>Server</h1>
 | 
				
			||||||
 | 
								<div>The local server address is:</div>
 | 
				
			||||||
 | 
								<div><input type="text" readonly value="${room}" style="width: 100%"></input></div>
 | 
				
			||||||
 | 
							</body>
 | 
				
			||||||
 | 
						`);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					main();
 | 
				
			||||||
							
								
								
									
										5
									
								
								apps/sneaker.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								apps/sneaker.json
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,5 @@
 | 
				
			|||||||
 | 
					{
 | 
				
			||||||
 | 
						"type": "tildefriends-app",
 | 
				
			||||||
 | 
						"emoji": "👟",
 | 
				
			||||||
 | 
						"previous": "&lYZRnT2UGQxXxYISbuaZewik9AuxBpcJumakwrePw5c=.sha256"
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										30
									
								
								apps/sneaker/app.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								apps/sneaker/app.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,30 @@
 | 
				
			|||||||
 | 
					import * as tfrpc from '/tfrpc.js';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					tfrpc.register(async function getAllIdentities() {
 | 
				
			||||||
 | 
						return ssb.getAllIdentities();
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					tfrpc.register(async function query(sql, args) {
 | 
				
			||||||
 | 
						let result = [];
 | 
				
			||||||
 | 
						await ssb.sqlAsync(sql, args, function callback(row) {
 | 
				
			||||||
 | 
							result.push(row);
 | 
				
			||||||
 | 
						});
 | 
				
			||||||
 | 
						return result;
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					tfrpc.register(async function store_blob(blob) {
 | 
				
			||||||
 | 
						if (Array.isArray(blob)) {
 | 
				
			||||||
 | 
							blob = Uint8Array.from(blob);
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return await ssb.blobStore(blob);
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					tfrpc.register(async function get_blob(id) {
 | 
				
			||||||
 | 
						return Array.from(new Uint8Array(await ssb.blobGet(id)));
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					tfrpc.register(async function store_message(message) {
 | 
				
			||||||
 | 
						return await ssb.storeMessage(message);
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					async function main() {
 | 
				
			||||||
 | 
						await app.setDocument(utf8Decode(await getFile('index.html')));
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					main();
 | 
				
			||||||
							
								
								
									
										3
									
								
								apps/sneaker/filesaver.min.js
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								apps/sneaker/filesaver.min.js
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@@ -0,0 +1,3 @@
 | 
				
			|||||||
 | 
					(function(a,b){if("function"==typeof define&&define.amd)define([],b);else if("undefined"!=typeof exports)b();else{b(),a.FileSaver={exports:{}}.exports}})(this,function(){"use strict";function b(a,b){return"undefined"==typeof b?b={autoBom:!1}:"object"!=typeof b&&(console.warn("Deprecated: Expected third argument to be a object"),b={autoBom:!b}),b.autoBom&&/^\s*(?:text\/\S*|application\/xml|\S*\/\S*\+xml)\s*;.*charset\s*=\s*utf-8/i.test(a.type)?new Blob(["\uFEFF",a],{type:a.type}):a}function c(a,b,c){var d=new XMLHttpRequest;d.open("GET",a),d.responseType="blob",d.onload=function(){g(d.response,b,c)},d.onerror=function(){console.error("could not download file")},d.send()}function d(a){var b=new XMLHttpRequest;b.open("HEAD",a,!1);try{b.send()}catch(a){}return 200<=b.status&&299>=b.status}function e(a){try{a.dispatchEvent(new MouseEvent("click"))}catch(c){var b=document.createEvent("MouseEvents");b.initMouseEvent("click",!0,!0,window,0,0,0,80,20,!1,!1,!1,!1,0,null),a.dispatchEvent(b)}}var f="object"==typeof window&&window.window===window?window:"object"==typeof self&&self.self===self?self:"object"==typeof global&&global.global===global?global:void 0,a=/Macintosh/.test(navigator.userAgent)&&/AppleWebKit/.test(navigator.userAgent)&&!/Safari/.test(navigator.userAgent),g=f.saveAs||("object"!=typeof window||window!==f?function(){}:"download"in HTMLAnchorElement.prototype&&!a?function(b,g,h){var i=f.URL||f.webkitURL,j=document.createElement("a");g=g||b.name||"download",j.download=g,j.rel="noopener","string"==typeof b?(j.href=b,j.origin===location.origin?e(j):d(j.href)?c(b,g,h):e(j,j.target="_blank")):(j.href=i.createObjectURL(b),setTimeout(function(){i.revokeObjectURL(j.href)},4E4),setTimeout(function(){e(j)},0))}:"msSaveOrOpenBlob"in navigator?function(f,g,h){if(g=g||f.name||"download","string"!=typeof f)navigator.msSaveOrOpenBlob(b(f,h),g);else if(d(f))c(f,g,h);else{var i=document.createElement("a");i.href=f,i.target="_blank",setTimeout(function(){e(i)})}}:function(b,d,e,g){if(g=g||open("","_blank"),g&&(g.document.title=g.document.body.innerText="downloading..."),"string"==typeof b)return c(b,d,e);var h="application/octet-stream"===b.type,i=/constructor/i.test(f.HTMLElement)||f.safari,j=/CriOS\/[\d]+/.test(navigator.userAgent);if((j||h&&i||a)&&"undefined"!=typeof FileReader){var k=new FileReader;k.onloadend=function(){var a=k.result;a=j?a:a.replace(/^data:[^;]*;/,"data:attachment/file;"),g?g.location.href=a:location=a,g=null},k.readAsDataURL(b)}else{var l=f.URL||f.webkitURL,m=l.createObjectURL(b);g?g.location=m:location.href=m,g=null,setTimeout(function(){l.revokeObjectURL(m)},4E4)}});f.saveAs=g.saveAs=g,"undefined"!=typeof module&&(module.exports=g)});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					//# sourceMappingURL=FileSaver.min.js.map
 | 
				
			||||||
							
								
								
									
										16
									
								
								apps/sneaker/index.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								apps/sneaker/index.html
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,16 @@
 | 
				
			|||||||
 | 
					<!doctype html>
 | 
				
			||||||
 | 
					<html style="color: #fff">
 | 
				
			||||||
 | 
						<head>
 | 
				
			||||||
 | 
							<title>Tilde Friends</title>
 | 
				
			||||||
 | 
							<base target="_top" />
 | 
				
			||||||
 | 
						</head>
 | 
				
			||||||
 | 
						<body>
 | 
				
			||||||
 | 
							<tf-sneaker-app />
 | 
				
			||||||
 | 
							<script>
 | 
				
			||||||
 | 
								window.litDisableBundleWarning = true;
 | 
				
			||||||
 | 
							</script>
 | 
				
			||||||
 | 
							<script src="filesaver.min.js"></script>
 | 
				
			||||||
 | 
							<script src="jszip.min.js"></script>
 | 
				
			||||||
 | 
							<script src="script.js" type="module"></script>
 | 
				
			||||||
 | 
						</body>
 | 
				
			||||||
 | 
					</html>
 | 
				
			||||||
							
								
								
									
										13
									
								
								apps/sneaker/jszip.min.js
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								apps/sneaker/jszip.min.js
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
							
								
								
									
										120
									
								
								apps/sneaker/lit-all.min.js
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										120
									
								
								apps/sneaker/lit-all.min.js
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
							
								
								
									
										1
									
								
								apps/sneaker/lit-all.min.js.map
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								apps/sneaker/lit-all.min.js.map
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
							
								
								
									
										350
									
								
								apps/sneaker/script.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										350
									
								
								apps/sneaker/script.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,350 @@
 | 
				
			|||||||
 | 
					import {LitElement, html} from './lit-all.min.js';
 | 
				
			||||||
 | 
					import * as tfrpc from '/static/tfrpc.js';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class TfSneakerAppElement extends LitElement {
 | 
				
			||||||
 | 
						static get properties() {
 | 
				
			||||||
 | 
							return {
 | 
				
			||||||
 | 
								feeds: {type: Object},
 | 
				
			||||||
 | 
								progress: {type: Object},
 | 
				
			||||||
 | 
								result: {type: String},
 | 
				
			||||||
 | 
							};
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						constructor() {
 | 
				
			||||||
 | 
							super();
 | 
				
			||||||
 | 
							this.feeds = [];
 | 
				
			||||||
 | 
							this.progress = undefined;
 | 
				
			||||||
 | 
							this.result = undefined;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						async search() {
 | 
				
			||||||
 | 
							let q = this.renderRoot.getElementById('search').value;
 | 
				
			||||||
 | 
							let result = await tfrpc.rpc.query(
 | 
				
			||||||
 | 
								`
 | 
				
			||||||
 | 
								SELECT messages.author AS id, json_extract(messages.content, '$.name') AS name
 | 
				
			||||||
 | 
								FROM messages_fts(?)
 | 
				
			||||||
 | 
								JOIN messages ON messages.rowid = messages_fts.rowid
 | 
				
			||||||
 | 
								WHERE
 | 
				
			||||||
 | 
									json_extract(messages.content, '$.type') = 'about' AND
 | 
				
			||||||
 | 
									json_extract(messages.content, '$.about') = messages.author AND
 | 
				
			||||||
 | 
									json_extract(messages.content, '$.name') IS NOT NULL
 | 
				
			||||||
 | 
								GROUP BY messages.author
 | 
				
			||||||
 | 
								HAVING MAX(messages.sequence)
 | 
				
			||||||
 | 
								ORDER BY COUNT(*) DESC
 | 
				
			||||||
 | 
								`,
 | 
				
			||||||
 | 
								[`"${q.replaceAll('"', '""')}"`]
 | 
				
			||||||
 | 
							);
 | 
				
			||||||
 | 
							this.feeds = Object.fromEntries(result.map((x) => [x.id, x.name]));
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						format_message(message) {
 | 
				
			||||||
 | 
							const k_flag_sequence_before_author = 1;
 | 
				
			||||||
 | 
							let out = {
 | 
				
			||||||
 | 
								previous: message.previous ?? null,
 | 
				
			||||||
 | 
							};
 | 
				
			||||||
 | 
							if (message.flags & k_flag_sequence_before_author) {
 | 
				
			||||||
 | 
								out.sequence = message.sequence;
 | 
				
			||||||
 | 
								out.author = message.author;
 | 
				
			||||||
 | 
							} else {
 | 
				
			||||||
 | 
								out.author = message.author;
 | 
				
			||||||
 | 
								out.sequence = message.sequence;
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							out.timestamp = message.timestamp;
 | 
				
			||||||
 | 
							out.hash = message.hash;
 | 
				
			||||||
 | 
							out.content = JSON.parse(message.content);
 | 
				
			||||||
 | 
							out.signature = message.signature;
 | 
				
			||||||
 | 
							return {key: message.id, value: out};
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						sanitize(value) {
 | 
				
			||||||
 | 
							return value.replaceAll('/', '_').replaceAll('+', '-');
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						guess_ext(data) {
 | 
				
			||||||
 | 
							function startsWith(prefix) {
 | 
				
			||||||
 | 
								if (data.length < prefix.length) {
 | 
				
			||||||
 | 
									return false;
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								for (let i = 0; i < prefix.length; i++) {
 | 
				
			||||||
 | 
									if (prefix[i] !== null && data[i] !== prefix[i]) {
 | 
				
			||||||
 | 
										return false;
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								return true;
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if (
 | 
				
			||||||
 | 
								startsWith(data, [0xff, 0xd8, 0xff, 0xdb]) ||
 | 
				
			||||||
 | 
								startsWith(
 | 
				
			||||||
 | 
									data,
 | 
				
			||||||
 | 
									[0xff, 0xd8, 0xff, 0xe0, 0x00, 0x10, 0x4a, 0x46, 0x49, 0x46, 0x00, 0x01]
 | 
				
			||||||
 | 
								) ||
 | 
				
			||||||
 | 
								startsWith(data, [0xff, 0xd8, 0xff, 0xee]) ||
 | 
				
			||||||
 | 
								startsWith(data, [
 | 
				
			||||||
 | 
									0xff,
 | 
				
			||||||
 | 
									0xd8,
 | 
				
			||||||
 | 
									0xff,
 | 
				
			||||||
 | 
									0xe1,
 | 
				
			||||||
 | 
									null,
 | 
				
			||||||
 | 
									null,
 | 
				
			||||||
 | 
									0x45,
 | 
				
			||||||
 | 
									0x78,
 | 
				
			||||||
 | 
									0x69,
 | 
				
			||||||
 | 
									0x66,
 | 
				
			||||||
 | 
									0x00,
 | 
				
			||||||
 | 
									0x00,
 | 
				
			||||||
 | 
								])
 | 
				
			||||||
 | 
							) {
 | 
				
			||||||
 | 
								return '.jpg';
 | 
				
			||||||
 | 
							} else if (
 | 
				
			||||||
 | 
								startsWith(data, [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a])
 | 
				
			||||||
 | 
							) {
 | 
				
			||||||
 | 
								return '.png';
 | 
				
			||||||
 | 
							} else if (
 | 
				
			||||||
 | 
								startsWith(data, [0x47, 0x49, 0x46, 0x38, 0x37, 0x61]) ||
 | 
				
			||||||
 | 
								startsWith(data, [0x47, 0x49, 0x46, 0x38, 0x39, 0x61])
 | 
				
			||||||
 | 
							) {
 | 
				
			||||||
 | 
								return '.gif';
 | 
				
			||||||
 | 
							} else if (
 | 
				
			||||||
 | 
								startsWith(data, [
 | 
				
			||||||
 | 
									0x52,
 | 
				
			||||||
 | 
									0x49,
 | 
				
			||||||
 | 
									0x46,
 | 
				
			||||||
 | 
									0x46,
 | 
				
			||||||
 | 
									null,
 | 
				
			||||||
 | 
									null,
 | 
				
			||||||
 | 
									null,
 | 
				
			||||||
 | 
									null,
 | 
				
			||||||
 | 
									0x57,
 | 
				
			||||||
 | 
									0x45,
 | 
				
			||||||
 | 
									0x42,
 | 
				
			||||||
 | 
									0x50,
 | 
				
			||||||
 | 
								])
 | 
				
			||||||
 | 
							) {
 | 
				
			||||||
 | 
								return '.webp';
 | 
				
			||||||
 | 
							} else if (startsWith(data, [0x3c, 0x73, 0x76, 0x67])) {
 | 
				
			||||||
 | 
								return '.svg';
 | 
				
			||||||
 | 
							} else if (
 | 
				
			||||||
 | 
								startsWith(data, [
 | 
				
			||||||
 | 
									null,
 | 
				
			||||||
 | 
									null,
 | 
				
			||||||
 | 
									null,
 | 
				
			||||||
 | 
									null,
 | 
				
			||||||
 | 
									0x66,
 | 
				
			||||||
 | 
									0x74,
 | 
				
			||||||
 | 
									0x79,
 | 
				
			||||||
 | 
									0x70,
 | 
				
			||||||
 | 
									0x6d,
 | 
				
			||||||
 | 
									0x70,
 | 
				
			||||||
 | 
									0x34,
 | 
				
			||||||
 | 
									0x32,
 | 
				
			||||||
 | 
								])
 | 
				
			||||||
 | 
							) {
 | 
				
			||||||
 | 
								return '.mp3';
 | 
				
			||||||
 | 
							} else if (
 | 
				
			||||||
 | 
								startsWith(data, [
 | 
				
			||||||
 | 
									null,
 | 
				
			||||||
 | 
									null,
 | 
				
			||||||
 | 
									null,
 | 
				
			||||||
 | 
									null,
 | 
				
			||||||
 | 
									0x66,
 | 
				
			||||||
 | 
									0x74,
 | 
				
			||||||
 | 
									0x79,
 | 
				
			||||||
 | 
									0x70,
 | 
				
			||||||
 | 
									0x69,
 | 
				
			||||||
 | 
									0x73,
 | 
				
			||||||
 | 
									0x6f,
 | 
				
			||||||
 | 
									0x6d,
 | 
				
			||||||
 | 
								]) ||
 | 
				
			||||||
 | 
								startsWith(data, [
 | 
				
			||||||
 | 
									null,
 | 
				
			||||||
 | 
									null,
 | 
				
			||||||
 | 
									null,
 | 
				
			||||||
 | 
									null,
 | 
				
			||||||
 | 
									0x66,
 | 
				
			||||||
 | 
									0x74,
 | 
				
			||||||
 | 
									0x79,
 | 
				
			||||||
 | 
									0x70,
 | 
				
			||||||
 | 
									0x6d,
 | 
				
			||||||
 | 
									0x70,
 | 
				
			||||||
 | 
									0x34,
 | 
				
			||||||
 | 
									0x32,
 | 
				
			||||||
 | 
								])
 | 
				
			||||||
 | 
							) {
 | 
				
			||||||
 | 
								return '.mp4';
 | 
				
			||||||
 | 
							} else {
 | 
				
			||||||
 | 
								return '.bin';
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						async export(id) {
 | 
				
			||||||
 | 
							let all_messages = '';
 | 
				
			||||||
 | 
							let sequence = -1;
 | 
				
			||||||
 | 
							let messages_done = 0;
 | 
				
			||||||
 | 
							let messages_max = (
 | 
				
			||||||
 | 
								await tfrpc.rpc.query(
 | 
				
			||||||
 | 
									'SELECT MAX(sequence) AS total FROM messages WHERE author = ?',
 | 
				
			||||||
 | 
									[id]
 | 
				
			||||||
 | 
								)
 | 
				
			||||||
 | 
							)[0].total;
 | 
				
			||||||
 | 
							while (true) {
 | 
				
			||||||
 | 
								let messages = await tfrpc.rpc.query(
 | 
				
			||||||
 | 
									`
 | 
				
			||||||
 | 
									SELECT author, id, sequence, timestamp, hash, json(content) AS content, signature, flags
 | 
				
			||||||
 | 
									FROM messages
 | 
				
			||||||
 | 
									WHERE author = ? AND SEQUENCE > ?
 | 
				
			||||||
 | 
									ORDER BY sequence LIMIT 100
 | 
				
			||||||
 | 
									`,
 | 
				
			||||||
 | 
									[id, sequence]
 | 
				
			||||||
 | 
								);
 | 
				
			||||||
 | 
								if (messages?.length) {
 | 
				
			||||||
 | 
									all_messages +=
 | 
				
			||||||
 | 
										messages
 | 
				
			||||||
 | 
											.map((x) => JSON.stringify(this.format_message(x)))
 | 
				
			||||||
 | 
											.join('\n') + '\n';
 | 
				
			||||||
 | 
									sequence = messages[messages.length - 1].sequence;
 | 
				
			||||||
 | 
									messages_done += messages.length;
 | 
				
			||||||
 | 
									this.progress = {
 | 
				
			||||||
 | 
										name: 'messages',
 | 
				
			||||||
 | 
										value: messages_done,
 | 
				
			||||||
 | 
										max: messages_max,
 | 
				
			||||||
 | 
									};
 | 
				
			||||||
 | 
								} else {
 | 
				
			||||||
 | 
									break;
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							let zip = new JSZip();
 | 
				
			||||||
 | 
							zip.file(`message/classic/${this.sanitize(id)}.ndjson`, all_messages);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							let blobs = await tfrpc.rpc.query(
 | 
				
			||||||
 | 
								`SELECT messages_refs.ref AS id
 | 
				
			||||||
 | 
								FROM messages
 | 
				
			||||||
 | 
								JOIN messages_refs ON messages.id = messages_refs.message
 | 
				
			||||||
 | 
								WHERE messages.author = ? AND messages_refs.ref LIKE '&%.sha256'`,
 | 
				
			||||||
 | 
								[id]
 | 
				
			||||||
 | 
							);
 | 
				
			||||||
 | 
							let blobs_done = 0;
 | 
				
			||||||
 | 
							for (let row of blobs) {
 | 
				
			||||||
 | 
								this.progress = {name: 'blobs', value: blobs_done, max: blobs.length};
 | 
				
			||||||
 | 
								let blob;
 | 
				
			||||||
 | 
								try {
 | 
				
			||||||
 | 
									blob = await tfrpc.rpc.get_blob(row.id);
 | 
				
			||||||
 | 
								} catch (e) {
 | 
				
			||||||
 | 
									console.log(`Failed to get ${row.id}: ${e.message}`);
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								if (blob) {
 | 
				
			||||||
 | 
									zip.file(
 | 
				
			||||||
 | 
										`blob/classic/${this.sanitize(row.id)}${this.guess_ext(blob)}`,
 | 
				
			||||||
 | 
										new Uint8Array(blob)
 | 
				
			||||||
 | 
									);
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								blobs_done++;
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							this.progress = {name: 'saving'};
 | 
				
			||||||
 | 
							let blob = await zip.generateAsync({type: 'blob'});
 | 
				
			||||||
 | 
							saveAs(blob, `${this.sanitize(id)}.zip`);
 | 
				
			||||||
 | 
							this.progress = null;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						keypress(event) {
 | 
				
			||||||
 | 
							if (event.key == 'Enter') {
 | 
				
			||||||
 | 
								this.search();
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						async import(event) {
 | 
				
			||||||
 | 
							let file = event.target.files[0];
 | 
				
			||||||
 | 
							if (!file) {
 | 
				
			||||||
 | 
								return;
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							this.progress = {name: 'loading'};
 | 
				
			||||||
 | 
							let zip = new JSZip();
 | 
				
			||||||
 | 
							file = await zip.loadAsync(file);
 | 
				
			||||||
 | 
							let messages = [];
 | 
				
			||||||
 | 
							let blobs = [];
 | 
				
			||||||
 | 
							file.forEach(function (path, entry) {
 | 
				
			||||||
 | 
								if (!entry.dir) {
 | 
				
			||||||
 | 
									if (path.startsWith('message/classic/')) {
 | 
				
			||||||
 | 
										messages.push(entry);
 | 
				
			||||||
 | 
									} else {
 | 
				
			||||||
 | 
										blobs.push(entry);
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							});
 | 
				
			||||||
 | 
							let success = {messages: 0, blobs: 0};
 | 
				
			||||||
 | 
							let progress = 0;
 | 
				
			||||||
 | 
							let total_messages = 0;
 | 
				
			||||||
 | 
							for (let entry of messages) {
 | 
				
			||||||
 | 
								let lines = (await entry.async('string')).split('\n');
 | 
				
			||||||
 | 
								total_messages += lines.length;
 | 
				
			||||||
 | 
								for (let line of lines) {
 | 
				
			||||||
 | 
									if (!line.length) {
 | 
				
			||||||
 | 
										continue;
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
									let message = JSON.parse(line);
 | 
				
			||||||
 | 
									this.progress = {
 | 
				
			||||||
 | 
										name: 'messages',
 | 
				
			||||||
 | 
										value: progress++,
 | 
				
			||||||
 | 
										max: total_messages,
 | 
				
			||||||
 | 
									};
 | 
				
			||||||
 | 
									if (await tfrpc.rpc.store_message(message.value)) {
 | 
				
			||||||
 | 
										success.messages++;
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							progress = 0;
 | 
				
			||||||
 | 
							for (let blob of blobs) {
 | 
				
			||||||
 | 
								this.progress = {name: 'blobs', value: progress++, max: blobs.length};
 | 
				
			||||||
 | 
								if (await tfrpc.rpc.store_blob(await blob.async('arraybuffer'))) {
 | 
				
			||||||
 | 
									success.blobs++;
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							this.progress = undefined;
 | 
				
			||||||
 | 
							this.result = `imported ${success.messages} messages and ${success.blobs} blobs`;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						render() {
 | 
				
			||||||
 | 
							let progress;
 | 
				
			||||||
 | 
							if (this.progress) {
 | 
				
			||||||
 | 
								if (this.progress.max) {
 | 
				
			||||||
 | 
									progress = html`<div>
 | 
				
			||||||
 | 
										<label for="progress">${this.progress.name}</label
 | 
				
			||||||
 | 
										><progress
 | 
				
			||||||
 | 
											value=${this.progress.value}
 | 
				
			||||||
 | 
											max=${this.progress.max}
 | 
				
			||||||
 | 
										></progress>
 | 
				
			||||||
 | 
									</div>`;
 | 
				
			||||||
 | 
								} else {
 | 
				
			||||||
 | 
									progress = html`<div><span>${this.progress.name}</span></div>`;
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							return html`<h1>SSB 👟net</h1>
 | 
				
			||||||
 | 
								<code>${this.result}</code>
 | 
				
			||||||
 | 
								${progress}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								<h2>Import</h2>
 | 
				
			||||||
 | 
								<input type="file" id="import" @change=${this.import}></input>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								<h2>Export</h2>
 | 
				
			||||||
 | 
								<input type="text" id="search" @keypress=${this.keypress}></input>
 | 
				
			||||||
 | 
								<input type="button" value="Search Users" @click=${this.search}></input>
 | 
				
			||||||
 | 
								<ul>
 | 
				
			||||||
 | 
									${Object.entries(this.feeds).map(
 | 
				
			||||||
 | 
										([id, name]) => html`
 | 
				
			||||||
 | 
											<li>
 | 
				
			||||||
 | 
												${this.progress
 | 
				
			||||||
 | 
													? undefined
 | 
				
			||||||
 | 
													: html`<input type="button" value="Export" @click=${() => this.export(id)}></input>`}
 | 
				
			||||||
 | 
												${name}
 | 
				
			||||||
 | 
												<code style="color: #ccc">${id}</code>
 | 
				
			||||||
 | 
											</li>
 | 
				
			||||||
 | 
										`
 | 
				
			||||||
 | 
									)}
 | 
				
			||||||
 | 
								</ul>
 | 
				
			||||||
 | 
							`;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					customElements.define('tf-sneaker-app', TfSneakerAppElement);
 | 
				
			||||||
@@ -1,4 +1,5 @@
 | 
				
			|||||||
{
 | 
					{
 | 
				
			||||||
  "type": "tildefriends-app",
 | 
						"type": "tildefriends-app",
 | 
				
			||||||
  "emoji": "🐌"
 | 
						"emoji": "🐌",
 | 
				
			||||||
 | 
						"previous": "&z0N6jlqflRd4+grj16K/IdllNVLQrPLbr7aKVs/21mE=.sha256"
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@@ -18,12 +18,21 @@ tfrpc.register(async function databaseSet(key, value) {
 | 
				
			|||||||
tfrpc.register(async function createIdentity() {
 | 
					tfrpc.register(async function createIdentity() {
 | 
				
			||||||
	return ssb.createIdentity();
 | 
						return ssb.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() {
 | 
					tfrpc.register(async function getIdentities() {
 | 
				
			||||||
	return ssb.getIdentities();
 | 
						return ssb.getIdentities();
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
tfrpc.register(async function getAllIdentities() {
 | 
					tfrpc.register(async function getAllIdentities() {
 | 
				
			||||||
	return ssb.getAllIdentities();
 | 
						return ssb.getAllIdentities();
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					tfrpc.register(async function following(ids, depth) {
 | 
				
			||||||
 | 
						return ssb.following(ids, depth);
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
tfrpc.register(async function getBroadcasts() {
 | 
					tfrpc.register(async function getBroadcasts() {
 | 
				
			||||||
	return ssb.getBroadcasts();
 | 
						return ssb.getBroadcasts();
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
@@ -67,7 +76,7 @@ tfrpc.register(function getHash(id, message) {
 | 
				
			|||||||
tfrpc.register(function setHash(hash) {
 | 
					tfrpc.register(function setHash(hash) {
 | 
				
			||||||
	return app.setHash(hash);
 | 
						return app.setHash(hash);
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
ssb.addEventListener('message', async function(id) {
 | 
					core.register('onMessage', async function (id) {
 | 
				
			||||||
	await tfrpc.rpc.notifyNewMessage(id);
 | 
						await tfrpc.rpc.notifyNewMessage(id);
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
tfrpc.register(async function store_blob(blob) {
 | 
					tfrpc.register(async function store_blob(blob) {
 | 
				
			||||||
@@ -79,19 +88,34 @@ tfrpc.register(async function store_blob(blob) {
 | 
				
			|||||||
tfrpc.register(async function get_blob(id) {
 | 
					tfrpc.register(async function get_blob(id) {
 | 
				
			||||||
	return utf8Decode(await ssb.blobGet(id));
 | 
						return utf8Decode(await ssb.blobGet(id));
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					tfrpc.register(async function store_message(message) {
 | 
				
			||||||
 | 
						return await ssb.storeMessage(message);
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
tfrpc.register(function apps() {
 | 
					tfrpc.register(function apps() {
 | 
				
			||||||
	return core.apps();
 | 
						return core.apps();
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
ssb.addEventListener('broadcasts', async function() {
 | 
					tfrpc.register(async function try_decrypt(id, content) {
 | 
				
			||||||
 | 
						return await ssb.privateMessageDecrypt(id, content);
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					tfrpc.register(async function encrypt(id, recipients, content) {
 | 
				
			||||||
 | 
						return await ssb.privateMessageEncrypt(id, recipients, content);
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					tfrpc.register(async function getActiveIdentity() {
 | 
				
			||||||
 | 
						return await ssb.getActiveIdentity();
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					core.register('onBroadcastsChanged', async function () {
 | 
				
			||||||
	await tfrpc.rpc.set('broadcasts', await ssb.getBroadcasts());
 | 
						await tfrpc.rpc.set('broadcasts', await ssb.getBroadcasts());
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
core.register('onConnectionsChanged', async function() {
 | 
					core.register('onConnectionsChanged', async function () {
 | 
				
			||||||
	await tfrpc.rpc.set('connections', await ssb.connections());
 | 
						await tfrpc.rpc.set('connections', await ssb.connections());
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					core.register('setActiveIdentity', async function (id) {
 | 
				
			||||||
 | 
						await tfrpc.rpc.set('identity', id);
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
async function main() {
 | 
					async function main() {
 | 
				
			||||||
	if (typeof(database) !== 'undefined') {
 | 
						if (typeof database !== 'undefined') {
 | 
				
			||||||
		g_database = await database('ssb');
 | 
							g_database = await database('ssb');
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	await app.setDocument(utf8Decode(await getFile('index.html')));
 | 
						await app.setDocument(utf8Decode(await getFile('index.html')));
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,90 +1,94 @@
 | 
				
			|||||||
function textNode(text) {
 | 
					function textNode(text) {
 | 
				
			||||||
  const node = new commonmark.Node("text", undefined);
 | 
						const node = new commonmark.Node('text', undefined);
 | 
				
			||||||
  node.literal = text;
 | 
						node.literal = text;
 | 
				
			||||||
  return node;
 | 
						return node;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function linkNode(text, link) {
 | 
					function linkNode(text, link) {
 | 
				
			||||||
  const linkNode = new commonmark.Node("link", undefined);
 | 
						const linkNode = new commonmark.Node('link', undefined);
 | 
				
			||||||
  linkNode.destination = `#q=${encodeURIComponent(link)}`;
 | 
						if (link.startsWith('#')) {
 | 
				
			||||||
  linkNode.appendChild(textNode(text));
 | 
							linkNode.destination = `#q=${encodeURIComponent(link)}`;
 | 
				
			||||||
  return linkNode;
 | 
						} else {
 | 
				
			||||||
 | 
							linkNode.destination = link;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						linkNode.appendChild(textNode(text));
 | 
				
			||||||
 | 
						return linkNode;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function splitMatches(text, regexp) {
 | 
					function splitMatches(text, regexp) {
 | 
				
			||||||
  // Regexp must be sticky.
 | 
						// Regexp must be sticky.
 | 
				
			||||||
  regexp = new RegExp(regexp, "gm");
 | 
						regexp = new RegExp(regexp, 'gm');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  let i = 0;
 | 
						let i = 0;
 | 
				
			||||||
  const result = [];
 | 
						const result = [];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  let match = regexp.exec(text);
 | 
						let match = regexp.exec(text);
 | 
				
			||||||
  while (match) {
 | 
						while (match) {
 | 
				
			||||||
    const matchText = match[0];
 | 
							const matchText = match[0];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (match.index > i) {
 | 
							if (match.index > i) {
 | 
				
			||||||
      result.push([text.substring(i, match.index), false]);
 | 
								result.push([text.substring(i, match.index), false]);
 | 
				
			||||||
    }
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    result.push([matchText, true]);
 | 
							result.push([matchText, true]);
 | 
				
			||||||
    i = match.index + matchText.length;
 | 
							i = match.index + matchText.length;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    match = regexp.exec(text);
 | 
							match = regexp.exec(text);
 | 
				
			||||||
  }
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  if (i < text.length) {
 | 
						if (i < text.length) {
 | 
				
			||||||
    result.push([text.substring(i, text.length), false]);
 | 
							result.push([text.substring(i, text.length), false]);
 | 
				
			||||||
  }
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  return result;
 | 
						return result;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const regex = new RegExp("(?<!\w)#[\\w-]+");
 | 
					const regex = new RegExp('(?:https?://[^ ]+[^ .,])|(?:(?<!\\w)#[\\w-]+)|(?:@[A-Za-z0-9+/]+=.ed25519)|(?:[%&][A-Za-z0-9+/]+=.sha256)');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function split(textNodes) {
 | 
					function split(textNodes) {
 | 
				
			||||||
  const text = textNodes.map(n => n.literal).join("");
 | 
						const text = textNodes.map((n) => n.literal).join('');
 | 
				
			||||||
  const parts = splitMatches(text, regex);
 | 
						const parts = splitMatches(text, regex);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  return parts.map(part => {
 | 
						return parts.map((part) => {
 | 
				
			||||||
    if (part[1]) {
 | 
							if (part[1]) {
 | 
				
			||||||
      return linkNode(part[0], part[0]);
 | 
								return linkNode(part[0], part[0]);
 | 
				
			||||||
    } else {
 | 
							} else {
 | 
				
			||||||
      return textNode(part[0]);
 | 
								return textNode(part[0]);
 | 
				
			||||||
    }
 | 
							}
 | 
				
			||||||
  });
 | 
						});
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function transform(parsed) {
 | 
					export function transform(parsed) {
 | 
				
			||||||
  const walker = parsed.walker();
 | 
						const walker = parsed.walker();
 | 
				
			||||||
  let event;
 | 
						let event;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  let nodes = [];
 | 
						let nodes = [];
 | 
				
			||||||
  while ((event = walker.next())) {
 | 
						while ((event = walker.next())) {
 | 
				
			||||||
    const node = event.node;
 | 
							const node = event.node;
 | 
				
			||||||
    if (event.entering && node.type === "text") {
 | 
							if (event.entering && node.type === 'text') {
 | 
				
			||||||
      nodes.push(node);
 | 
								nodes.push(node);
 | 
				
			||||||
    } else {
 | 
							} else {
 | 
				
			||||||
      if (nodes.length > 0) {
 | 
								if (nodes.length > 0) {
 | 
				
			||||||
        split(nodes)
 | 
									split(nodes)
 | 
				
			||||||
          .reverse()
 | 
										.reverse()
 | 
				
			||||||
          .forEach(newNode => {
 | 
										.forEach((newNode) => {
 | 
				
			||||||
            nodes[0].insertAfter(newNode);
 | 
											nodes[0].insertAfter(newNode);
 | 
				
			||||||
          });
 | 
										});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        nodes.forEach(n => n.unlink());
 | 
									nodes.forEach((n) => n.unlink());
 | 
				
			||||||
        nodes = [];
 | 
									nodes = [];
 | 
				
			||||||
      }
 | 
								}
 | 
				
			||||||
    }
 | 
							}
 | 
				
			||||||
  }
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  if (nodes.length > 0) {
 | 
						if (nodes.length > 0) {
 | 
				
			||||||
    split(nodes)
 | 
							split(nodes)
 | 
				
			||||||
      .reverse()
 | 
								.reverse()
 | 
				
			||||||
      .forEach(newNode => {
 | 
								.forEach((newNode) => {
 | 
				
			||||||
        nodes[0].insertAfter(newNode);
 | 
									nodes[0].insertAfter(newNode);
 | 
				
			||||||
      });
 | 
								});
 | 
				
			||||||
    nodes.forEach(n => n.unlink());
 | 
							nodes.forEach((n) => n.unlink());
 | 
				
			||||||
  }
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  return parsed;
 | 
						return parsed;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,109 +1,164 @@
 | 
				
			|||||||
 | 
					import * as tfrpc from '/static/tfrpc.js';
 | 
				
			||||||
 | 
					import {html, render} from './lit-all.min.js';
 | 
				
			||||||
 | 
					import {styles} from './tf-styles.js';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
let g_emojis;
 | 
					let g_emojis;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function get_emojis() {
 | 
					function get_emojis() {
 | 
				
			||||||
	if (g_emojis) {
 | 
						if (g_emojis) {
 | 
				
			||||||
		return Promise.resolve(g_emojis);
 | 
							return Promise.resolve(g_emojis);
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	return fetch('emojis.json').then(function(result) {
 | 
						return fetch('emojis.json').then(function (result) {
 | 
				
			||||||
		g_emojis = result.json();
 | 
							g_emojis = result.json();
 | 
				
			||||||
		return g_emojis;
 | 
							return g_emojis;
 | 
				
			||||||
	});
 | 
						});
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function picker(callback, anchor) {
 | 
					async function get_recent(author) {
 | 
				
			||||||
	get_emojis().then(function(json) {
 | 
						let recent = await tfrpc.rpc.query(
 | 
				
			||||||
		let div = document.createElement('div');
 | 
							`
 | 
				
			||||||
		div.id = 'emoji_picker';
 | 
							SELECT DISTINCT content ->> '$.vote.expression' AS value
 | 
				
			||||||
		div.style.color = '#000';
 | 
							FROM messages
 | 
				
			||||||
		div.style.background = '#fff';
 | 
							WHERE author = ? AND
 | 
				
			||||||
		div.style.border = '1px solid #000';
 | 
							content ->> '$.type' = 'vote'
 | 
				
			||||||
		div.style.display = 'block';
 | 
							ORDER BY timestamp DESC LIMIT 10
 | 
				
			||||||
		div.style.position = 'absolute';
 | 
						`,
 | 
				
			||||||
		div.style.minWidth = 'min(16em, 90vw)';
 | 
							[author]
 | 
				
			||||||
		div.style.width = 'min(16em, 90vw)';
 | 
						);
 | 
				
			||||||
		div.style.maxWidth = 'min(16em, 90vw)';
 | 
						return recent.map((x) => x.value);
 | 
				
			||||||
		div.style.maxHeight = '16em';
 | 
					}
 | 
				
			||||||
		div.style.overflow = 'scroll';
 | 
					
 | 
				
			||||||
		div.style.fontWeight = 'bold';
 | 
					export async function picker(callback, anchor, author) {
 | 
				
			||||||
		div.style.fontSize = 'xx-large';
 | 
						let json = await get_emojis();
 | 
				
			||||||
		let input = document.createElement('input');
 | 
						let recent = await get_recent(author);
 | 
				
			||||||
		input.type = 'text';
 | 
					
 | 
				
			||||||
		input.style.display = 'block';
 | 
						let div = document.createElement('div');
 | 
				
			||||||
		input.style.boxSizing = 'border-box';
 | 
						div.id = 'emoji_picker';
 | 
				
			||||||
		input.style.width = '100%';
 | 
						div.style.color = '#000';
 | 
				
			||||||
		input.style.margin = '0';
 | 
						div.style.background = '#fff';
 | 
				
			||||||
		input.style.position = 'relative';
 | 
						div.style.border = '1px solid #000';
 | 
				
			||||||
		div.appendChild(input);
 | 
						div.style.display = 'block';
 | 
				
			||||||
		let list = document.createElement('div');
 | 
						div.style.overflow = 'scroll';
 | 
				
			||||||
		div.appendChild(list);
 | 
						div.style.fontWeight = 'bold';
 | 
				
			||||||
		div.addEventListener('mousedown', function(event) {
 | 
						div.style.fontSize = 'xx-large';
 | 
				
			||||||
			event.stopPropagation();
 | 
						let input = document.createElement('input');
 | 
				
			||||||
		});
 | 
						input.type = 'text';
 | 
				
			||||||
 | 
						input.style.display = 'block';
 | 
				
			||||||
		function cleanup() {
 | 
						input.style.boxSizing = 'border-box';
 | 
				
			||||||
			console.log('emoji cleanup');
 | 
						input.style.width = '100%';
 | 
				
			||||||
			div.parentElement.removeChild(div);
 | 
						input.style.margin = '0';
 | 
				
			||||||
			window.removeEventListener('keydown', key_down);
 | 
						input.style.position = 'relative';
 | 
				
			||||||
			console.log('removing click');
 | 
						div.appendChild(input);
 | 
				
			||||||
			document.body.removeEventListener('mousedown', cleanup);
 | 
						let list = document.createElement('div');
 | 
				
			||||||
		}
 | 
						div.appendChild(list);
 | 
				
			||||||
 | 
						div.addEventListener('mousedown', function (event) {
 | 
				
			||||||
		function key_down(event) {
 | 
							event.stopPropagation();
 | 
				
			||||||
			if (event.key == 'Escape') {
 | 
						});
 | 
				
			||||||
				cleanup();
 | 
					
 | 
				
			||||||
			}
 | 
						function key_down(event) {
 | 
				
			||||||
		}
 | 
							if (event.key == 'Escape') {
 | 
				
			||||||
 | 
								cleanup();
 | 
				
			||||||
		function refresh() {
 | 
							}
 | 
				
			||||||
			while (list.firstChild) {
 | 
						}
 | 
				
			||||||
				list.removeChild(list.firstChild);
 | 
					
 | 
				
			||||||
			}
 | 
						function chosen(event) {
 | 
				
			||||||
			let search = input.value;
 | 
							console.log(event.srcElement.innerText);
 | 
				
			||||||
			let any_at_all = false;
 | 
							callback(event.srcElement.innerText);
 | 
				
			||||||
			Object.entries(json).forEach(function(row) {
 | 
							cleanup();
 | 
				
			||||||
				let header = document.createElement('div');
 | 
						}
 | 
				
			||||||
				header.appendChild(document.createTextNode(row[0]));
 | 
					
 | 
				
			||||||
				list.appendChild(header);
 | 
						function refresh() {
 | 
				
			||||||
				let any = false;
 | 
							while (list.firstChild) {
 | 
				
			||||||
				for (let entry of row[1]) {
 | 
								list.removeChild(list.firstChild);
 | 
				
			||||||
					if (search &&
 | 
							}
 | 
				
			||||||
						search.length &&
 | 
							let search = input.value.toLowerCase();
 | 
				
			||||||
						entry.name.indexOf(search) == -1) {
 | 
							let any_at_all = false;
 | 
				
			||||||
						continue;
 | 
							if (recent) {
 | 
				
			||||||
					}
 | 
								let emoji_to_name = {};
 | 
				
			||||||
					let emoji = document.createElement('span');
 | 
								for (let row of Object.values(json)) {
 | 
				
			||||||
					const k_size = '1.25em';
 | 
									for (let entry of Object.entries(row)) {
 | 
				
			||||||
					emoji.style.display = 'inline-block';
 | 
										emoji_to_name[entry[1]] = entry[0];
 | 
				
			||||||
					emoji.style.overflow = 'hidden';
 | 
									}
 | 
				
			||||||
					emoji.style.cursor = 'pointer';
 | 
								}
 | 
				
			||||||
					emoji.onclick = function() {
 | 
								let header = document.createElement('div');
 | 
				
			||||||
						callback(entry);
 | 
								header.appendChild(document.createTextNode('Recent'));
 | 
				
			||||||
						cleanup();
 | 
								list.appendChild(header);
 | 
				
			||||||
					}
 | 
								let any = false;
 | 
				
			||||||
					emoji.title = entry.name;
 | 
								for (let entry of recent) {
 | 
				
			||||||
					emoji.appendChild(document.createTextNode(entry.emoji));
 | 
									if (
 | 
				
			||||||
					list.appendChild(emoji);
 | 
										search &&
 | 
				
			||||||
					any = true;
 | 
										search.length &&
 | 
				
			||||||
					any_at_all = true;
 | 
										(emoji_to_name[entry] || '').toLowerCase().indexOf(search) == -1
 | 
				
			||||||
				}
 | 
									) {
 | 
				
			||||||
				if (!any) {
 | 
										continue;
 | 
				
			||||||
					list.removeChild(header);
 | 
									}
 | 
				
			||||||
				}
 | 
									let emoji = document.createElement('span');
 | 
				
			||||||
			});
 | 
									const k_size = '1.25em';
 | 
				
			||||||
			if (!any_at_all) {
 | 
									emoji.style.display = 'inline-block';
 | 
				
			||||||
				list.appendChild(document.createTextNode('No matches found.'));
 | 
									emoji.style.overflow = 'hidden';
 | 
				
			||||||
			}
 | 
									emoji.style.cursor = 'pointer';
 | 
				
			||||||
		}
 | 
									emoji.onclick = chosen;
 | 
				
			||||||
		refresh();
 | 
									emoji.title = emoji_to_name[entry] || entry;
 | 
				
			||||||
		input.oninput = refresh;
 | 
									emoji.appendChild(document.createTextNode(entry));
 | 
				
			||||||
		document.body.appendChild(div);
 | 
									list.appendChild(emoji);
 | 
				
			||||||
		div.style.position = 'fixed';
 | 
									any = true;
 | 
				
			||||||
		div.style.top = '50%';
 | 
								}
 | 
				
			||||||
		div.style.left = '50%';
 | 
								if (!any) {
 | 
				
			||||||
		div.style.transform = 'translate(-50%, -50%)';
 | 
									list.removeChild(header);
 | 
				
			||||||
		input.focus();
 | 
								}
 | 
				
			||||||
		console.log('adding click');
 | 
							}
 | 
				
			||||||
		document.body.addEventListener('mousedown', cleanup);
 | 
							for (let row of Object.entries(json)) {
 | 
				
			||||||
		window.addEventListener('keydown', key_down);
 | 
								let header = document.createElement('div');
 | 
				
			||||||
	});
 | 
								header.appendChild(document.createTextNode(row[0]));
 | 
				
			||||||
 | 
								list.appendChild(header);
 | 
				
			||||||
 | 
								let any = false;
 | 
				
			||||||
 | 
								for (let entry of Object.entries(row[1])) {
 | 
				
			||||||
 | 
									if (
 | 
				
			||||||
 | 
										search &&
 | 
				
			||||||
 | 
										search.length &&
 | 
				
			||||||
 | 
										entry[0].toLowerCase().indexOf(search) == -1
 | 
				
			||||||
 | 
									) {
 | 
				
			||||||
 | 
										continue;
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
									let emoji = document.createElement('span');
 | 
				
			||||||
 | 
									const k_size = '1.25em';
 | 
				
			||||||
 | 
									emoji.style.display = 'inline-block';
 | 
				
			||||||
 | 
									emoji.style.overflow = 'hidden';
 | 
				
			||||||
 | 
									emoji.style.cursor = 'pointer';
 | 
				
			||||||
 | 
									emoji.onclick = chosen;
 | 
				
			||||||
 | 
									emoji.title = entry[0];
 | 
				
			||||||
 | 
									emoji.appendChild(document.createTextNode(entry[1]));
 | 
				
			||||||
 | 
									list.appendChild(emoji);
 | 
				
			||||||
 | 
									any = true;
 | 
				
			||||||
 | 
									any_at_all = true;
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								if (!any) {
 | 
				
			||||||
 | 
									list.removeChild(header);
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							if (!any_at_all) {
 | 
				
			||||||
 | 
								list.appendChild(document.createTextNode('No matches found.'));
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						refresh();
 | 
				
			||||||
 | 
						input.oninput = refresh;
 | 
				
			||||||
 | 
						let modal = html`
 | 
				
			||||||
 | 
							<style>
 | 
				
			||||||
 | 
								${styles}
 | 
				
			||||||
 | 
							</style>
 | 
				
			||||||
 | 
							<div class="w3-modal" style="display: block">
 | 
				
			||||||
 | 
								<div class="w3-modal-content w3-card-4">${div}</div>
 | 
				
			||||||
 | 
							</div>
 | 
				
			||||||
 | 
						`;
 | 
				
			||||||
 | 
						let parent = document.createElement('div');
 | 
				
			||||||
 | 
						document.body.appendChild(parent);
 | 
				
			||||||
 | 
						function cleanup() {
 | 
				
			||||||
 | 
							parent.parentElement.removeChild(parent);
 | 
				
			||||||
 | 
							window.removeEventListener('keydown', key_down);
 | 
				
			||||||
 | 
							document.body.removeEventListener('mousedown', cleanup);
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						render(modal, parent);
 | 
				
			||||||
 | 
						input.focus();
 | 
				
			||||||
 | 
						document.body.addEventListener('mousedown', cleanup);
 | 
				
			||||||
 | 
						window.addEventListener('keydown', key_down);
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
							
								
								
									
										15116
									
								
								apps/ssb/emojis.json
									
									
									
									
									
								
							
							
						
						
									
										15116
									
								
								apps/ssb/emojis.json
									
									
									
									
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
							
								
								
									
										3
									
								
								apps/ssb/filesaver.min.js
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								apps/ssb/filesaver.min.js
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@@ -0,0 +1,3 @@
 | 
				
			|||||||
 | 
					(function(a,b){if("function"==typeof define&&define.amd)define([],b);else if("undefined"!=typeof exports)b();else{b(),a.FileSaver={exports:{}}.exports}})(this,function(){"use strict";function b(a,b){return"undefined"==typeof b?b={autoBom:!1}:"object"!=typeof b&&(console.warn("Deprecated: Expected third argument to be a object"),b={autoBom:!b}),b.autoBom&&/^\s*(?:text\/\S*|application\/xml|\S*\/\S*\+xml)\s*;.*charset\s*=\s*utf-8/i.test(a.type)?new Blob(["\uFEFF",a],{type:a.type}):a}function c(a,b,c){var d=new XMLHttpRequest;d.open("GET",a),d.responseType="blob",d.onload=function(){g(d.response,b,c)},d.onerror=function(){console.error("could not download file")},d.send()}function d(a){var b=new XMLHttpRequest;b.open("HEAD",a,!1);try{b.send()}catch(a){}return 200<=b.status&&299>=b.status}function e(a){try{a.dispatchEvent(new MouseEvent("click"))}catch(c){var b=document.createEvent("MouseEvents");b.initMouseEvent("click",!0,!0,window,0,0,0,80,20,!1,!1,!1,!1,0,null),a.dispatchEvent(b)}}var f="object"==typeof window&&window.window===window?window:"object"==typeof self&&self.self===self?self:"object"==typeof global&&global.global===global?global:void 0,a=/Macintosh/.test(navigator.userAgent)&&/AppleWebKit/.test(navigator.userAgent)&&!/Safari/.test(navigator.userAgent),g=f.saveAs||("object"!=typeof window||window!==f?function(){}:"download"in HTMLAnchorElement.prototype&&!a?function(b,g,h){var i=f.URL||f.webkitURL,j=document.createElement("a");g=g||b.name||"download",j.download=g,j.rel="noopener","string"==typeof b?(j.href=b,j.origin===location.origin?e(j):d(j.href)?c(b,g,h):e(j,j.target="_blank")):(j.href=i.createObjectURL(b),setTimeout(function(){i.revokeObjectURL(j.href)},4E4),setTimeout(function(){e(j)},0))}:"msSaveOrOpenBlob"in navigator?function(f,g,h){if(g=g||f.name||"download","string"!=typeof f)navigator.msSaveOrOpenBlob(b(f,h),g);else if(d(f))c(f,g,h);else{var i=document.createElement("a");i.href=f,i.target="_blank",setTimeout(function(){e(i)})}}:function(b,d,e,g){if(g=g||open("","_blank"),g&&(g.document.title=g.document.body.innerText="downloading..."),"string"==typeof b)return c(b,d,e);var h="application/octet-stream"===b.type,i=/constructor/i.test(f.HTMLElement)||f.safari,j=/CriOS\/[\d]+/.test(navigator.userAgent);if((j||h&&i||a)&&"undefined"!=typeof FileReader){var k=new FileReader;k.onloadend=function(){var a=k.result;a=j?a:a.replace(/^data:[^;]*;/,"data:attachment/file;"),g?g.location.href=a:location=a,g=null},k.readAsDataURL(b)}else{var l=f.URL||f.webkitURL,m=l.createObjectURL(b);g?g.location=m:location.href=m,g=null,setTimeout(function(){l.revokeObjectURL(m)},4E4)}});f.saveAs=g.saveAs=g,"undefined"!=typeof module&&(module.exports=g)});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					//# sourceMappingURL=FileSaver.min.js.map
 | 
				
			||||||
							
								
								
									
										1
									
								
								apps/ssb/filesaver.min.js.map
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								apps/ssb/filesaver.min.js.map
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							@@ -1,8 +1,8 @@
 | 
				
			|||||||
<!DOCTYPE html>
 | 
					<!doctype html>
 | 
				
			||||||
<html style="color: #fff">
 | 
					<html>
 | 
				
			||||||
	<head>
 | 
						<head>
 | 
				
			||||||
		<title>Tilde Friends</title>
 | 
							<title>Tilde Friends</title>
 | 
				
			||||||
		<base target="_top">
 | 
							<base target="_top" />
 | 
				
			||||||
		<link rel="stylesheet" href="tribute.css" />
 | 
							<link rel="stylesheet" href="tribute.css" />
 | 
				
			||||||
		<style>
 | 
							<style>
 | 
				
			||||||
			.tribute-container {
 | 
								.tribute-container {
 | 
				
			||||||
@@ -10,11 +10,14 @@
 | 
				
			|||||||
			}
 | 
								}
 | 
				
			||||||
		</style>
 | 
							</style>
 | 
				
			||||||
	</head>
 | 
						</head>
 | 
				
			||||||
	<body>
 | 
						<body style="margin: 0; padding: 0">
 | 
				
			||||||
		<tf-app/>
 | 
							<tf-app></tf-app>
 | 
				
			||||||
		<script>window.litDisableBundleWarning = true;</script>
 | 
							<tf-reactions-modal id="reactions_modal"></tf-reactions-modal>
 | 
				
			||||||
 | 
							<script>
 | 
				
			||||||
 | 
								window.litDisableBundleWarning = true;
 | 
				
			||||||
 | 
							</script>
 | 
				
			||||||
 | 
							<script src="filesaver.min.js"></script>
 | 
				
			||||||
		<script src="commonmark.min.js"></script>
 | 
							<script src="commonmark.min.js"></script>
 | 
				
			||||||
		<script src="commonmark-linkify.js" type="module"></script>
 | 
					 | 
				
			||||||
		<script src="commonmark-hashtag.js" type="module"></script>
 | 
							<script src="commonmark-hashtag.js" type="module"></script>
 | 
				
			||||||
		<script src="script.js" type="module"></script>
 | 
							<script src="script.js" type="module"></script>
 | 
				
			||||||
	</body>
 | 
						</body>
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										56
									
								
								apps/ssb/lit-all.min.js
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										56
									
								
								apps/ssb/lit-all.min.js
									
									
									
									
										vendored
									
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							@@ -1,13 +1,17 @@
 | 
				
			|||||||
import {LitElement, html} from './lit-all.min.js';
 | 
					import {LitElement, html} from './lit-all.min.js';
 | 
				
			||||||
import * as tfrpc from '/static/tfrpc.js';
 | 
					import * as tfrpc from '/static/tfrpc.js';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import * as tf_id_picker from './tf-id-picker.js';
 | 
					 | 
				
			||||||
import * as tf_app from './tf-app.js';
 | 
					import * as tf_app from './tf-app.js';
 | 
				
			||||||
import * as tf_message from './tf-message.js';
 | 
					import * as tf_message from './tf-message.js';
 | 
				
			||||||
import * as tf_user from './tf-user.js';
 | 
					import * as tf_user from './tf-user.js';
 | 
				
			||||||
import * as tf_compose from './tf-compose.js';
 | 
					import * as tf_compose from './tf-compose.js';
 | 
				
			||||||
import * as tf_news from './tf-news.js';
 | 
					import * as tf_news from './tf-news.js';
 | 
				
			||||||
import * as tf_profile from './tf-profile.js';
 | 
					import * as tf_profile from './tf-profile.js';
 | 
				
			||||||
 | 
					import * as tf_reactions_modal from './tf-reactions-modal.js';
 | 
				
			||||||
 | 
					import * as tf_tab_mentions from './tf-tab-mentions.js';
 | 
				
			||||||
import * as tf_tab_news from './tf-tab-news.js';
 | 
					import * as tf_tab_news from './tf-tab-news.js';
 | 
				
			||||||
 | 
					import * as tf_tab_news_feed from './tf-tab-news-feed.js';
 | 
				
			||||||
import * as tf_tab_search from './tf-tab-search.js';
 | 
					import * as tf_tab_search from './tf-tab-search.js';
 | 
				
			||||||
import * as tf_tab_connections from './tf-tab-connections.js';
 | 
					import * as tf_tab_connections from './tf-tab-connections.js';
 | 
				
			||||||
 | 
					import * as tf_tab_query from './tf-tab-query.js';
 | 
				
			||||||
 | 
					import * as tf_tag from './tf-tag.js';
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -16,6 +16,7 @@ class TfElement extends LitElement {
 | 
				
			|||||||
			following: {type: Array},
 | 
								following: {type: Array},
 | 
				
			||||||
			users: {type: Object},
 | 
								users: {type: Object},
 | 
				
			||||||
			ids: {type: Array},
 | 
								ids: {type: Array},
 | 
				
			||||||
 | 
								tags: {type: Array},
 | 
				
			||||||
		};
 | 
							};
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -32,9 +33,14 @@ class TfElement extends LitElement {
 | 
				
			|||||||
		this.following = [];
 | 
							this.following = [];
 | 
				
			||||||
		this.users = {};
 | 
							this.users = {};
 | 
				
			||||||
		this.loaded = false;
 | 
							this.loaded = false;
 | 
				
			||||||
		tfrpc.rpc.getBroadcasts().then(b => { self.broadcasts = b || [] });
 | 
							this.tags = [];
 | 
				
			||||||
		tfrpc.rpc.getConnections().then(c => { self.connections = c || [] });
 | 
							tfrpc.rpc.getBroadcasts().then((b) => {
 | 
				
			||||||
		tfrpc.rpc.getHash().then(hash => self.set_hash(hash));
 | 
								self.broadcasts = b || [];
 | 
				
			||||||
 | 
							});
 | 
				
			||||||
 | 
							tfrpc.rpc.getConnections().then((c) => {
 | 
				
			||||||
 | 
								self.connections = c || [];
 | 
				
			||||||
 | 
							});
 | 
				
			||||||
 | 
							tfrpc.rpc.getHash().then((hash) => self.set_hash(hash));
 | 
				
			||||||
		tfrpc.register(function hashChanged(hash) {
 | 
							tfrpc.register(function hashChanged(hash) {
 | 
				
			||||||
			self.set_hash(hash);
 | 
								self.set_hash(hash);
 | 
				
			||||||
		});
 | 
							});
 | 
				
			||||||
@@ -46,13 +52,15 @@ class TfElement extends LitElement {
 | 
				
			|||||||
				self.broadcasts = value;
 | 
									self.broadcasts = value;
 | 
				
			||||||
			} else if (name === 'connections') {
 | 
								} else if (name === 'connections') {
 | 
				
			||||||
				self.connections = value;
 | 
									self.connections = value;
 | 
				
			||||||
 | 
								} else if (name === 'identity') {
 | 
				
			||||||
 | 
									self.whoami = value;
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
		});
 | 
							});
 | 
				
			||||||
		this.initial_load();
 | 
							this.initial_load();
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	async initial_load() {
 | 
						async initial_load() {
 | 
				
			||||||
		let whoami = await tfrpc.rpc.localStorageGet('whoami');
 | 
							let whoami = await tfrpc.rpc.getActiveIdentity();
 | 
				
			||||||
		let ids = (await tfrpc.rpc.getIdentities()) || [];
 | 
							let ids = (await tfrpc.rpc.getIdentities()) || [];
 | 
				
			||||||
		this.whoami = whoami ?? (ids.length ? ids[0] : undefined);
 | 
							this.whoami = whoami ?? (ids.length ? ids[0] : undefined);
 | 
				
			||||||
		this.ids = ids;
 | 
							this.ids = ids;
 | 
				
			||||||
@@ -64,83 +72,15 @@ class TfElement extends LitElement {
 | 
				
			|||||||
			this.tab = 'search';
 | 
								this.tab = 'search';
 | 
				
			||||||
		} else if (this.hash === '#connections') {
 | 
							} else if (this.hash === '#connections') {
 | 
				
			||||||
			this.tab = 'connections';
 | 
								this.tab = 'connections';
 | 
				
			||||||
 | 
							} else if (this.hash === '#mentions') {
 | 
				
			||||||
 | 
								this.tab = 'mentions';
 | 
				
			||||||
 | 
							} else if (this.hash.startsWith('#sql=')) {
 | 
				
			||||||
 | 
								this.tab = 'query';
 | 
				
			||||||
		} else {
 | 
							} else {
 | 
				
			||||||
			this.tab = 'news';
 | 
								this.tab = 'news';
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	async contacts_internal(id, last_row_id, following, max_row_id) {
 | 
					 | 
				
			||||||
		let result = Object.assign({}, following[id] || {});
 | 
					 | 
				
			||||||
		result.following = result.following || {};
 | 
					 | 
				
			||||||
		result.blocking = result.blocking || {};
 | 
					 | 
				
			||||||
		let contacts = await tfrpc.rpc.query(
 | 
					 | 
				
			||||||
			`
 | 
					 | 
				
			||||||
				SELECT content FROM messages
 | 
					 | 
				
			||||||
				WHERE author = ? AND
 | 
					 | 
				
			||||||
				rowid > ? AND
 | 
					 | 
				
			||||||
				rowid <= ? AND
 | 
					 | 
				
			||||||
				json_extract(content, '$.type') = 'contact'
 | 
					 | 
				
			||||||
				ORDER BY sequence
 | 
					 | 
				
			||||||
			`,
 | 
					 | 
				
			||||||
			[id, last_row_id, max_row_id]);
 | 
					 | 
				
			||||||
		for (let row of contacts) {
 | 
					 | 
				
			||||||
			let contact = JSON.parse(row.content);
 | 
					 | 
				
			||||||
			if (contact.following === true) {
 | 
					 | 
				
			||||||
				result.following[contact.contact] = true;
 | 
					 | 
				
			||||||
			} else if (contact.following === false) {
 | 
					 | 
				
			||||||
				delete result.following[contact.contact];
 | 
					 | 
				
			||||||
			} else if (contact.blocking === true) {
 | 
					 | 
				
			||||||
				result.blocking[contact.contact] = true;
 | 
					 | 
				
			||||||
			} else if (contact.blocking === false) {
 | 
					 | 
				
			||||||
				delete result.blocking[contact.contact];
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
		following[id] = result;
 | 
					 | 
				
			||||||
		return result;
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	async contact(id, last_row_id, following, max_row_id) {
 | 
					 | 
				
			||||||
		return await this.contacts_internal(id, last_row_id, following, max_row_id);
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	async following_deep_internal(ids, depth, blocking, last_row_id, following, max_row_id) {
 | 
					 | 
				
			||||||
		let contacts = await Promise.all([...new Set(ids)].map(x => this.contact(x, last_row_id, following, max_row_id)));
 | 
					 | 
				
			||||||
		let result = {};
 | 
					 | 
				
			||||||
		for (let i = 0; i < ids.length; i++) {
 | 
					 | 
				
			||||||
			let id = ids[i];
 | 
					 | 
				
			||||||
			let contact = contacts[i];
 | 
					 | 
				
			||||||
			let all_blocking = Object.assign({}, contact.blocking, blocking);
 | 
					 | 
				
			||||||
			let found = Object.keys(contact.following).filter(y => !all_blocking[y]);
 | 
					 | 
				
			||||||
			let deeper = depth > 1 ? await this.following_deep_internal(found, depth - 1, all_blocking, last_row_id, following, max_row_id) : [];
 | 
					 | 
				
			||||||
			result[id] = [id, ...found, ...deeper];
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
		return [...new Set(Object.values(result).flat())];
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	async following_deep(ids, depth, blocking) {
 | 
					 | 
				
			||||||
		const k_cache_version = 5;
 | 
					 | 
				
			||||||
		let cache = await tfrpc.rpc.databaseGet('following');
 | 
					 | 
				
			||||||
		cache = cache ? JSON.parse(cache) : {};
 | 
					 | 
				
			||||||
		if (cache.version !== k_cache_version) {
 | 
					 | 
				
			||||||
			cache = {
 | 
					 | 
				
			||||||
				version: k_cache_version,
 | 
					 | 
				
			||||||
				following: {},
 | 
					 | 
				
			||||||
				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 result = await this.following_deep_internal(ids, depth, blocking, cache.last_row_id, cache.following, max_row_id);
 | 
					 | 
				
			||||||
		cache.last_row_id = max_row_id;
 | 
					 | 
				
			||||||
		let store = JSON.stringify(cache);
 | 
					 | 
				
			||||||
		/* 2023-02-20: Exceeding message size. */
 | 
					 | 
				
			||||||
		//if (store.length < 512 * 1024) {
 | 
					 | 
				
			||||||
			await tfrpc.rpc.databaseSet('following', store);
 | 
					 | 
				
			||||||
		//}
 | 
					 | 
				
			||||||
		return [result, cache.following];
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	async fetch_about(ids, users) {
 | 
						async fetch_about(ids, users) {
 | 
				
			||||||
		const k_cache_version = 1;
 | 
							const k_cache_version = 1;
 | 
				
			||||||
		let cache = await tfrpc.rpc.databaseGet('about');
 | 
							let cache = await tfrpc.rpc.databaseGet('about');
 | 
				
			||||||
@@ -152,9 +92,14 @@ class TfElement extends LitElement {
 | 
				
			|||||||
				last_row_id: 0,
 | 
									last_row_id: 0,
 | 
				
			||||||
			};
 | 
								};
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
		let max_row_id = (await tfrpc.rpc.query(`
 | 
							let max_row_id = (
 | 
				
			||||||
 | 
								await tfrpc.rpc.query(
 | 
				
			||||||
 | 
									`
 | 
				
			||||||
			SELECT MAX(rowid) AS max_row_id FROM messages
 | 
								SELECT MAX(rowid) AS max_row_id FROM messages
 | 
				
			||||||
		`, []))[0].max_row_id;
 | 
							`,
 | 
				
			||||||
 | 
									[]
 | 
				
			||||||
 | 
								)
 | 
				
			||||||
 | 
							)[0].max_row_id;
 | 
				
			||||||
		for (let id of Object.keys(cache.about)) {
 | 
							for (let id of Object.keys(cache.about)) {
 | 
				
			||||||
			if (ids.indexOf(id) == -1) {
 | 
								if (ids.indexOf(id) == -1) {
 | 
				
			||||||
				delete cache.about[id];
 | 
									delete cache.about[id];
 | 
				
			||||||
@@ -164,7 +109,7 @@ class TfElement extends LitElement {
 | 
				
			|||||||
		let abouts = await tfrpc.rpc.query(
 | 
							let abouts = await tfrpc.rpc.query(
 | 
				
			||||||
			`
 | 
								`
 | 
				
			||||||
				SELECT
 | 
									SELECT
 | 
				
			||||||
					messages.*
 | 
										messages.id, messages.previous, messages.author, messages.sequence, messages.timestamp, messages.hash, json(messages.content) AS content, messages.signature
 | 
				
			||||||
				FROM
 | 
									FROM
 | 
				
			||||||
					messages,
 | 
										messages,
 | 
				
			||||||
					json_each(?1) AS following
 | 
										json_each(?1) AS following
 | 
				
			||||||
@@ -175,7 +120,7 @@ class TfElement extends LitElement {
 | 
				
			|||||||
					json_extract(messages.content, '$.type') = 'about'
 | 
										json_extract(messages.content, '$.type') = 'about'
 | 
				
			||||||
				UNION
 | 
									UNION
 | 
				
			||||||
				SELECT
 | 
									SELECT
 | 
				
			||||||
					messages.*
 | 
										messages.id, messages.previous, messages.author, messages.sequence, messages.timestamp, messages.hash, json(messages.content) AS content, messages.signature
 | 
				
			||||||
				FROM
 | 
									FROM
 | 
				
			||||||
					messages,
 | 
										messages,
 | 
				
			||||||
					json_each(?2) AS following
 | 
										json_each(?2) AS following
 | 
				
			||||||
@@ -186,17 +131,21 @@ class TfElement extends LitElement {
 | 
				
			|||||||
				ORDER BY messages.author, messages.sequence
 | 
									ORDER BY messages.author, messages.sequence
 | 
				
			||||||
			`,
 | 
								`,
 | 
				
			||||||
			[
 | 
								[
 | 
				
			||||||
				JSON.stringify(ids.filter(id => cache.about[id])),
 | 
									JSON.stringify(ids.filter((id) => cache.about[id])),
 | 
				
			||||||
				JSON.stringify(ids.filter(id => !cache.about[id])),
 | 
									JSON.stringify(ids.filter((id) => !cache.about[id])),
 | 
				
			||||||
				cache.last_row_id,
 | 
									cache.last_row_id,
 | 
				
			||||||
				max_row_id,
 | 
									max_row_id,
 | 
				
			||||||
			]);
 | 
								]
 | 
				
			||||||
 | 
							);
 | 
				
			||||||
		for (let about of abouts) {
 | 
							for (let about of abouts) {
 | 
				
			||||||
			let content = JSON.parse(about.content);
 | 
								let content = JSON.parse(about.content);
 | 
				
			||||||
			if (content.about === about.author) {
 | 
								if (content.about === about.author) {
 | 
				
			||||||
				delete content.type;
 | 
									delete content.type;
 | 
				
			||||||
				delete content.about;
 | 
									delete content.about;
 | 
				
			||||||
				cache.about[about.author] = Object.assign(cache.about[about.author] || {}, content);
 | 
									cache.about[about.author] = Object.assign(
 | 
				
			||||||
 | 
										cache.about[about.author] || {},
 | 
				
			||||||
 | 
										content
 | 
				
			||||||
 | 
									);
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
		cache.last_row_id = max_row_id;
 | 
							cache.last_row_id = max_row_id;
 | 
				
			||||||
@@ -211,17 +160,16 @@ class TfElement extends LitElement {
 | 
				
			|||||||
	async fetch_new_message(id) {
 | 
						async fetch_new_message(id) {
 | 
				
			||||||
		let messages = await tfrpc.rpc.query(
 | 
							let messages = await tfrpc.rpc.query(
 | 
				
			||||||
			`
 | 
								`
 | 
				
			||||||
				SELECT messages.*
 | 
									SELECT messages.id, previous, author, sequence, timestamp, hash, json(content) AS content, signature
 | 
				
			||||||
				FROM messages
 | 
									FROM messages
 | 
				
			||||||
				JOIN json_each(?) AS following ON messages.author = following.value
 | 
									JOIN json_each(?) AS following ON messages.author = following.value
 | 
				
			||||||
				WHERE messages.id = ?
 | 
									WHERE messages.id = ?
 | 
				
			||||||
			`,
 | 
								`,
 | 
				
			||||||
			[
 | 
								[JSON.stringify(this.following), id]
 | 
				
			||||||
				JSON.stringify(this.following),
 | 
							);
 | 
				
			||||||
				id,
 | 
					 | 
				
			||||||
			]);
 | 
					 | 
				
			||||||
		if (messages && messages.length) {
 | 
							if (messages && messages.length) {
 | 
				
			||||||
			this.unread = [...this.unread, ...messages];
 | 
								this.unread = [...this.unread, ...messages];
 | 
				
			||||||
 | 
								this.unread = this.unread.slice(this.unread.length - 1024);
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -238,25 +186,66 @@ class TfElement extends LitElement {
 | 
				
			|||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	async create_identity() {
 | 
						async create_identity() {
 | 
				
			||||||
		if (confirm("Are you sure you want to create a new identity?")) {
 | 
							if (confirm('Are you sure you want to create a new identity?')) {
 | 
				
			||||||
			await tfrpc.rpc.createIdentity();
 | 
								await tfrpc.rpc.createIdentity();
 | 
				
			||||||
			this.ids = (await tfrpc.rpc.getIdentities()) || [];
 | 
								this.ids = (await tfrpc.rpc.getIdentities()) || [];
 | 
				
			||||||
 | 
								if (this.ids && !this.whoami) {
 | 
				
			||||||
 | 
									this.whoami = this.ids[0];
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	render_id_picker() {
 | 
						async load_recent_tags() {
 | 
				
			||||||
		return html`
 | 
							let start = new Date();
 | 
				
			||||||
			<tf-id-picker id="picker" selected=${this.whoami} .ids=${this.ids} @change=${this._handle_whoami_changed}></tf-id-picker>
 | 
							this.tags = await tfrpc.rpc.query(
 | 
				
			||||||
			<button @click=${this.create_identity}>Create Identity</button>
 | 
								`
 | 
				
			||||||
		`;
 | 
								WITH
 | 
				
			||||||
 | 
									recent AS (SELECT id, json(content) AS content FROM messages
 | 
				
			||||||
 | 
										WHERE messages.timestamp > ? AND json_extract(content, '$.type') = 'post'
 | 
				
			||||||
 | 
										ORDER BY timestamp DESC LIMIT 1024),
 | 
				
			||||||
 | 
									recent_channels AS (SELECT recent.id, '#' || json_extract(content, '$.channel') AS tag
 | 
				
			||||||
 | 
										FROM recent
 | 
				
			||||||
 | 
										WHERE json_extract(content, '$.channel') IS NOT NULL),
 | 
				
			||||||
 | 
									recent_mentions AS (SELECT recent.id, json_extract(mention.value, '$.link') AS tag
 | 
				
			||||||
 | 
										FROM recent, json_each(recent.content, '$.mentions') AS mention
 | 
				
			||||||
 | 
										WHERE json_valid(mention.value) AND tag LIKE '#%'),
 | 
				
			||||||
 | 
									combined AS (SELECT id, tag FROM recent_channels UNION ALL SELECT id, tag FROM recent_mentions),
 | 
				
			||||||
 | 
									by_message AS (SELECT DISTINCT id, tag FROM combined)
 | 
				
			||||||
 | 
								SELECT tag, COUNT(*) AS count FROM by_message GROUP BY tag ORDER BY count DESC LIMIT 10
 | 
				
			||||||
 | 
							`,
 | 
				
			||||||
 | 
								[new Date() - 7 * 24 * 60 * 60 * 1000]
 | 
				
			||||||
 | 
							);
 | 
				
			||||||
 | 
							console.log('tags took', (new Date() - start) / 1000.0, 'seconds');
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	async load() {
 | 
						async load() {
 | 
				
			||||||
		let whoami = this.whoami;
 | 
							let whoami = this.whoami;
 | 
				
			||||||
		let [following, users] = await this.following_deep([whoami], 2, {});
 | 
							let tags = this.load_recent_tags();
 | 
				
			||||||
		users = await this.fetch_about(following.sort(), users);
 | 
							let following = await tfrpc.rpc.following([whoami], 2);
 | 
				
			||||||
		this.following = following;
 | 
							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});
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							console.log(by_count.sort((x, y) => y.count - x.count).slice(0, 20));
 | 
				
			||||||
 | 
							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'
 | 
				
			||||||
 | 
							);
 | 
				
			||||||
 | 
							this.following = Object.keys(following);
 | 
				
			||||||
		this.users = users;
 | 
							this.users = users;
 | 
				
			||||||
 | 
							await tags;
 | 
				
			||||||
		console.log(`load finished ${whoami} => ${this.whoami}`);
 | 
							console.log(`load finished ${whoami} => ${this.whoami}`);
 | 
				
			||||||
		this.whoami = whoami;
 | 
							this.whoami = whoami;
 | 
				
			||||||
		this.loaded = whoami;
 | 
							this.loaded = whoami;
 | 
				
			||||||
@@ -267,15 +256,54 @@ class TfElement extends LitElement {
 | 
				
			|||||||
		let users = this.users;
 | 
							let users = this.users;
 | 
				
			||||||
		if (this.tab === 'news') {
 | 
							if (this.tab === 'news') {
 | 
				
			||||||
			return html`
 | 
								return html`
 | 
				
			||||||
				<tf-tab-news .following=${this.following} whoami=${this.whoami} .users=${this.users} hash=${this.hash} .unread=${this.unread} @refresh=${() => this.unread = []}></tf-tab-news>
 | 
									<tf-tab-news
 | 
				
			||||||
 | 
										id="tf-tab-news"
 | 
				
			||||||
 | 
										.following=${this.following}
 | 
				
			||||||
 | 
										whoami=${this.whoami}
 | 
				
			||||||
 | 
										.users=${this.users}
 | 
				
			||||||
 | 
										hash=${this.hash}
 | 
				
			||||||
 | 
										.unread=${this.unread}
 | 
				
			||||||
 | 
										@refresh=${() => (this.unread = [])}
 | 
				
			||||||
 | 
										?loading=${this.loading}
 | 
				
			||||||
 | 
									></tf-tab-news>
 | 
				
			||||||
			`;
 | 
								`;
 | 
				
			||||||
		} else if (this.tab === 'connections') {
 | 
							} else if (this.tab === 'connections') {
 | 
				
			||||||
			return html`
 | 
								return html`
 | 
				
			||||||
				<tf-tab-connections .users=${this.users} .connections=${this.connections} .broadcasts=${this.broadcasts}></tf-tab-connections>
 | 
									<tf-tab-connections
 | 
				
			||||||
 | 
										.users=${this.users}
 | 
				
			||||||
 | 
										.connections=${this.connections}
 | 
				
			||||||
 | 
										.broadcasts=${this.broadcasts}
 | 
				
			||||||
 | 
									></tf-tab-connections>
 | 
				
			||||||
 | 
								`;
 | 
				
			||||||
 | 
							} else if (this.tab === 'mentions') {
 | 
				
			||||||
 | 
								return html`
 | 
				
			||||||
 | 
									<tf-tab-mentions
 | 
				
			||||||
 | 
										.following=${this.following}
 | 
				
			||||||
 | 
										whoami=${this.whoami}
 | 
				
			||||||
 | 
										.users="${this.users}}"
 | 
				
			||||||
 | 
									></tf-tab-mentions>
 | 
				
			||||||
			`;
 | 
								`;
 | 
				
			||||||
		} else if (this.tab === 'search') {
 | 
							} else if (this.tab === 'search') {
 | 
				
			||||||
			return html`
 | 
								return html`
 | 
				
			||||||
				<tf-tab-search .following=${this.following} whoami=${this.whoami} .users=${this.users} query=${this.hash?.startsWith('#q=') ? decodeURIComponent(this.hash.substring(3)) : null}></tf-tab-search>
 | 
									<tf-tab-search
 | 
				
			||||||
 | 
										.following=${this.following}
 | 
				
			||||||
 | 
										whoami=${this.whoami}
 | 
				
			||||||
 | 
										.users=${this.users}
 | 
				
			||||||
 | 
										query=${this.hash?.startsWith('#q=')
 | 
				
			||||||
 | 
											? decodeURIComponent(this.hash.substring(3))
 | 
				
			||||||
 | 
											: 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>
 | 
				
			||||||
			`;
 | 
								`;
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
@@ -286,6 +314,10 @@ class TfElement extends LitElement {
 | 
				
			|||||||
			await tfrpc.rpc.setHash('#');
 | 
								await tfrpc.rpc.setHash('#');
 | 
				
			||||||
		} else if (tab === 'connections') {
 | 
							} else if (tab === 'connections') {
 | 
				
			||||||
			await tfrpc.rpc.setHash('#connections');
 | 
								await tfrpc.rpc.setHash('#connections');
 | 
				
			||||||
 | 
							} else if (tab === 'mentions') {
 | 
				
			||||||
 | 
								await tfrpc.rpc.setHash('#mentions');
 | 
				
			||||||
 | 
							} else if (tab === 'query') {
 | 
				
			||||||
 | 
								await tfrpc.rpc.setHash('#sql=');
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -293,30 +325,63 @@ class TfElement extends LitElement {
 | 
				
			|||||||
		let self = this;
 | 
							let self = this;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		if (!this.loading && this.whoami && this.loaded !== this.whoami) {
 | 
							if (!this.loading && this.whoami && this.loaded !== this.whoami) {
 | 
				
			||||||
			console.log(`starting loading ${this.whoami} ${this.loaded}`);
 | 
					 | 
				
			||||||
			this.loading = true;
 | 
								this.loading = true;
 | 
				
			||||||
			this.load().finally(function() {
 | 
								this.load().finally(function () {
 | 
				
			||||||
				self.loading = false;
 | 
									self.loading = false;
 | 
				
			||||||
			});
 | 
								});
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							const k_tabs = {
 | 
				
			||||||
 | 
								'📰': 'news',
 | 
				
			||||||
 | 
								'📡': 'connections',
 | 
				
			||||||
 | 
								'@': 'mentions',
 | 
				
			||||||
 | 
								'🔍': 'search',
 | 
				
			||||||
 | 
								'👩💻': 'query',
 | 
				
			||||||
 | 
							};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		let tabs = html`
 | 
							let tabs = html`
 | 
				
			||||||
			<div>
 | 
								<div class="w3-bar w3-theme-l1">
 | 
				
			||||||
				<input type="button" class="tab" value="News" ?disabled=${self.tab == 'news'} @click=${() => self.set_tab('news')}></input>
 | 
									${Object.entries(k_tabs).map(
 | 
				
			||||||
				<input type="button" class="tab" value="Connections" ?disabled=${self.tab == 'connections'} @click=${() => self.set_tab('connections')}></input>
 | 
										([k, v]) => html`
 | 
				
			||||||
				<input type="button" class="tab" value="Search" ?disabled=${self.tab == 'search'} @click=${() => self.set_tab('search')}></input>
 | 
											<button
 | 
				
			||||||
 | 
												title=${v}
 | 
				
			||||||
 | 
												class="w3-bar-item w3-padding w3-hover-theme tab ${self.tab == v
 | 
				
			||||||
 | 
													? 'w3-theme-l2'
 | 
				
			||||||
 | 
													: 'w3-theme-l1'}"
 | 
				
			||||||
 | 
												@click=${() => self.set_tab(v)}
 | 
				
			||||||
 | 
											>
 | 
				
			||||||
 | 
												${k}
 | 
				
			||||||
 | 
												<span class=${self.tab == v ? '' : 'w3-hide-small'}
 | 
				
			||||||
 | 
													>${v.charAt(0).toUpperCase() + v.substring(1)}</span
 | 
				
			||||||
 | 
												>
 | 
				
			||||||
 | 
											</button>
 | 
				
			||||||
 | 
										`
 | 
				
			||||||
 | 
									)}
 | 
				
			||||||
			</div>
 | 
								</div>
 | 
				
			||||||
		`;
 | 
							`;
 | 
				
			||||||
		let contents =
 | 
							let contents = !this.loaded
 | 
				
			||||||
				!this.loaded ?
 | 
								? this.loading
 | 
				
			||||||
					this.loading ?
 | 
									? html`<div
 | 
				
			||||||
						html`<div>Loading...</div>` :
 | 
												class="w3-panel w3-theme-l5 w3-card-4 w3-padding-large w3-round-xlarge"
 | 
				
			||||||
						html`<div>Select or create an identity.</div>` :
 | 
											>
 | 
				
			||||||
					this.render_tab();
 | 
												Loading...
 | 
				
			||||||
 | 
											</div>
 | 
				
			||||||
 | 
											${this.render_tab()}`
 | 
				
			||||||
 | 
									: html`<div>Select or create an identity.</div>`
 | 
				
			||||||
 | 
								: this.render_tab();
 | 
				
			||||||
		return html`
 | 
							return html`
 | 
				
			||||||
			${this.render_id_picker()}
 | 
								<div
 | 
				
			||||||
			${tabs}
 | 
									style="width: 100vw; min-height: 100vh; height: 100%"
 | 
				
			||||||
			${contents}
 | 
									class="w3-theme-dark"
 | 
				
			||||||
 | 
								>
 | 
				
			||||||
 | 
									${tabs}
 | 
				
			||||||
 | 
									<div style="padding: 8px">
 | 
				
			||||||
 | 
										${this.tags.map(
 | 
				
			||||||
 | 
											(x) => html`<tf-tag tag=${x.tag} count=${x.count}></tf-tag>`
 | 
				
			||||||
 | 
										)}
 | 
				
			||||||
 | 
										${contents}
 | 
				
			||||||
 | 
									</div>
 | 
				
			||||||
 | 
								</div>
 | 
				
			||||||
		`;
 | 
							`;
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,4 +1,4 @@
 | 
				
			|||||||
import {LitElement, html, unsafeHTML} from './lit-all.min.js';
 | 
					import {LitElement, html, unsafeHTML, live} from './lit-all.min.js';
 | 
				
			||||||
import * as tfutils from './tf-utils.js';
 | 
					import * as tfutils from './tf-utils.js';
 | 
				
			||||||
import * as tfrpc from '/static/tfrpc.js';
 | 
					import * as tfrpc from '/static/tfrpc.js';
 | 
				
			||||||
import {styles} from './tf-styles.js';
 | 
					import {styles} from './tf-styles.js';
 | 
				
			||||||
@@ -13,7 +13,8 @@ class TfComposeElement extends LitElement {
 | 
				
			|||||||
			branch: {type: String},
 | 
								branch: {type: String},
 | 
				
			||||||
			apps: {type: Object},
 | 
								apps: {type: Object},
 | 
				
			||||||
			drafts: {type: Object},
 | 
								drafts: {type: Object},
 | 
				
			||||||
		}
 | 
								author: {type: String},
 | 
				
			||||||
 | 
							};
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	static styles = styles;
 | 
						static styles = styles;
 | 
				
			||||||
@@ -25,6 +26,7 @@ class TfComposeElement extends LitElement {
 | 
				
			|||||||
		this.branch = undefined;
 | 
							this.branch = undefined;
 | 
				
			||||||
		this.apps = undefined;
 | 
							this.apps = undefined;
 | 
				
			||||||
		this.drafts = {};
 | 
							this.drafts = {};
 | 
				
			||||||
 | 
							this.author = undefined;
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	process_text(text) {
 | 
						process_text(text) {
 | 
				
			||||||
@@ -56,13 +58,15 @@ class TfComposeElement extends LitElement {
 | 
				
			|||||||
			if (!draft.mentions[link]) {
 | 
								if (!draft.mentions[link]) {
 | 
				
			||||||
				draft.mentions[link] = {
 | 
									draft.mentions[link] = {
 | 
				
			||||||
					link: link,
 | 
										link: link,
 | 
				
			||||||
				}
 | 
									};
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
			draft.mentions[link].name = name.startsWith('@') ? name.substring(1) : name;
 | 
								draft.mentions[link].name = name.startsWith('@')
 | 
				
			||||||
 | 
									? name.substring(1)
 | 
				
			||||||
 | 
									: name;
 | 
				
			||||||
			updated = true;
 | 
								updated = true;
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
		if (updated) {
 | 
							if (updated) {
 | 
				
			||||||
			this.requestUpdate();
 | 
								setTimeout(() => this.notify(draft), 0);
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
		return tfutils.markdown(text);
 | 
							return tfutils.markdown(text);
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
@@ -70,36 +74,37 @@ class TfComposeElement extends LitElement {
 | 
				
			|||||||
	input(event) {
 | 
						input(event) {
 | 
				
			||||||
		let edit = this.renderRoot.getElementById('edit');
 | 
							let edit = this.renderRoot.getElementById('edit');
 | 
				
			||||||
		let preview = this.renderRoot.getElementById('preview');
 | 
							let preview = this.renderRoot.getElementById('preview');
 | 
				
			||||||
		preview.innerHTML = this.process_text(edit.value);
 | 
							preview.innerHTML = this.process_text(edit.innerText);
 | 
				
			||||||
		let content_warning = this.renderRoot.getElementById('content_warning');
 | 
							let content_warning = this.renderRoot.getElementById('content_warning');
 | 
				
			||||||
		let content_warning_preview = this.renderRoot.getElementById('content_warning_preview');
 | 
							let content_warning_preview = this.renderRoot.getElementById(
 | 
				
			||||||
 | 
								'content_warning_preview'
 | 
				
			||||||
 | 
							);
 | 
				
			||||||
		if (content_warning && content_warning_preview) {
 | 
							if (content_warning && content_warning_preview) {
 | 
				
			||||||
			content_warning_preview.innerText = content_warning.value;
 | 
								content_warning_preview.innerText = content_warning.value;
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
							let draft = this.get_draft();
 | 
				
			||||||
 | 
							draft.text = edit.innerText;
 | 
				
			||||||
 | 
							draft.content_warning = content_warning?.innerText;
 | 
				
			||||||
 | 
							setTimeout(() => this.notify(draft), 0);
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	notify(draft) {
 | 
						notify(draft) {
 | 
				
			||||||
		this.dispatchEvent(new CustomEvent('tf-draft', {
 | 
							this.dispatchEvent(
 | 
				
			||||||
			bubbles: true,
 | 
								new CustomEvent('tf-draft', {
 | 
				
			||||||
			composed: true,
 | 
									bubbles: true,
 | 
				
			||||||
			detail: {
 | 
									composed: true,
 | 
				
			||||||
				id: this.branch,
 | 
									detail: {
 | 
				
			||||||
				draft: draft
 | 
										id: this.branch,
 | 
				
			||||||
			},
 | 
										draft: draft,
 | 
				
			||||||
		}));
 | 
									},
 | 
				
			||||||
	}
 | 
								})
 | 
				
			||||||
 | 
							);
 | 
				
			||||||
	change() {
 | 
					 | 
				
			||||||
		let draft = this.get_draft();
 | 
					 | 
				
			||||||
		draft.text = this.renderRoot.getElementById('edit')?.value;
 | 
					 | 
				
			||||||
		draft.content_warning = this.renderRoot.getElementById('content_warning')?.value;
 | 
					 | 
				
			||||||
		this.notify(draft);
 | 
					 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	convert_to_format(buffer, type, mime_type) {
 | 
						convert_to_format(buffer, type, mime_type) {
 | 
				
			||||||
		return new Promise(function(resolve, reject) {
 | 
							return new Promise(function (resolve, reject) {
 | 
				
			||||||
			let img = new Image();
 | 
								let img = new Image();
 | 
				
			||||||
			img.onload = function() {
 | 
								img.onload = function () {
 | 
				
			||||||
				let canvas = document.createElement('canvas');
 | 
									let canvas = document.createElement('canvas');
 | 
				
			||||||
				let width_scale = Math.min(img.width, 1024) / img.width;
 | 
									let width_scale = Math.min(img.width, 1024) / img.width;
 | 
				
			||||||
				let height_scale = Math.min(img.height, 1024) / img.height;
 | 
									let height_scale = Math.min(img.height, 1024) / img.height;
 | 
				
			||||||
@@ -109,13 +114,17 @@ class TfComposeElement extends LitElement {
 | 
				
			|||||||
				let context = canvas.getContext('2d');
 | 
									let context = canvas.getContext('2d');
 | 
				
			||||||
				context.drawImage(img, 0, 0, canvas.width, canvas.height);
 | 
									context.drawImage(img, 0, 0, canvas.width, canvas.height);
 | 
				
			||||||
				let data_url = canvas.toDataURL(mime_type);
 | 
									let data_url = canvas.toDataURL(mime_type);
 | 
				
			||||||
				let result = atob(data_url.split(',')[1]).split('').map(x => x.charCodeAt(0));
 | 
									let result = atob(data_url.split(',')[1])
 | 
				
			||||||
 | 
										.split('')
 | 
				
			||||||
 | 
										.map((x) => x.charCodeAt(0));
 | 
				
			||||||
				resolve(result);
 | 
									resolve(result);
 | 
				
			||||||
			}
 | 
								};
 | 
				
			||||||
			img.onerror = function(event) {
 | 
								img.onerror = function (event) {
 | 
				
			||||||
				reject(new Error('Failed to load image.'));
 | 
									reject(new Error('Failed to load image.'));
 | 
				
			||||||
			};
 | 
								};
 | 
				
			||||||
			let raw = Array.from(new Uint8Array(buffer)).map(b => String.fromCharCode(b)).join('');
 | 
								let raw = Array.from(new Uint8Array(buffer))
 | 
				
			||||||
 | 
									.map((b) => String.fromCharCode(b))
 | 
				
			||||||
 | 
									.join('');
 | 
				
			||||||
			let original = `data:${type};base64,${btoa(raw)}`;
 | 
								let original = `data:${type};base64,${btoa(raw)}`;
 | 
				
			||||||
			img.src = original;
 | 
								img.src = original;
 | 
				
			||||||
		});
 | 
							});
 | 
				
			||||||
@@ -131,7 +140,11 @@ class TfComposeElement extends LitElement {
 | 
				
			|||||||
				let best_buffer;
 | 
									let best_buffer;
 | 
				
			||||||
				let best_type;
 | 
									let best_type;
 | 
				
			||||||
				for (let format of ['image/png', 'image/jpeg', 'image/webp']) {
 | 
									for (let format of ['image/png', 'image/jpeg', 'image/webp']) {
 | 
				
			||||||
					let test_buffer = await self.convert_to_format(buffer, file.type, format);
 | 
										let test_buffer = await self.convert_to_format(
 | 
				
			||||||
 | 
											buffer,
 | 
				
			||||||
 | 
											file.type,
 | 
				
			||||||
 | 
											format
 | 
				
			||||||
 | 
										);
 | 
				
			||||||
					if (!best_buffer || test_buffer.length < best_buffer.length) {
 | 
										if (!best_buffer || test_buffer.length < best_buffer.length) {
 | 
				
			||||||
						best_buffer = test_buffer;
 | 
											best_buffer = test_buffer;
 | 
				
			||||||
						best_type = format;
 | 
											best_type = format;
 | 
				
			||||||
@@ -154,10 +167,9 @@ class TfComposeElement extends LitElement {
 | 
				
			|||||||
				size: buffer.length ?? buffer.byteLength,
 | 
									size: buffer.length ?? buffer.byteLength,
 | 
				
			||||||
			};
 | 
								};
 | 
				
			||||||
			let edit = self.renderRoot.getElementById('edit');
 | 
								let edit = self.renderRoot.getElementById('edit');
 | 
				
			||||||
			edit.value += `\n`;
 | 
								edit.innerText += `\n`;
 | 
				
			||||||
			self.change();
 | 
					 | 
				
			||||||
			self.input();
 | 
								self.input();
 | 
				
			||||||
		} catch(e) {
 | 
							} catch (e) {
 | 
				
			||||||
			alert(e?.message);
 | 
								alert(e?.message);
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
@@ -176,13 +188,13 @@ class TfComposeElement extends LitElement {
 | 
				
			|||||||
		}
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	submit() {
 | 
						async submit() {
 | 
				
			||||||
		let self = this;
 | 
							let self = this;
 | 
				
			||||||
		let draft = this.get_draft();
 | 
							let draft = this.get_draft();
 | 
				
			||||||
		let edit = this.renderRoot.getElementById('edit');
 | 
							let edit = this.renderRoot.getElementById('edit');
 | 
				
			||||||
		let message = {
 | 
							let message = {
 | 
				
			||||||
			type: 'post',
 | 
								type: 'post',
 | 
				
			||||||
			text: edit.value,
 | 
								text: edit.innerText,
 | 
				
			||||||
		};
 | 
							};
 | 
				
			||||||
		if (this.root || this.branch) {
 | 
							if (this.root || this.branch) {
 | 
				
			||||||
			message.root = this.root;
 | 
								message.root = this.root;
 | 
				
			||||||
@@ -195,43 +207,110 @@ class TfComposeElement extends LitElement {
 | 
				
			|||||||
			message.contentWarning = draft.content_warning;
 | 
								message.contentWarning = draft.content_warning;
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
		console.log('Would post:', message);
 | 
							console.log('Would post:', message);
 | 
				
			||||||
		tfrpc.rpc.appendMessage(this.whoami, message).then(function() {
 | 
							if (draft.encrypt_to) {
 | 
				
			||||||
			edit.value = '';
 | 
								let to = new Set(draft.encrypt_to);
 | 
				
			||||||
			self.change();
 | 
								to.add(this.whoami);
 | 
				
			||||||
			self.notify(undefined);
 | 
								to = [...to];
 | 
				
			||||||
			self.requestUpdate();
 | 
								message.recps = to;
 | 
				
			||||||
		}).catch(function(error) {
 | 
								console.log('message is now', message);
 | 
				
			||||||
 | 
								message = await tfrpc.rpc.encrypt(
 | 
				
			||||||
 | 
									this.whoami,
 | 
				
			||||||
 | 
									to,
 | 
				
			||||||
 | 
									JSON.stringify(message)
 | 
				
			||||||
 | 
								);
 | 
				
			||||||
 | 
								console.log('encrypted as', message);
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							try {
 | 
				
			||||||
 | 
								await tfrpc.rpc.appendMessage(this.whoami, message).then(function () {
 | 
				
			||||||
 | 
									edit.innerText = '';
 | 
				
			||||||
 | 
									self.input();
 | 
				
			||||||
 | 
									self.notify(undefined);
 | 
				
			||||||
 | 
									self.requestUpdate();
 | 
				
			||||||
 | 
								});
 | 
				
			||||||
 | 
							} catch (error) {
 | 
				
			||||||
			alert(error.message);
 | 
								alert(error.message);
 | 
				
			||||||
		});
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	discard() {
 | 
						discard() {
 | 
				
			||||||
		let edit = this.renderRoot.getElementById('edit');
 | 
					 | 
				
			||||||
		edit.value = '';
 | 
					 | 
				
			||||||
		this.change();
 | 
					 | 
				
			||||||
		let preview = this.renderRoot.getElementById('preview');
 | 
					 | 
				
			||||||
		preview.innerHTML = '';
 | 
					 | 
				
			||||||
		this.notify(undefined);
 | 
							this.notify(undefined);
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	attach() {
 | 
						attach() {
 | 
				
			||||||
		let self = this;
 | 
							let self = this;
 | 
				
			||||||
		let edit = this.renderRoot.getElementById('edit');
 | 
					 | 
				
			||||||
		let input = document.createElement('input');
 | 
							let input = document.createElement('input');
 | 
				
			||||||
		input.type = 'file';
 | 
							input.type = 'file';
 | 
				
			||||||
		input.onchange = function(event) {
 | 
							input.onchange = function (event) {
 | 
				
			||||||
			let file = event.target.files[0];
 | 
								let file = event.target.files[0];
 | 
				
			||||||
			self.add_file(file);
 | 
								self.add_file(file);
 | 
				
			||||||
		};
 | 
							};
 | 
				
			||||||
		input.click();
 | 
							input.click();
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						async autocomplete(text, callback) {
 | 
				
			||||||
 | 
							this.last_autocomplete = text;
 | 
				
			||||||
 | 
							let results = [];
 | 
				
			||||||
 | 
							try {
 | 
				
			||||||
 | 
								let rows = await tfrpc.rpc.query(
 | 
				
			||||||
 | 
									`
 | 
				
			||||||
 | 
									SELECT json(messages.content) AS content FROM messages_fts(?)
 | 
				
			||||||
 | 
									JOIN messages ON messages.rowid = messages_fts.rowid
 | 
				
			||||||
 | 
									WHERE json(messages.content) LIKE ?
 | 
				
			||||||
 | 
									ORDER BY timestamp DESC LIMIT 10
 | 
				
			||||||
 | 
								`,
 | 
				
			||||||
 | 
									['"' + text.replace('"', '""') + '"', `%%`]
 | 
				
			||||||
 | 
								);
 | 
				
			||||||
 | 
								for (let row of rows) {
 | 
				
			||||||
 | 
									for (let match of row.content.matchAll(/!\[([^\]]*)\]\((&.*?)\)/g)) {
 | 
				
			||||||
 | 
										if (match[1].toLowerCase().indexOf(text.toLowerCase()) != -1) {
 | 
				
			||||||
 | 
											results.push({key: match[1], value: match[2]});
 | 
				
			||||||
 | 
										}
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							} finally {
 | 
				
			||||||
 | 
								if (this.last_autocomplete === text) {
 | 
				
			||||||
 | 
									callback(results);
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	firstUpdated() {
 | 
						firstUpdated() {
 | 
				
			||||||
 | 
							let values = Object.entries(this.users).map((x) => ({
 | 
				
			||||||
 | 
								key: x[1].name ?? x[0],
 | 
				
			||||||
 | 
								value: x[0],
 | 
				
			||||||
 | 
							}));
 | 
				
			||||||
 | 
							if (this.author) {
 | 
				
			||||||
 | 
								values = [].concat(
 | 
				
			||||||
 | 
									[
 | 
				
			||||||
 | 
										{
 | 
				
			||||||
 | 
											key: this.users[this.author]?.name,
 | 
				
			||||||
 | 
											value: this.author,
 | 
				
			||||||
 | 
										},
 | 
				
			||||||
 | 
									],
 | 
				
			||||||
 | 
									values
 | 
				
			||||||
 | 
								);
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
		let tribute = new Tribute({
 | 
							let tribute = new Tribute({
 | 
				
			||||||
			values: Object.entries(this.users).map(x => ({key: x[1].name, value: x[0]})),
 | 
								iframe: this.shadowRoot,
 | 
				
			||||||
			selectTemplate: function(item) {
 | 
								collection: [
 | 
				
			||||||
				return `[@${item.original.key}](${item.original.value})`;
 | 
									{
 | 
				
			||||||
			},
 | 
										values: values,
 | 
				
			||||||
 | 
										selectTemplate: function (item) {
 | 
				
			||||||
 | 
											return item
 | 
				
			||||||
 | 
												? `[@${item.original.key}](${item.original.value})`
 | 
				
			||||||
 | 
												: undefined;
 | 
				
			||||||
 | 
										},
 | 
				
			||||||
 | 
									},
 | 
				
			||||||
 | 
									{
 | 
				
			||||||
 | 
										trigger: '&',
 | 
				
			||||||
 | 
										values: this.autocomplete,
 | 
				
			||||||
 | 
										selectTemplate: function (item) {
 | 
				
			||||||
 | 
											return item
 | 
				
			||||||
 | 
												? ``
 | 
				
			||||||
 | 
												: undefined;
 | 
				
			||||||
 | 
										},
 | 
				
			||||||
 | 
									},
 | 
				
			||||||
 | 
								],
 | 
				
			||||||
		});
 | 
							});
 | 
				
			||||||
		tribute.attach(this.renderRoot.getElementById('edit'));
 | 
							tribute.attach(this.renderRoot.getElementById('edit'));
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
@@ -239,27 +318,59 @@ class TfComposeElement extends LitElement {
 | 
				
			|||||||
	updated() {
 | 
						updated() {
 | 
				
			||||||
		super.updated();
 | 
							super.updated();
 | 
				
			||||||
		let edit = this.renderRoot.getElementById('edit');
 | 
							let edit = this.renderRoot.getElementById('edit');
 | 
				
			||||||
		if (this.last_updated_text !== edit.value) {
 | 
							if (this.last_updated_text !== edit.innerText) {
 | 
				
			||||||
			let preview = this.renderRoot.getElementById('preview');
 | 
								let preview = this.renderRoot.getElementById('preview');
 | 
				
			||||||
			preview.innerHTML = this.process_text(edit.value);
 | 
								preview.innerHTML = this.process_text(edit.innerText);
 | 
				
			||||||
			this.last_updated_text = edit.value;
 | 
								this.last_updated_text = edit.innerText;
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							let encrypt = this.renderRoot.getElementById('encrypt_to');
 | 
				
			||||||
 | 
							if (encrypt) {
 | 
				
			||||||
 | 
								let tribute = new Tribute({
 | 
				
			||||||
 | 
									iframe: this.shadowRoot,
 | 
				
			||||||
 | 
									values: Object.entries(this.users).map((x) => ({
 | 
				
			||||||
 | 
										key: x[1].name,
 | 
				
			||||||
 | 
										value: x[0],
 | 
				
			||||||
 | 
									})),
 | 
				
			||||||
 | 
									selectTemplate: function (item) {
 | 
				
			||||||
 | 
										return item.original.value;
 | 
				
			||||||
 | 
									},
 | 
				
			||||||
 | 
								});
 | 
				
			||||||
 | 
								tribute.attach(encrypt);
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	remove_mention(id) {
 | 
						remove_mention(id) {
 | 
				
			||||||
		let draft = this.get_draft();
 | 
							let draft = this.get_draft();
 | 
				
			||||||
		delete draft.mentions[id];
 | 
							delete draft.mentions[id];
 | 
				
			||||||
		this.notify(draft);
 | 
							setTimeout(() => this.notify(), 0);
 | 
				
			||||||
		this.requestUpdate();
 | 
					 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	render_mention(mention) {
 | 
						render_mention(mention) {
 | 
				
			||||||
		let self = this;
 | 
							let self = this;
 | 
				
			||||||
		return html`
 | 
							return html` <div style="display: flex; flex-direction: row">
 | 
				
			||||||
			<div>
 | 
								<div style="align-self: center; margin: 0.5em">
 | 
				
			||||||
				<pre style="white-space: pre-wrap">${JSON.stringify(mention, null, 2)}</pre>
 | 
									<button
 | 
				
			||||||
				<input type="button" value="x" @click=${() => self.remove_mention(mention.link)}></input>
 | 
										class="w3-button w3-theme-d1"
 | 
				
			||||||
			</div>`;
 | 
										title="Remove ${mention.name} mention"
 | 
				
			||||||
 | 
										@click=${() => self.remove_mention(mention.link)}
 | 
				
			||||||
 | 
									>
 | 
				
			||||||
 | 
										🚮
 | 
				
			||||||
 | 
									</button>
 | 
				
			||||||
 | 
								</div>
 | 
				
			||||||
 | 
								<div style="display: flex; flex-direction: column">
 | 
				
			||||||
 | 
									<h3>${mention.name}</h3>
 | 
				
			||||||
 | 
									<div style="padding-left: 1em">
 | 
				
			||||||
 | 
										${Object.entries(mention)
 | 
				
			||||||
 | 
											.filter((x) => x[0] != 'name')
 | 
				
			||||||
 | 
											.map(
 | 
				
			||||||
 | 
												(x) =>
 | 
				
			||||||
 | 
													html`<div>
 | 
				
			||||||
 | 
														<span style="font-weight: bold">${x[0]}</span>: ${x[1]}
 | 
				
			||||||
 | 
													</div>`
 | 
				
			||||||
 | 
											)}
 | 
				
			||||||
 | 
									</div>
 | 
				
			||||||
 | 
								</div>
 | 
				
			||||||
 | 
							</div>`;
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	render_attach_app() {
 | 
						render_attach_app() {
 | 
				
			||||||
@@ -283,34 +394,51 @@ class TfComposeElement extends LitElement {
 | 
				
			|||||||
					};
 | 
										};
 | 
				
			||||||
				}
 | 
									}
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
			let draft = this.get_draft();
 | 
								let draft = self.get_draft();
 | 
				
			||||||
			draft.mentions = Object.assign(draft.mentions || {}, mentions);
 | 
								draft.mentions = Object.assign(draft.mentions || {}, mentions);
 | 
				
			||||||
			this.requestUpdate();
 | 
								self.requestUpdate();
 | 
				
			||||||
			this.notify(draft);
 | 
								self.notify(draft);
 | 
				
			||||||
			this.apps = null;
 | 
								self.apps = null;
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		if (this.apps) {
 | 
							if (this.apps) {
 | 
				
			||||||
			return html`
 | 
								return html`
 | 
				
			||||||
				<div>
 | 
									<div class="w3-card-4 w3-margin w3-padding">
 | 
				
			||||||
					<select id="select">
 | 
										<select id="select" class="w3-select w3-theme-d1">
 | 
				
			||||||
						${Object.keys(self.apps).map(app => html`<option value=${app}>${app}</option>`)}
 | 
											${Object.keys(self.apps).map(
 | 
				
			||||||
 | 
												(app) => html`<option value=${app}>${app}</option>`
 | 
				
			||||||
 | 
											)}
 | 
				
			||||||
					</select>
 | 
										</select>
 | 
				
			||||||
					<input type="button" value="Attach" @click=${attach_selected_app}></input>
 | 
										<button class="w3-button w3-theme-d1" @click=${attach_selected_app}>
 | 
				
			||||||
					<input type="button" value="Cancel" @click=${() => this.apps = null}></input>
 | 
											Attach
 | 
				
			||||||
 | 
										</button>
 | 
				
			||||||
 | 
										<button
 | 
				
			||||||
 | 
											class="w3-button w3-theme-d1"
 | 
				
			||||||
 | 
											@click=${() => (this.apps = null)}
 | 
				
			||||||
 | 
										>
 | 
				
			||||||
 | 
											Cancel
 | 
				
			||||||
 | 
										</button>
 | 
				
			||||||
				</div>
 | 
									</div>
 | 
				
			||||||
				`;
 | 
								`;
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	render_attach_app_button() {
 | 
						render_attach_app_button() {
 | 
				
			||||||
 | 
							let self = this;
 | 
				
			||||||
		async function attach_app() {
 | 
							async function attach_app() {
 | 
				
			||||||
			this.apps = await tfrpc.rpc.apps();
 | 
								self.apps = await tfrpc.rpc.apps();
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
		if (!this.apps) {
 | 
							if (!this.apps) {
 | 
				
			||||||
			return html`<input type="button" value="Attach App" @click=${attach_app}></input>`
 | 
								return html`<button class="w3-button w3-theme-d1" @click=${attach_app}>
 | 
				
			||||||
 | 
									Attach App
 | 
				
			||||||
 | 
								</button>`;
 | 
				
			||||||
		} else {
 | 
							} else {
 | 
				
			||||||
			return html`<input type="button" value="Discard App" @click=${() => this.apps = null}></input>`
 | 
								return html`<button
 | 
				
			||||||
 | 
									class="w3-button w3-theme-d1"
 | 
				
			||||||
 | 
									@click=${() => (this.apps = null)}
 | 
				
			||||||
 | 
								>
 | 
				
			||||||
 | 
									Discard App
 | 
				
			||||||
 | 
								</button>`;
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -326,15 +454,17 @@ class TfComposeElement extends LitElement {
 | 
				
			|||||||
		let draft = this.get_draft();
 | 
							let draft = this.get_draft();
 | 
				
			||||||
		if (draft.content_warning !== undefined) {
 | 
							if (draft.content_warning !== undefined) {
 | 
				
			||||||
			return html`
 | 
								return html`
 | 
				
			||||||
				<div>
 | 
									<div class="w3-container w3-padding">
 | 
				
			||||||
					<input type="checkbox" id="cw" @change=${() => self.set_content_warning(undefined)} checked></input>
 | 
										<p>
 | 
				
			||||||
					<label for="cw">CW</label>
 | 
											<input type="checkbox" class="w3-check w3-theme-d1" id="cw" @change=${() => self.set_content_warning(undefined)} checked="checked"></input>
 | 
				
			||||||
					<input type="text" id="content_warning" @input=${this.input} @change=${this.change} value=${draft.content_warning}></input>
 | 
											<label for="cw">CW</label>
 | 
				
			||||||
 | 
										</p>
 | 
				
			||||||
 | 
										<input type="text" class="w3-input w3-border w3-theme-d1" id="content_warning" placeholder="Enter a content warning here." @input=${this.input} @change=${this.change} value=${draft.content_warning}></input>
 | 
				
			||||||
				</div>
 | 
									</div>
 | 
				
			||||||
			`;
 | 
								`;
 | 
				
			||||||
		} else {
 | 
							} else {
 | 
				
			||||||
			return html`
 | 
								return html`
 | 
				
			||||||
				<input type="checkbox" id="cw" @change=${() => self.set_content_warning('')}></input>
 | 
									<input type="checkbox" class="w3-check w3-theme-d1" id="cw" @change=${() => self.set_content_warning('')}></input>
 | 
				
			||||||
				<label for="cw">CW</label>
 | 
									<label for="cw">CW</label>
 | 
				
			||||||
			`;
 | 
								`;
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
@@ -344,28 +474,104 @@ class TfComposeElement extends LitElement {
 | 
				
			|||||||
		return this.drafts[this.branch || ''] || {};
 | 
							return this.drafts[this.branch || ''] || {};
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						update_encrypt(event) {
 | 
				
			||||||
 | 
							let input = event.srcElement;
 | 
				
			||||||
 | 
							let matches = input.value.match(/@.*?\.ed25519/g);
 | 
				
			||||||
 | 
							if (matches) {
 | 
				
			||||||
 | 
								let draft = this.get_draft();
 | 
				
			||||||
 | 
								let to = [...new Set(matches.concat(draft.encrypt_to))];
 | 
				
			||||||
 | 
								this.set_encrypt(to);
 | 
				
			||||||
 | 
								input.value = '';
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						render_encrypt() {
 | 
				
			||||||
 | 
							let draft = this.get_draft();
 | 
				
			||||||
 | 
							if (draft.encrypt_to === undefined) {
 | 
				
			||||||
 | 
								return;
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							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>
 | 
				
			||||||
 | 
									<button class="w3-button w3-theme-d1" @click=${() => this.set_encrypt(undefined)}>🚮</button>
 | 
				
			||||||
 | 
								</div>
 | 
				
			||||||
 | 
								<ul>
 | 
				
			||||||
 | 
									${draft.encrypt_to.map(
 | 
				
			||||||
 | 
										(x) => html`
 | 
				
			||||||
 | 
										<li>
 | 
				
			||||||
 | 
											<tf-user id=${x} .users=${this.users}></tf-user>
 | 
				
			||||||
 | 
											<input type="button" class="w3-button w3-theme-d1" value="🚮" @click=${() => this.set_encrypt(draft.encrypt_to.filter((id) => id != x))}></input>
 | 
				
			||||||
 | 
										</li>`
 | 
				
			||||||
 | 
									)}
 | 
				
			||||||
 | 
								</ul>
 | 
				
			||||||
 | 
							`;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						set_encrypt(encrypt) {
 | 
				
			||||||
 | 
							let draft = this.get_draft();
 | 
				
			||||||
 | 
							draft.encrypt_to = encrypt;
 | 
				
			||||||
 | 
							this.notify(draft);
 | 
				
			||||||
 | 
							this.requestUpdate();
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	render() {
 | 
						render() {
 | 
				
			||||||
		let self = this;
 | 
							let self = this;
 | 
				
			||||||
		let draft = self.get_draft();
 | 
							let draft = self.get_draft();
 | 
				
			||||||
		let content_warning =
 | 
							let content_warning =
 | 
				
			||||||
			draft.content_warning !== undefined ?
 | 
								draft.content_warning !== undefined
 | 
				
			||||||
			html`<div id="content_warning_preview" class="content_warning">${draft.content_warning}</div>` :
 | 
									? html`<div class="w3-panel w3-round-xlarge w3-theme-d2">
 | 
				
			||||||
			undefined;
 | 
											<p id="content_warning_preview">${draft.content_warning}</p>
 | 
				
			||||||
 | 
										</div>`
 | 
				
			||||||
 | 
									: undefined;
 | 
				
			||||||
 | 
							let encrypt =
 | 
				
			||||||
 | 
								draft.encrypt_to !== undefined
 | 
				
			||||||
 | 
									? undefined
 | 
				
			||||||
 | 
									: html`<button
 | 
				
			||||||
 | 
											class="w3-button w3-theme-d1"
 | 
				
			||||||
 | 
											@click=${() => this.set_encrypt([])}
 | 
				
			||||||
 | 
										>
 | 
				
			||||||
 | 
											🔐
 | 
				
			||||||
 | 
										</button>`;
 | 
				
			||||||
		let result = html`
 | 
							let result = html`
 | 
				
			||||||
			<div style="display: flex; flex-direction: row; width: 100%">
 | 
								<div
 | 
				
			||||||
				<textarea id="edit" @input=${this.input} @change=${this.change} @paste=${this.paste} style="flex: 1 0 50%">${draft.text}</textarea>
 | 
									class="w3-card-4 w3-theme-d4 w3-padding-small"
 | 
				
			||||||
				<div style="flex: 1 0 50%">
 | 
									style="box-sizing: border-box"
 | 
				
			||||||
					${content_warning}
 | 
								>
 | 
				
			||||||
					<div id="preview"></div>
 | 
									${this.render_encrypt()}
 | 
				
			||||||
 | 
									<div class="w3-container w3-padding-small">
 | 
				
			||||||
 | 
										<div class="w3-half">
 | 
				
			||||||
 | 
											<span
 | 
				
			||||||
 | 
												class="w3-input w3-theme-d1 w3-border"
 | 
				
			||||||
 | 
												style="resize: vertical; width: 100%; overflow: hidden; white-space: pre-wrap"
 | 
				
			||||||
 | 
												placeholder="Write a post here."
 | 
				
			||||||
 | 
												id="edit"
 | 
				
			||||||
 | 
												@input=${this.input}
 | 
				
			||||||
 | 
												@paste=${this.paste}
 | 
				
			||||||
 | 
												contenteditable
 | 
				
			||||||
 | 
												.innerText=${live(draft.text ?? '')}
 | 
				
			||||||
 | 
											></span>
 | 
				
			||||||
 | 
										</div>
 | 
				
			||||||
 | 
										<div class="w3-half w3-padding">
 | 
				
			||||||
 | 
											${content_warning}
 | 
				
			||||||
 | 
											<div id="preview"></div>
 | 
				
			||||||
 | 
										</div>
 | 
				
			||||||
				</div>
 | 
									</div>
 | 
				
			||||||
 | 
									${Object.values(draft.mentions || {}).map((x) =>
 | 
				
			||||||
 | 
										self.render_mention(x)
 | 
				
			||||||
 | 
									)}
 | 
				
			||||||
 | 
									${this.render_attach_app()} ${this.render_content_warning()}
 | 
				
			||||||
 | 
									<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>
 | 
				
			||||||
			</div>
 | 
								</div>
 | 
				
			||||||
			${Object.values(draft.mentions || {}).map(x => self.render_mention(x))}
 | 
					 | 
				
			||||||
			${this.render_content_warning()}
 | 
					 | 
				
			||||||
			${this.render_attach_app()}
 | 
					 | 
				
			||||||
			<input type="button" value="Submit" @click=${this.submit}></input>
 | 
					 | 
				
			||||||
			<input type="button" value="Attach" @click=${this.attach}></input>
 | 
					 | 
				
			||||||
			${this.render_attach_app_button()}
 | 
					 | 
				
			||||||
			<input type="button" value="Discard" @click=${this.discard}></input>
 | 
					 | 
				
			||||||
		`;
 | 
							`;
 | 
				
			||||||
		return result;
 | 
							return result;
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,37 +0,0 @@
 | 
				
			|||||||
import {LitElement, html} from './lit-all.min.js';
 | 
					 | 
				
			||||||
import * as tfrpc from '/static/tfrpc.js';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
/*
 | 
					 | 
				
			||||||
** Provide a list of IDs, and this lets the user pick one.
 | 
					 | 
				
			||||||
*/
 | 
					 | 
				
			||||||
class TfIdentityPickerElement extends LitElement {
 | 
					 | 
				
			||||||
	static get properties() {
 | 
					 | 
				
			||||||
		return {
 | 
					 | 
				
			||||||
			ids: {type: Array},
 | 
					 | 
				
			||||||
			selected: {type: String},
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	constructor() {
 | 
					 | 
				
			||||||
		super();
 | 
					 | 
				
			||||||
		let self = this;
 | 
					 | 
				
			||||||
		this.ids = [];
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	changed(event) {
 | 
					 | 
				
			||||||
		this.selected = event.srcElement.value;
 | 
					 | 
				
			||||||
		this.dispatchEvent(new Event('change', {
 | 
					 | 
				
			||||||
			srcElement: this,
 | 
					 | 
				
			||||||
		}));
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	render() {
 | 
					 | 
				
			||||||
		return html`
 | 
					 | 
				
			||||||
			<select @change=${this.changed} style="max-width: 100%">
 | 
					 | 
				
			||||||
				${(this.ids ?? []).map(id => html`<option ?selected=${id == this.selected} value=${id}>${id}</option>`)}
 | 
					 | 
				
			||||||
			</select>
 | 
					 | 
				
			||||||
		`;
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
customElements.define('tf-id-picker', TfIdentityPickerElement);
 | 
					 | 
				
			||||||
@@ -1,4 +1,4 @@
 | 
				
			|||||||
import {LitElement, html, unsafeHTML} from './lit-all.min.js';
 | 
					import {LitElement, html, render, unsafeHTML} from './lit-all.min.js';
 | 
				
			||||||
import * as tfrpc from '/static/tfrpc.js';
 | 
					import * as tfrpc from '/static/tfrpc.js';
 | 
				
			||||||
import * as tfutils from './tf-utils.js';
 | 
					import * as tfutils from './tf-utils.js';
 | 
				
			||||||
import * as emojis from './emojis.js';
 | 
					import * as emojis from './emojis.js';
 | 
				
			||||||
@@ -11,10 +11,10 @@ class TfMessageElement extends LitElement {
 | 
				
			|||||||
			message: {type: Object},
 | 
								message: {type: Object},
 | 
				
			||||||
			users: {type: Object},
 | 
								users: {type: Object},
 | 
				
			||||||
			drafts: {type: Object},
 | 
								drafts: {type: Object},
 | 
				
			||||||
			raw: {type: Boolean},
 | 
								format: {type: String},
 | 
				
			||||||
			blog_data: {type: String},
 | 
								blog_data: {type: String},
 | 
				
			||||||
			expanded: {type: Object},
 | 
								expanded: {type: Object},
 | 
				
			||||||
		}
 | 
							};
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	static styles = styles;
 | 
						static styles = styles;
 | 
				
			||||||
@@ -26,17 +26,38 @@ class TfMessageElement extends LitElement {
 | 
				
			|||||||
		this.message = {};
 | 
							this.message = {};
 | 
				
			||||||
		this.users = {};
 | 
							this.users = {};
 | 
				
			||||||
		this.drafts = {};
 | 
							this.drafts = {};
 | 
				
			||||||
		this.raw = false;
 | 
							this.format = 'message';
 | 
				
			||||||
		this.expanded = {};
 | 
							this.expanded = {};
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	show_reply() {
 | 
						show_reply() {
 | 
				
			||||||
		let event = new CustomEvent('tf-draft', {bubbles: true, composed: true, detail: {id: this.message?.id, draft: ''}});
 | 
							let event = new CustomEvent('tf-draft', {
 | 
				
			||||||
 | 
								bubbles: true,
 | 
				
			||||||
 | 
								composed: true,
 | 
				
			||||||
 | 
								detail: {
 | 
				
			||||||
 | 
									id: this.message?.id,
 | 
				
			||||||
 | 
									draft: {
 | 
				
			||||||
 | 
										encrypt_to: this.message?.decrypted?.recps,
 | 
				
			||||||
 | 
									},
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
							});
 | 
				
			||||||
		this.dispatchEvent(event);
 | 
							this.dispatchEvent(event);
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	discard_reply() {
 | 
						discard_reply() {
 | 
				
			||||||
		this.dispatchEvent(new CustomEvent('tf-draft', {bubbles: true, composed: true, detail: {id: this.id, draft: undefined}}));
 | 
							this.dispatchEvent(
 | 
				
			||||||
 | 
								new CustomEvent('tf-draft', {
 | 
				
			||||||
 | 
									bubbles: true,
 | 
				
			||||||
 | 
									composed: true,
 | 
				
			||||||
 | 
									detail: {id: this.id, draft: undefined},
 | 
				
			||||||
 | 
								})
 | 
				
			||||||
 | 
							);
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						show_reactions() {
 | 
				
			||||||
 | 
							let modal = document.getElementById('reactions_modal');
 | 
				
			||||||
 | 
							modal.users = this.users;
 | 
				
			||||||
 | 
							modal.votes = this.message?.votes || [];
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	render_votes() {
 | 
						render_votes() {
 | 
				
			||||||
@@ -51,12 +72,21 @@ class TfMessageElement extends LitElement {
 | 
				
			|||||||
				return expression;
 | 
									return expression;
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
		return html`<div>${(this.message.votes || []).map(
 | 
							if (this.message?.votes?.length) {
 | 
				
			||||||
			vote => html`
 | 
								return html`<div class="w3-button" @click=${this.show_reactions}>
 | 
				
			||||||
				<span title="${this.users[vote.author]?.name ?? vote.author} ${new Date(vote.timestamp)}">
 | 
									${(this.message.votes || []).map(
 | 
				
			||||||
					${normalize_expression(vote.content.vote.expression)}
 | 
										(vote) => html`
 | 
				
			||||||
				</span>
 | 
											<span
 | 
				
			||||||
			`)}</div>`;
 | 
												title="${this.users[vote.author]?.name ?? vote.author} ${new Date(
 | 
				
			||||||
 | 
													vote.timestamp
 | 
				
			||||||
 | 
												)}"
 | 
				
			||||||
 | 
											>
 | 
				
			||||||
 | 
												${normalize_expression(vote.content.vote.expression)}
 | 
				
			||||||
 | 
											</span>
 | 
				
			||||||
 | 
										`
 | 
				
			||||||
 | 
									)}
 | 
				
			||||||
 | 
								</div>`;
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	render_raw() {
 | 
						render_raw() {
 | 
				
			||||||
@@ -69,31 +99,41 @@ class TfMessageElement extends LitElement {
 | 
				
			|||||||
			hash: this.message?.hash,
 | 
								hash: this.message?.hash,
 | 
				
			||||||
			content: this.message?.content,
 | 
								content: this.message?.content,
 | 
				
			||||||
			signature: this.message?.signature,
 | 
								signature: this.message?.signature,
 | 
				
			||||||
		}
 | 
							};
 | 
				
			||||||
		return html`<div style="white-space: pre-wrap">${JSON.stringify(raw, null, 2)}</div>`
 | 
							return html`<div style="white-space: pre-wrap">
 | 
				
			||||||
 | 
								${JSON.stringify(raw, null, 2)}
 | 
				
			||||||
 | 
							</div>`;
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	vote(emoji) {
 | 
						vote(emoji) {
 | 
				
			||||||
		let reaction = emoji.emoji;
 | 
							let reaction = emoji;
 | 
				
			||||||
		let message = this.message.id;
 | 
							let message = this.message.id;
 | 
				
			||||||
		if (confirm('Are you sure you want to react with ' + reaction + ' to ' + message + '?')) {
 | 
							if (
 | 
				
			||||||
			tfrpc.rpc.appendMessage(
 | 
								confirm(
 | 
				
			||||||
				this.whoami,
 | 
									'Are you sure you want to react with ' +
 | 
				
			||||||
				{
 | 
										reaction +
 | 
				
			||||||
 | 
										' to ' +
 | 
				
			||||||
 | 
										message +
 | 
				
			||||||
 | 
										'?'
 | 
				
			||||||
 | 
								)
 | 
				
			||||||
 | 
							) {
 | 
				
			||||||
 | 
								tfrpc.rpc
 | 
				
			||||||
 | 
									.appendMessage(this.whoami, {
 | 
				
			||||||
					type: 'vote',
 | 
										type: 'vote',
 | 
				
			||||||
					vote: {
 | 
										vote: {
 | 
				
			||||||
						link: message,
 | 
											link: message,
 | 
				
			||||||
						value: 1,
 | 
											value: 1,
 | 
				
			||||||
						expression: reaction,
 | 
											expression: reaction,
 | 
				
			||||||
					},
 | 
										},
 | 
				
			||||||
				}).catch(function(error) {
 | 
									})
 | 
				
			||||||
 | 
									.catch(function (error) {
 | 
				
			||||||
					alert(error?.message);
 | 
										alert(error?.message);
 | 
				
			||||||
				});
 | 
									});
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	react(event) {
 | 
						react(event) {
 | 
				
			||||||
		emojis.picker(x => this.vote(x));
 | 
							emojis.picker((x) => this.vote(x), null, this.whoami);
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	show_image(link) {
 | 
						show_image(link) {
 | 
				
			||||||
@@ -127,57 +167,89 @@ class TfMessageElement extends LitElement {
 | 
				
			|||||||
	body_click(event) {
 | 
						body_click(event) {
 | 
				
			||||||
		if (event.srcElement.tagName == 'IMG') {
 | 
							if (event.srcElement.tagName == 'IMG') {
 | 
				
			||||||
			this.show_image(event.srcElement.src);
 | 
								this.show_image(event.srcElement.src);
 | 
				
			||||||
 | 
							} else if (
 | 
				
			||||||
 | 
								event.srcElement.tagName == 'DIV' &&
 | 
				
			||||||
 | 
								event.srcElement.classList.contains('img_caption')
 | 
				
			||||||
 | 
							) {
 | 
				
			||||||
 | 
								let next = event.srcElement.nextSibling;
 | 
				
			||||||
 | 
								if (next.style.display != 'none') {
 | 
				
			||||||
 | 
									next.style.display = 'none';
 | 
				
			||||||
 | 
								} else {
 | 
				
			||||||
 | 
									next.style.display = 'block';
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	render_mention(mention) {
 | 
						render_mention(mention) {
 | 
				
			||||||
		if (!mention?.link || typeof(mention.link) != 'string') {
 | 
							if (!mention?.link || typeof mention.link != 'string') {
 | 
				
			||||||
			return html` <pre>${JSON.stringify(mention)}</pre>`;
 | 
								return html` <pre>${JSON.stringify(mention)}</pre>`;
 | 
				
			||||||
		} else if (mention?.link?.startsWith('&') &&
 | 
							} else if (
 | 
				
			||||||
			mention?.type?.startsWith('image/')) {
 | 
								mention?.link?.startsWith('&') &&
 | 
				
			||||||
 | 
								mention?.type?.startsWith('image/')
 | 
				
			||||||
 | 
							) {
 | 
				
			||||||
			return html`
 | 
								return html`
 | 
				
			||||||
				<img src=${'/' + mention.link + '/view'} style="max-width: 128px; max-height: 128px" title=${mention.name} @click=${() => this.show_image('/' + mention.link + '/view')}>
 | 
									<img
 | 
				
			||||||
 | 
										src=${'/' + mention.link + '/view'}
 | 
				
			||||||
 | 
										style="max-width: 128px; max-height: 128px"
 | 
				
			||||||
 | 
										title=${mention.name}
 | 
				
			||||||
 | 
										@click=${() => this.show_image('/' + mention.link + '/view')}
 | 
				
			||||||
 | 
									/>
 | 
				
			||||||
			`;
 | 
								`;
 | 
				
			||||||
		} else if (mention.link?.startsWith('&') &&
 | 
							} else if (
 | 
				
			||||||
			mention.name?.startsWith('audio:')) {
 | 
								mention.link?.startsWith('&') &&
 | 
				
			||||||
 | 
								mention.name?.startsWith('audio:')
 | 
				
			||||||
 | 
							) {
 | 
				
			||||||
			return html`
 | 
								return html`
 | 
				
			||||||
				<audio controls style="height: 32px">
 | 
									<audio controls style="height: 32px">
 | 
				
			||||||
					<source src=${'/' + mention.link + '/view'}></source>
 | 
										<source src=${'/' + mention.link + '/view'}></source>
 | 
				
			||||||
				</audio>
 | 
									</audio>
 | 
				
			||||||
			`;
 | 
								`;
 | 
				
			||||||
		} else if (mention.link?.startsWith('&') &&
 | 
							} else if (
 | 
				
			||||||
			mention.name?.startsWith('video:')) {
 | 
								mention.link?.startsWith('&') &&
 | 
				
			||||||
 | 
								mention.name?.startsWith('video:')
 | 
				
			||||||
 | 
							) {
 | 
				
			||||||
			return html`
 | 
								return html`
 | 
				
			||||||
				<video controls style="max-height: 240px; max-width: 128px">
 | 
									<video controls style="max-height: 240px; max-width: 128px">
 | 
				
			||||||
					<source src=${'/' + mention.link + '/view'}></source>
 | 
										<source src=${'/' + mention.link + '/view'}></source>
 | 
				
			||||||
				</video>
 | 
									</video>
 | 
				
			||||||
			`;
 | 
								`;
 | 
				
			||||||
		} else if (mention.link?.startsWith('&') &&
 | 
							} else if (
 | 
				
			||||||
			mention?.type === 'application/tildefriends') {
 | 
								mention.link?.startsWith('&') &&
 | 
				
			||||||
 | 
								mention?.type === 'application/tildefriends'
 | 
				
			||||||
 | 
							) {
 | 
				
			||||||
			return html` <a href=${`/${mention.link}/`}>😎 ${mention.name}</a>`;
 | 
								return html` <a href=${`/${mention.link}/`}>😎 ${mention.name}</a>`;
 | 
				
			||||||
		} else if (mention.link?.startsWith('%') || mention.link?.startsWith('@')) {
 | 
							} else if (mention.link?.startsWith('%') || mention.link?.startsWith('@')) {
 | 
				
			||||||
			return html` <a href=${'#' + encodeURIComponent(mention.link)}>${mention.name}</a>`;
 | 
								return html` <a href=${'#' + encodeURIComponent(mention.link)}
 | 
				
			||||||
 | 
									>${mention.name}</a
 | 
				
			||||||
 | 
								>`;
 | 
				
			||||||
		} else if (mention.link?.startsWith('#')) {
 | 
							} else if (mention.link?.startsWith('#')) {
 | 
				
			||||||
			return html` <a href=${'#q=' + encodeURIComponent(mention.link)}>${mention.link}</a>`;
 | 
								return html` <a href=${'#q=' + encodeURIComponent(mention.link)}
 | 
				
			||||||
		} else if (Object.keys(mention).length == 2 && mention.link && mention.name) {
 | 
									>${mention.link}</a
 | 
				
			||||||
 | 
								>`;
 | 
				
			||||||
 | 
							} else if (
 | 
				
			||||||
 | 
								Object.keys(mention).length == 2 &&
 | 
				
			||||||
 | 
								mention.link &&
 | 
				
			||||||
 | 
								mention.name
 | 
				
			||||||
 | 
							) {
 | 
				
			||||||
			return html` <a href=${`/${mention.link}/view`}>${mention.name}</a>`;
 | 
								return html` <a href=${`/${mention.link}/view`}>${mention.name}</a>`;
 | 
				
			||||||
		} else {
 | 
							} else {
 | 
				
			||||||
			return html` <pre style="white-space: pre-wrap">${JSON.stringify(mention, null, 2)}</pre>`;
 | 
								return html` <pre style="white-space: pre-wrap">
 | 
				
			||||||
 | 
					${JSON.stringify(mention, null, 2)}</pre
 | 
				
			||||||
 | 
								>`;
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	render_mentions() {
 | 
						render_mentions() {
 | 
				
			||||||
		let mentions = this.message?.content?.mentions || [];
 | 
							let mentions = this.message?.content?.mentions || [];
 | 
				
			||||||
		mentions = mentions.filter(x =>
 | 
							mentions = mentions.filter(
 | 
				
			||||||
			x.name?.startsWith('audio:') ||
 | 
								(x) => this.message?.content?.text?.indexOf(x.link) === -1
 | 
				
			||||||
			x.name?.startsWith('video:') ||
 | 
							);
 | 
				
			||||||
			this.message?.content?.text?.indexOf(x.link) === -1);
 | 
					 | 
				
			||||||
		if (mentions.length) {
 | 
							if (mentions.length) {
 | 
				
			||||||
			let self = this;
 | 
								let self = this;
 | 
				
			||||||
			return html`
 | 
								return html`
 | 
				
			||||||
				<fieldset style="background-color: rgba(0, 0, 0, 0.1); padding: 0.5em; border: 1px solid black">
 | 
									<fieldset style="padding: 0.5em; border: 1px solid black">
 | 
				
			||||||
					<legend>Mentions</legend>
 | 
										<legend>Mentions</legend>
 | 
				
			||||||
					${mentions.map(x => self.render_mention(x))}
 | 
										${mentions.map((x) => self.render_mention(x))}
 | 
				
			||||||
				</fieldset>
 | 
									</fieldset>
 | 
				
			||||||
			`;
 | 
								`;
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
@@ -188,66 +260,206 @@ class TfMessageElement extends LitElement {
 | 
				
			|||||||
			return 0;
 | 
								return 0;
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
		let total = message.child_messages.length;
 | 
							let total = message.child_messages.length;
 | 
				
			||||||
		for (let m of message.child_messages)
 | 
							for (let m of message.child_messages) {
 | 
				
			||||||
		{
 | 
					 | 
				
			||||||
			total += this.total_child_messages(m);
 | 
								total += this.total_child_messages(m);
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
		return total;
 | 
							return total;
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	set_expanded(expanded, tag) {
 | 
						set_expanded(expanded, tag) {
 | 
				
			||||||
		this.dispatchEvent(new CustomEvent('tf-expand', {bubbles: true, composed: true, detail: {id: (this.message.id || '') + (tag || ''), expanded: expanded}}));
 | 
							this.dispatchEvent(
 | 
				
			||||||
 | 
								new CustomEvent('tf-expand', {
 | 
				
			||||||
 | 
									bubbles: true,
 | 
				
			||||||
 | 
									composed: true,
 | 
				
			||||||
 | 
									detail: {id: (this.message.id || '') + (tag || ''), expanded: expanded},
 | 
				
			||||||
 | 
								})
 | 
				
			||||||
 | 
							);
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	toggle_expanded(tag) {
 | 
						toggle_expanded(tag) {
 | 
				
			||||||
		this.set_expanded(!this.expanded[(this.message.id || '') + (tag || '')], tag);
 | 
							this.set_expanded(
 | 
				
			||||||
 | 
								!this.expanded[(this.message.id || '') + (tag || '')],
 | 
				
			||||||
 | 
								tag
 | 
				
			||||||
 | 
							);
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	render_children() {
 | 
						render_children() {
 | 
				
			||||||
		let self = this;
 | 
							let self = this;
 | 
				
			||||||
		if (this.message.child_messages?.length) {
 | 
							if (this.message.child_messages?.length) {
 | 
				
			||||||
			if (!this.expanded[this.message.id]) {
 | 
								if (!this.expanded[this.message.id]) {
 | 
				
			||||||
				return html`<input type="button" value=${this.total_child_messages(this.message) + ' More'} @click=${() => self.set_expanded(true)}></input>`;
 | 
									return html`<button
 | 
				
			||||||
 | 
										class="w3-button w3-theme-d1"
 | 
				
			||||||
 | 
										@click=${() => self.set_expanded(true)}
 | 
				
			||||||
 | 
									>
 | 
				
			||||||
 | 
										+ ${this.total_child_messages(this.message) + ' More'}
 | 
				
			||||||
 | 
									</button>`;
 | 
				
			||||||
			} else {
 | 
								} else {
 | 
				
			||||||
				return html`<input type="button" value="Collapse" @click=${() => self.set_expanded(false)}></input>${(this.message.child_messages || []).map(x => html`<tf-message .message=${x} whoami=${this.whoami} .users=${this.users} .drafts=${this.drafts} .expanded=${this.expanded}></tf-message>`)}`;
 | 
									return html`<button
 | 
				
			||||||
 | 
											class="w3-button w3-theme-d1"
 | 
				
			||||||
 | 
											@click=${() => self.set_expanded(false)}
 | 
				
			||||||
 | 
										>
 | 
				
			||||||
 | 
											Collapse</button
 | 
				
			||||||
 | 
										>${(this.message.child_messages || []).map(
 | 
				
			||||||
 | 
											(x) =>
 | 
				
			||||||
 | 
												html`<tf-message
 | 
				
			||||||
 | 
													.message=${x}
 | 
				
			||||||
 | 
													whoami=${this.whoami}
 | 
				
			||||||
 | 
													.users=${this.users}
 | 
				
			||||||
 | 
													.drafts=${this.drafts}
 | 
				
			||||||
 | 
													.expanded=${this.expanded}
 | 
				
			||||||
 | 
												></tf-message>`
 | 
				
			||||||
 | 
										)}`;
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						render_channels() {
 | 
				
			||||||
 | 
							let content = this.message?.content;
 | 
				
			||||||
 | 
							if (this?.messsage?.decrypted?.type == 'post') {
 | 
				
			||||||
 | 
								content = this.message.decrypted;
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							let channels = [];
 | 
				
			||||||
 | 
							if (typeof content.channel === 'string') {
 | 
				
			||||||
 | 
								channels.push(`#${content.channel}`);
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							if (Array.isArray(content.mentions)) {
 | 
				
			||||||
 | 
								for (let mention of content.mentions) {
 | 
				
			||||||
 | 
									if (typeof mention?.link === 'string' && mention.link.startsWith('#')) {
 | 
				
			||||||
 | 
										channels.push(mention.link);
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							return channels.map((x) => html`<tf-tag tag=${x}></tf-tag>`);
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	render() {
 | 
						render() {
 | 
				
			||||||
		let content = this.message?.content;
 | 
							let content = this.message?.content;
 | 
				
			||||||
 | 
							if (this.message?.decrypted?.type == 'post') {
 | 
				
			||||||
 | 
								content = this.message.decrypted;
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							let class_background = this.message?.decrypted
 | 
				
			||||||
 | 
								? 'w3-pale-red'
 | 
				
			||||||
 | 
								: 'w3-theme-d4';
 | 
				
			||||||
		let self = this;
 | 
							let self = this;
 | 
				
			||||||
		let raw_button = this.raw ?
 | 
							let raw_button;
 | 
				
			||||||
				html`<input type="button" value="Message" @click=${() => self.raw = false}></input>` :
 | 
							switch (this.format) {
 | 
				
			||||||
				html`<input type="button" value="Raw" @click=${() => self.raw = true}></input>`;
 | 
								case 'raw':
 | 
				
			||||||
 | 
									if (content?.type == 'post' || content?.type == 'blog') {
 | 
				
			||||||
 | 
										raw_button = html`<button
 | 
				
			||||||
 | 
											class="w3-button w3-theme-d1"
 | 
				
			||||||
 | 
											@click=${() => (self.format = 'md')}
 | 
				
			||||||
 | 
										>
 | 
				
			||||||
 | 
											Markdown
 | 
				
			||||||
 | 
										</button>`;
 | 
				
			||||||
 | 
									} else {
 | 
				
			||||||
 | 
										raw_button = html`<button
 | 
				
			||||||
 | 
											class="w3-button w3-theme-d1"
 | 
				
			||||||
 | 
											@click=${() => (self.format = 'message')}
 | 
				
			||||||
 | 
										>
 | 
				
			||||||
 | 
											Message
 | 
				
			||||||
 | 
										</button>`;
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
									break;
 | 
				
			||||||
 | 
								case 'md':
 | 
				
			||||||
 | 
									raw_button = html`<button
 | 
				
			||||||
 | 
										class="w3-button w3-theme-d1"
 | 
				
			||||||
 | 
										@click=${() => (self.format = 'message')}
 | 
				
			||||||
 | 
									>
 | 
				
			||||||
 | 
										Message
 | 
				
			||||||
 | 
									</button>`;
 | 
				
			||||||
 | 
									break;
 | 
				
			||||||
 | 
								case 'decrypted':
 | 
				
			||||||
 | 
									raw_button = html`<button
 | 
				
			||||||
 | 
										class="w3-button w3-theme-d1"
 | 
				
			||||||
 | 
										@click=${() => (self.format = 'raw')}
 | 
				
			||||||
 | 
									>
 | 
				
			||||||
 | 
										Raw
 | 
				
			||||||
 | 
									</button>`;
 | 
				
			||||||
 | 
									break;
 | 
				
			||||||
 | 
								default:
 | 
				
			||||||
 | 
									if (this.message.decrypted) {
 | 
				
			||||||
 | 
										raw_button = html`<button
 | 
				
			||||||
 | 
											class="w3-button w3-theme-d1"
 | 
				
			||||||
 | 
											@click=${() => (self.format = 'decrypted')}
 | 
				
			||||||
 | 
										>
 | 
				
			||||||
 | 
											Decrypted
 | 
				
			||||||
 | 
										</button>`;
 | 
				
			||||||
 | 
									} else {
 | 
				
			||||||
 | 
										raw_button = html`<button
 | 
				
			||||||
 | 
											class="w3-button w3-theme-d1"
 | 
				
			||||||
 | 
											@click=${() => (self.format = 'raw')}
 | 
				
			||||||
 | 
										>
 | 
				
			||||||
 | 
											Raw
 | 
				
			||||||
 | 
										</button>`;
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
									break;
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
		function small_frame(inner) {
 | 
							function small_frame(inner) {
 | 
				
			||||||
 | 
								let body;
 | 
				
			||||||
			return html`
 | 
								return html`
 | 
				
			||||||
				<div style="border: 1px solid black; background-color: rgba(255, 255, 255, 0.1); margin-top: 8px; padding: 16px; display: inline-block; overflow-wrap: anywhere">
 | 
									<div
 | 
				
			||||||
 | 
										class="w3-card-4 w3-theme-d4 w3-border-theme"
 | 
				
			||||||
 | 
										style="margin-top: 8px; padding: 16px; display: inline-block; overflow-wrap: anywhere"
 | 
				
			||||||
 | 
									>
 | 
				
			||||||
					<tf-user id=${self.message.author} .users=${self.users}></tf-user>
 | 
										<tf-user id=${self.message.author} .users=${self.users}></tf-user>
 | 
				
			||||||
					<span style="padding-right: 8px"><a tfarget="_top" href=${'#' + self.message.id}>%</a> ${new Date(self.message.timestamp).toLocaleString()}</span>
 | 
										<span style="padding-right: 8px"
 | 
				
			||||||
					${raw_button}
 | 
											><a tfarget="_top" href=${'#' + self.message.id}>%</a> ${new Date(
 | 
				
			||||||
					${self.raw ? self.render_raw() : inner}
 | 
												self.message.timestamp
 | 
				
			||||||
 | 
											).toLocaleString()}</span
 | 
				
			||||||
 | 
										>
 | 
				
			||||||
 | 
										${raw_button} ${self.format == 'raw' ? self.render_raw() : inner}
 | 
				
			||||||
					${self.render_votes()}
 | 
										${self.render_votes()}
 | 
				
			||||||
 | 
										${(self.message.child_messages || []).map(
 | 
				
			||||||
 | 
											(x) => html`
 | 
				
			||||||
 | 
												<tf-message
 | 
				
			||||||
 | 
													.message=${x}
 | 
				
			||||||
 | 
													whoami=${self.whoami}
 | 
				
			||||||
 | 
													.users=${self.users}
 | 
				
			||||||
 | 
													.drafts=${self.drafts}
 | 
				
			||||||
 | 
													.expanded=${self.expanded}
 | 
				
			||||||
 | 
												></tf-message>
 | 
				
			||||||
 | 
											`
 | 
				
			||||||
 | 
										)}
 | 
				
			||||||
				</div>
 | 
									</div>
 | 
				
			||||||
			`
 | 
								`;
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
		if (this.message?.type === 'contact_group') {
 | 
							if (this.message?.type === 'contact_group') {
 | 
				
			||||||
			return html`
 | 
								return html` <div
 | 
				
			||||||
				<div style="border: 1px solid black; background-color: rgba(255, 255, 255, 0.1); margin-top: 8px; padding: 16px; overflow-wrap: anywhere">
 | 
									class="w3-card-4 w3-theme-d4 w3-border-theme"
 | 
				
			||||||
					${this.message.messages.map(x => 
 | 
									style="margin-top: 8px; padding: 16px; overflow-wrap: anywhere"
 | 
				
			||||||
						html`<tf-message .message=${x} whoami=${this.whoami} .users=${this.users} .drafts=${this.drafts} .expanded=${this.expanded}></tf-message>`
 | 
								>
 | 
				
			||||||
					)}
 | 
									${this.message.messages.map(
 | 
				
			||||||
				</div>`;
 | 
										(x) =>
 | 
				
			||||||
 | 
											html`<tf-message
 | 
				
			||||||
 | 
												.message=${x}
 | 
				
			||||||
 | 
												whoami=${this.whoami}
 | 
				
			||||||
 | 
												.users=${this.users}
 | 
				
			||||||
 | 
												.drafts=${this.drafts}
 | 
				
			||||||
 | 
												.expanded=${this.expanded}
 | 
				
			||||||
 | 
											></tf-message>`
 | 
				
			||||||
 | 
									)}
 | 
				
			||||||
 | 
								</div>`;
 | 
				
			||||||
		} else if (this.message.placeholder) {
 | 
							} else if (this.message.placeholder) {
 | 
				
			||||||
			return html`
 | 
								return html` <div
 | 
				
			||||||
				<div style="border: 1px solid black; background-color: rgba(255, 255, 255, 0.1); margin-top: 8px; padding: 16px; overflow-wrap: anywhere">
 | 
									class="w3-card-4 w3-theme-d4 w3-border-theme"
 | 
				
			||||||
					<a target="_top" href=${'#' + this.message.id}>${this.message.id}</a> (placeholder)
 | 
									style="margin-top: 8px; padding: 16px; overflow-wrap: anywhere"
 | 
				
			||||||
					<div>${this.render_votes()}</div>
 | 
								>
 | 
				
			||||||
					${(this.message.child_messages || []).map(x => html`
 | 
									<a target="_top" href=${'#' + this.message.id}>${this.message.id}</a>
 | 
				
			||||||
						<tf-message .message=${x} whoami=${this.whoami} .users=${this.users} .drafts=${this.drafts} .expanded=${this.expanded}></tf-message>
 | 
									(placeholder)
 | 
				
			||||||
					`)}
 | 
									<div>${this.render_votes()}</div>
 | 
				
			||||||
				</div>`;
 | 
									${(this.message.child_messages || []).map(
 | 
				
			||||||
		} else if (typeof(content?.type === 'string')) {
 | 
										(x) => html`
 | 
				
			||||||
 | 
											<tf-message
 | 
				
			||||||
 | 
												.message=${x}
 | 
				
			||||||
 | 
												whoami=${this.whoami}
 | 
				
			||||||
 | 
												.users=${this.users}
 | 
				
			||||||
 | 
												.drafts=${this.drafts}
 | 
				
			||||||
 | 
												.expanded=${this.expanded}
 | 
				
			||||||
 | 
											></tf-message>
 | 
				
			||||||
 | 
										`
 | 
				
			||||||
 | 
									)}
 | 
				
			||||||
 | 
								</div>`;
 | 
				
			||||||
 | 
							} else if (typeof (content?.type === 'string')) {
 | 
				
			||||||
			if (content.type == 'about') {
 | 
								if (content.type == 'about') {
 | 
				
			||||||
				let name;
 | 
									let name;
 | 
				
			||||||
				let image;
 | 
									let image;
 | 
				
			||||||
@@ -257,7 +469,7 @@ class TfMessageElement extends LitElement {
 | 
				
			|||||||
				}
 | 
									}
 | 
				
			||||||
				if (content.image !== undefined) {
 | 
									if (content.image !== undefined) {
 | 
				
			||||||
					image = html`
 | 
										image = html`
 | 
				
			||||||
						<div><img src=${'/' + (typeof(content.image?.link) == 'string' ? content.image.link : content.image) + '/view'} style="width: 256px; height: auto"></img></div>
 | 
											<div><img src=${'/' + (typeof content.image?.link == 'string' ? content.image.link : content.image) + '/view'} style="width: 256px; height: auto"></img></div>
 | 
				
			||||||
					`;
 | 
										`;
 | 
				
			||||||
				}
 | 
									}
 | 
				
			||||||
				if (content.description !== undefined) {
 | 
									if (content.description !== undefined) {
 | 
				
			||||||
@@ -265,65 +477,100 @@ class TfMessageElement extends LitElement {
 | 
				
			|||||||
						<div style="flex: 1 0 50%; overflow-wrap: anywhere">
 | 
											<div style="flex: 1 0 50%; overflow-wrap: anywhere">
 | 
				
			||||||
							<div>${unsafeHTML(tfutils.markdown(content.description))}</div>
 | 
												<div>${unsafeHTML(tfutils.markdown(content.description))}</div>
 | 
				
			||||||
						</div>
 | 
											</div>
 | 
				
			||||||
					`
 | 
										`;
 | 
				
			||||||
				}
 | 
									}
 | 
				
			||||||
				let update = content.about == this.message.author ?
 | 
									let update =
 | 
				
			||||||
					html`<div style="font-weight: bold">Updated profile.</div>` :
 | 
										content.about == this.message.author
 | 
				
			||||||
					html`<div style="font-weight: bold">Updated profile for <tf-user id=${content.about} .users=${this.users}></tf-user>.</div>`;
 | 
											? html`<div style="font-weight: bold">Updated profile.</div>`
 | 
				
			||||||
				return small_frame(html`
 | 
											: html`<div style="font-weight: bold">
 | 
				
			||||||
					${update}
 | 
													Updated profile for
 | 
				
			||||||
					${name}
 | 
													<tf-user id=${content.about} .users=${this.users}></tf-user>.
 | 
				
			||||||
					${image}
 | 
												</div>`;
 | 
				
			||||||
					${description}
 | 
									return small_frame(html` ${update} ${name} ${image} ${description} `);
 | 
				
			||||||
				`);
 | 
					 | 
				
			||||||
			} else if (content.type == 'contact') {
 | 
								} else if (content.type == 'contact') {
 | 
				
			||||||
				return html`
 | 
									return html`
 | 
				
			||||||
					<div>
 | 
										<div>
 | 
				
			||||||
						<tf-user id=${this.message.author} .users=${this.users}></tf-user>
 | 
											<tf-user id=${this.message.author} .users=${this.users}></tf-user>
 | 
				
			||||||
						is
 | 
											is
 | 
				
			||||||
						${
 | 
											${content.blocking === true
 | 
				
			||||||
							content.blocking === true ? 'blocking' :
 | 
												? 'blocking'
 | 
				
			||||||
							content.blocking === false ? 'no longer blocking' :
 | 
												: content.blocking === false
 | 
				
			||||||
							content.following === true ? 'following' :
 | 
													? 'no longer blocking'
 | 
				
			||||||
							content.following === false ? 'no longer following' :
 | 
													: content.following === true
 | 
				
			||||||
							'?'
 | 
														? 'following'
 | 
				
			||||||
						}
 | 
														: content.following === false
 | 
				
			||||||
						<tf-user id=${this.message.content.contact} .users=${this.users}></tf-user>
 | 
															? 'no longer following'
 | 
				
			||||||
 | 
															: '?'}
 | 
				
			||||||
 | 
											<tf-user
 | 
				
			||||||
 | 
												id=${this.message.content.contact}
 | 
				
			||||||
 | 
												.users=${this.users}
 | 
				
			||||||
 | 
											></tf-user>
 | 
				
			||||||
					</div>
 | 
										</div>
 | 
				
			||||||
				`;
 | 
									`;
 | 
				
			||||||
			} else if (content.type == 'post') {
 | 
								} else if (content.type == 'post') {
 | 
				
			||||||
				let reply = (this.drafts[this.message?.id] !== undefined) ? html`
 | 
									let reply =
 | 
				
			||||||
					<tf-compose
 | 
										this.drafts[this.message?.id] !== undefined
 | 
				
			||||||
						whoami=${this.whoami}
 | 
											? html`
 | 
				
			||||||
						.users=${this.users}
 | 
													<tf-compose
 | 
				
			||||||
						root=${this.message.content.root || this.message.id}
 | 
														whoami=${this.whoami}
 | 
				
			||||||
						branch=${this.message.id}
 | 
														.users=${this.users}
 | 
				
			||||||
						.drafts=${this.drafts}
 | 
														root=${content.root || this.message.id}
 | 
				
			||||||
						@tf-discard=${this.discard_reply}></tf-compose>
 | 
														branch=${this.message.id}
 | 
				
			||||||
				` : html`
 | 
														.drafts=${this.drafts}
 | 
				
			||||||
					<input type="button" value="Reply" @click=${this.show_reply}></input>
 | 
														@tf-discard=${this.discard_reply}
 | 
				
			||||||
				`;
 | 
														author=${this.message.author}
 | 
				
			||||||
 | 
													></tf-compose>
 | 
				
			||||||
 | 
												`
 | 
				
			||||||
 | 
											: html`
 | 
				
			||||||
 | 
													<button class="w3-button w3-theme-d1" @click=${this.show_reply}>
 | 
				
			||||||
 | 
														Reply
 | 
				
			||||||
 | 
													</button>
 | 
				
			||||||
 | 
												`;
 | 
				
			||||||
				let self = this;
 | 
									let self = this;
 | 
				
			||||||
				let body = this.raw ?
 | 
									let body;
 | 
				
			||||||
					this.render_raw() :
 | 
									switch (this.format) {
 | 
				
			||||||
					unsafeHTML(tfutils.markdown(content.text));
 | 
										case 'raw':
 | 
				
			||||||
 | 
											body = this.render_raw();
 | 
				
			||||||
 | 
											break;
 | 
				
			||||||
 | 
										case 'md':
 | 
				
			||||||
 | 
											body = html`<code
 | 
				
			||||||
 | 
												style="white-space: pre-wrap; overflow-wrap: anywhere"
 | 
				
			||||||
 | 
												>${content.text}</code
 | 
				
			||||||
 | 
											>`;
 | 
				
			||||||
 | 
											break;
 | 
				
			||||||
 | 
										case 'message':
 | 
				
			||||||
 | 
											body = unsafeHTML(tfutils.markdown(content.text));
 | 
				
			||||||
 | 
											break;
 | 
				
			||||||
 | 
										case 'decrypted':
 | 
				
			||||||
 | 
											body = html`<pre
 | 
				
			||||||
 | 
												style="white-space: pre-wrap; overflow-wrap: anywhere"
 | 
				
			||||||
 | 
											>
 | 
				
			||||||
 | 
					${JSON.stringify(content, null, 2)}</pre
 | 
				
			||||||
 | 
											>`;
 | 
				
			||||||
 | 
											break;
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
				let content_warning = html`
 | 
									let content_warning = html`
 | 
				
			||||||
					<div class="content_warning" @click=${x => this.toggle_expanded(':cw')}>${content.contentWarning}</div>
 | 
										<div
 | 
				
			||||||
					`;
 | 
											class="w3-panel w3-round-xlarge w3-theme-l4"
 | 
				
			||||||
				let content_html =
 | 
											style="cursor: pointer"
 | 
				
			||||||
					html`
 | 
											@click=${(x) => this.toggle_expanded(':cw')}
 | 
				
			||||||
						<div @click=${this.body_click}>${body}</div>
 | 
										>
 | 
				
			||||||
						${this.render_mentions()}
 | 
											<p>${content.contentWarning}</p>
 | 
				
			||||||
					`;
 | 
										</div>
 | 
				
			||||||
				let payload =
 | 
									`;
 | 
				
			||||||
					content.contentWarning ?
 | 
									let content_html = html`
 | 
				
			||||||
						self.expanded[(this.message.id || '') + ':cw'] ?
 | 
										${this.render_channels()}
 | 
				
			||||||
							html`
 | 
										<div @click=${this.body_click}>${body}</div>
 | 
				
			||||||
								${content_warning}
 | 
										${this.render_mentions()}
 | 
				
			||||||
								${content_html}
 | 
									`;
 | 
				
			||||||
							` :
 | 
									let payload = content.contentWarning
 | 
				
			||||||
							content_warning :
 | 
										? self.expanded[(this.message.id || '') + ':cw']
 | 
				
			||||||
						content_html;
 | 
											? html` ${content_warning} ${content_html} `
 | 
				
			||||||
 | 
											: content_warning
 | 
				
			||||||
 | 
										: content_html;
 | 
				
			||||||
 | 
									let is_encrypted = this.message?.decrypted
 | 
				
			||||||
 | 
										? html`<span style="align-self: center">🔓</span>`
 | 
				
			||||||
 | 
										: undefined;
 | 
				
			||||||
				return html`
 | 
									return html`
 | 
				
			||||||
					<style>
 | 
										<style>
 | 
				
			||||||
						code {
 | 
											code {
 | 
				
			||||||
@@ -339,37 +586,97 @@ class TfMessageElement extends LitElement {
 | 
				
			|||||||
							display: block;
 | 
												display: block;
 | 
				
			||||||
						}
 | 
											}
 | 
				
			||||||
					</style>
 | 
										</style>
 | 
				
			||||||
					<div style="border: 1px solid black; background-color: rgba(255, 255, 255, 0.1); margin-top: 8px; padding: 16px">
 | 
										<div
 | 
				
			||||||
 | 
											class="w3-card-4 ${class_background} w3-border-theme"
 | 
				
			||||||
 | 
											style="margin-top: 8px; padding: 16px"
 | 
				
			||||||
 | 
										>
 | 
				
			||||||
						<div style="display: flex; flex-direction: row">
 | 
											<div style="display: flex; flex-direction: row">
 | 
				
			||||||
							<tf-user id=${this.message.author} .users=${this.users}></tf-user>
 | 
												<tf-user id=${this.message.author} .users=${this.users}></tf-user>
 | 
				
			||||||
 | 
												${is_encrypted}
 | 
				
			||||||
							<span style="flex: 1"></span>
 | 
												<span style="flex: 1"></span>
 | 
				
			||||||
							<span style="padding-right: 8px"><a target="_top" href=${'#' + self.message.id}>%</a> ${new Date(this.message.timestamp).toLocaleString()}</span>
 | 
												<span style="padding-right: 8px"
 | 
				
			||||||
 | 
													><a target="_top" href=${'#' + self.message.id}>%</a>
 | 
				
			||||||
 | 
													${new Date(this.message.timestamp).toLocaleString()}</span
 | 
				
			||||||
 | 
												>
 | 
				
			||||||
							<span>${raw_button}</span>
 | 
												<span>${raw_button}</span>
 | 
				
			||||||
						</div>
 | 
											</div>
 | 
				
			||||||
						${payload}
 | 
											${payload} ${this.render_votes()}
 | 
				
			||||||
						${this.render_votes()}
 | 
											<p>
 | 
				
			||||||
						<div>
 | 
					 | 
				
			||||||
							${reply}
 | 
												${reply}
 | 
				
			||||||
							<input type="button" value="React" @click=${this.react}></input>
 | 
												<button class="w3-button w3-theme-d1" @click=${this.react}>
 | 
				
			||||||
 | 
													React
 | 
				
			||||||
 | 
												</button>
 | 
				
			||||||
 | 
											</p>
 | 
				
			||||||
 | 
											${this.render_children()}
 | 
				
			||||||
 | 
										</div>
 | 
				
			||||||
 | 
									`;
 | 
				
			||||||
 | 
								} else if (content.type === 'issue') {
 | 
				
			||||||
 | 
									let is_encrypted = this.message?.decrypted
 | 
				
			||||||
 | 
										? html`<span style="align-self: center">🔓</span>`
 | 
				
			||||||
 | 
										: undefined;
 | 
				
			||||||
 | 
									return html`
 | 
				
			||||||
 | 
										<style>
 | 
				
			||||||
 | 
											code {
 | 
				
			||||||
 | 
												white-space: pre-wrap;
 | 
				
			||||||
 | 
												overflow-wrap: break-word;
 | 
				
			||||||
 | 
											}
 | 
				
			||||||
 | 
											div {
 | 
				
			||||||
 | 
												overflow-wrap: anywhere;
 | 
				
			||||||
 | 
											}
 | 
				
			||||||
 | 
											img {
 | 
				
			||||||
 | 
												max-width: 100%;
 | 
				
			||||||
 | 
												height: auto;
 | 
				
			||||||
 | 
												display: block;
 | 
				
			||||||
 | 
											}
 | 
				
			||||||
 | 
										</style>
 | 
				
			||||||
 | 
										<div
 | 
				
			||||||
 | 
											class="w3-card-4 ${class_background} w3-border-theme"
 | 
				
			||||||
 | 
											style="margin-top: 8px; padding: 16px"
 | 
				
			||||||
 | 
										>
 | 
				
			||||||
 | 
											<div style="display: flex; flex-direction: row">
 | 
				
			||||||
 | 
												<tf-user id=${this.message.author} .users=${this.users}></tf-user>
 | 
				
			||||||
 | 
												${is_encrypted}
 | 
				
			||||||
 | 
												<span style="flex: 1"></span>
 | 
				
			||||||
 | 
												<span style="padding-right: 8px"
 | 
				
			||||||
 | 
													><a target="_top" href=${'#' + self.message.id}>%</a>
 | 
				
			||||||
 | 
													${new Date(this.message.timestamp).toLocaleString()}</span
 | 
				
			||||||
 | 
												>
 | 
				
			||||||
 | 
												<span>${raw_button}</span>
 | 
				
			||||||
						</div>
 | 
											</div>
 | 
				
			||||||
 | 
											${content.text} ${this.render_votes()}
 | 
				
			||||||
 | 
											<p>
 | 
				
			||||||
 | 
												<button class="w3-button w3-theme-d1" @click=${this.react}>
 | 
				
			||||||
 | 
													React
 | 
				
			||||||
 | 
												</button>
 | 
				
			||||||
 | 
											</p>
 | 
				
			||||||
						${this.render_children()}
 | 
											${this.render_children()}
 | 
				
			||||||
					</div>
 | 
										</div>
 | 
				
			||||||
				`;
 | 
									`;
 | 
				
			||||||
			} else if (content.type === 'blog') {
 | 
								} else if (content.type === 'blog') {
 | 
				
			||||||
				let self = this;
 | 
									let self = this;
 | 
				
			||||||
				tfrpc.rpc.get_blob(content.blog).then(function(data) {
 | 
									tfrpc.rpc.get_blob(content.blog).then(function (data) {
 | 
				
			||||||
					self.blog_data = data;
 | 
										self.blog_data = data;
 | 
				
			||||||
				});
 | 
									});
 | 
				
			||||||
				let payload =
 | 
									let payload = this.expanded[(this.message.id || '') + ':blog']
 | 
				
			||||||
						this.expanded[(this.message.id || '') + ':blog'] ?
 | 
										? html`<div>
 | 
				
			||||||
						html`<div>${this.blog_data ? unsafeHTML(tfutils.markdown(this.blog_data)) : 'Loading...'}</div>` :
 | 
												${this.blog_data
 | 
				
			||||||
						undefined;
 | 
													? unsafeHTML(tfutils.markdown(this.blog_data))
 | 
				
			||||||
				let body = this.raw ?
 | 
													: 'Loading...'}
 | 
				
			||||||
						this.render_raw() :
 | 
											</div>`
 | 
				
			||||||
						html`
 | 
										: undefined;
 | 
				
			||||||
 | 
									let body;
 | 
				
			||||||
 | 
									switch (this.format) {
 | 
				
			||||||
 | 
										case 'raw':
 | 
				
			||||||
 | 
											body = this.render_raw();
 | 
				
			||||||
 | 
											break;
 | 
				
			||||||
 | 
										case 'md':
 | 
				
			||||||
 | 
											body = content.summary;
 | 
				
			||||||
 | 
											break;
 | 
				
			||||||
 | 
										case 'message':
 | 
				
			||||||
 | 
											body = html`
 | 
				
			||||||
							<div
 | 
												<div
 | 
				
			||||||
								style="border: 1px solid #fff; border-radius: 1em; padding: 8px; margin: 4px; cursor: pointer"
 | 
													style="border: 1px solid #fff; border-radius: 1em; padding: 8px; margin: 4px; cursor: pointer"
 | 
				
			||||||
								@click=${x => self.toggle_expanded(':blog')}>
 | 
													@click=${(x) => self.toggle_expanded(':blog')}>
 | 
				
			||||||
								<h2>${content.title}</h2>
 | 
													<h2>${content.title}</h2>
 | 
				
			||||||
								<div style="display: flex; flex-direction: row">
 | 
													<div style="display: flex; flex-direction: row">
 | 
				
			||||||
									<img src=/${content.thumbnail}/view></img>
 | 
														<img src=/${content.thumbnail}/view></img>
 | 
				
			||||||
@@ -378,6 +685,26 @@ class TfMessageElement extends LitElement {
 | 
				
			|||||||
							</div>
 | 
												</div>
 | 
				
			||||||
							${payload}
 | 
												${payload}
 | 
				
			||||||
						`;
 | 
											`;
 | 
				
			||||||
 | 
											break;
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
									let reply =
 | 
				
			||||||
 | 
										this.drafts[this.message?.id] !== undefined
 | 
				
			||||||
 | 
											? html`
 | 
				
			||||||
 | 
													<tf-compose
 | 
				
			||||||
 | 
														whoami=${this.whoami}
 | 
				
			||||||
 | 
														.users=${this.users}
 | 
				
			||||||
 | 
														root=${content.root || this.message.id}
 | 
				
			||||||
 | 
														branch=${this.message.id}
 | 
				
			||||||
 | 
														.drafts=${this.drafts}
 | 
				
			||||||
 | 
														@tf-discard=${this.discard_reply}
 | 
				
			||||||
 | 
														author=${this.message.author}
 | 
				
			||||||
 | 
													></tf-compose>
 | 
				
			||||||
 | 
												`
 | 
				
			||||||
 | 
											: html`
 | 
				
			||||||
 | 
													<button class="w3-button w3-theme-d1" @click=${this.show_reply}>
 | 
				
			||||||
 | 
														Reply
 | 
				
			||||||
 | 
													</button>
 | 
				
			||||||
 | 
												`;
 | 
				
			||||||
				return html`
 | 
									return html`
 | 
				
			||||||
					<style>
 | 
										<style>
 | 
				
			||||||
						code {
 | 
											code {
 | 
				
			||||||
@@ -393,40 +720,74 @@ class TfMessageElement extends LitElement {
 | 
				
			|||||||
							display: block;
 | 
												display: block;
 | 
				
			||||||
						}
 | 
											}
 | 
				
			||||||
					</style>
 | 
										</style>
 | 
				
			||||||
					<div style="border: 1px solid black; background-color: rgba(255, 255, 255, 0.1); margin-top: 8px; padding: 16px">
 | 
										<div
 | 
				
			||||||
 | 
											class="w3-card-4 w3-theme-d4 w3-border-theme"
 | 
				
			||||||
 | 
											style="margin-top: 8px; padding: 16px"
 | 
				
			||||||
 | 
										>
 | 
				
			||||||
						<div style="display: flex; flex-direction: row">
 | 
											<div style="display: flex; flex-direction: row">
 | 
				
			||||||
							<tf-user id=${this.message.author} .users=${this.users}></tf-user>
 | 
												<tf-user id=${this.message.author} .users=${this.users}></tf-user>
 | 
				
			||||||
							<span style="flex: 1"></span>
 | 
												<span style="flex: 1"></span>
 | 
				
			||||||
							<span style="padding-right: 8px"><a target="_top" href=${'#' + self.message.id}>%</a> ${new Date(this.message.timestamp).toLocaleString()}</span>
 | 
												<span style="padding-right: 8px"
 | 
				
			||||||
 | 
													><a target="_top" href=${'#' + self.message.id}>%</a>
 | 
				
			||||||
 | 
													${new Date(this.message.timestamp).toLocaleString()}</span
 | 
				
			||||||
 | 
												>
 | 
				
			||||||
							<span>${raw_button}</span>
 | 
												<span>${raw_button}</span>
 | 
				
			||||||
						</div>
 | 
											</div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
						<div>${body}</div>
 | 
											<div>${body}</div>
 | 
				
			||||||
						${this.render_mentions()}
 | 
											${this.render_mentions()}
 | 
				
			||||||
						${this.render_votes()}
 | 
											<div>
 | 
				
			||||||
 | 
												${reply}
 | 
				
			||||||
 | 
												<button class="w3-button w3-theme-d1" @click=${this.react}>
 | 
				
			||||||
 | 
													React
 | 
				
			||||||
 | 
												</button>
 | 
				
			||||||
 | 
											</div>
 | 
				
			||||||
 | 
											${this.render_votes()} ${this.render_children()}
 | 
				
			||||||
					</div>
 | 
										</div>
 | 
				
			||||||
				`;
 | 
									`;
 | 
				
			||||||
			} else if (content.type === 'pub') {
 | 
								} else if (content.type === 'pub') {
 | 
				
			||||||
				return small_frame(html`
 | 
									return small_frame(
 | 
				
			||||||
				<style>
 | 
										html` <style>
 | 
				
			||||||
					span {
 | 
												span {
 | 
				
			||||||
						overflow-wrap: anywhere;
 | 
													overflow-wrap: anywhere;
 | 
				
			||||||
					}
 | 
												}
 | 
				
			||||||
				</style>
 | 
											</style>
 | 
				
			||||||
				<span>
 | 
											<span>
 | 
				
			||||||
					<div>
 | 
												<div>
 | 
				
			||||||
						🍻 <tf-user .users=${this.users} id=${content.address.key}></tf-user>
 | 
													🍻
 | 
				
			||||||
					</div>
 | 
													<tf-user
 | 
				
			||||||
					<pre>${content.address.host}:${content.address.port}</pre>
 | 
														.users=${this.users}
 | 
				
			||||||
				</span>`);
 | 
														id=${content.address.key}
 | 
				
			||||||
 | 
													></tf-user>
 | 
				
			||||||
 | 
												</div>
 | 
				
			||||||
 | 
												<pre>${content.address.host}:${content.address.port}</pre>
 | 
				
			||||||
 | 
											</span>`
 | 
				
			||||||
 | 
									);
 | 
				
			||||||
			} else if (content.type === 'channel') {
 | 
								} else if (content.type === 'channel') {
 | 
				
			||||||
				return small_frame(html`
 | 
									return small_frame(html`
 | 
				
			||||||
					<div>
 | 
										<div>
 | 
				
			||||||
						${content.subscribed ? 'subscribed to' : 'unsubscribed from'} <a href=${'#q=' + encodeURIComponent('#' + content.channel)}>#${content.channel}</a>
 | 
											${content.subscribed ? 'subscribed to' : 'unsubscribed from'}
 | 
				
			||||||
 | 
											<a href=${'#q=' + encodeURIComponent('#' + content.channel)}
 | 
				
			||||||
 | 
												>#${content.channel}</a
 | 
				
			||||||
 | 
											>
 | 
				
			||||||
					</div>
 | 
										</div>
 | 
				
			||||||
				`);
 | 
									`);
 | 
				
			||||||
			} else if (typeof(this.message.content) == 'string') {
 | 
								} else if (typeof this.message.content == 'string') {
 | 
				
			||||||
				return small_frame(html`<span>🔒</span>`);
 | 
									if (this.message?.decrypted) {
 | 
				
			||||||
 | 
										if (this.format == 'decrypted') {
 | 
				
			||||||
 | 
											return small_frame(
 | 
				
			||||||
 | 
												html`<span>🔓</span>
 | 
				
			||||||
 | 
													<pre>${JSON.stringify(this.message.decrypted, null, 2)}</pre>`
 | 
				
			||||||
 | 
											);
 | 
				
			||||||
 | 
										} else {
 | 
				
			||||||
 | 
											return small_frame(
 | 
				
			||||||
 | 
												html`<span>🔓</span>
 | 
				
			||||||
 | 
													<div>${this.message.decrypted.type}</div>`
 | 
				
			||||||
 | 
											);
 | 
				
			||||||
 | 
										}
 | 
				
			||||||
 | 
									} else {
 | 
				
			||||||
 | 
										return small_frame(html`<span>🔒</span>`);
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
			} else {
 | 
								} else {
 | 
				
			||||||
				return small_frame(html`<div><b>type</b>: ${content.type}</div>`);
 | 
									return small_frame(html`<div><b>type</b>: ${content.type}</div>`);
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -11,7 +11,7 @@ class TfNewsElement extends LitElement {
 | 
				
			|||||||
			following: {type: Array},
 | 
								following: {type: Array},
 | 
				
			||||||
			drafts: {type: Object},
 | 
								drafts: {type: Object},
 | 
				
			||||||
			expanded: {type: Object},
 | 
								expanded: {type: Object},
 | 
				
			||||||
		}
 | 
							};
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	static styles = styles;
 | 
						static styles = styles;
 | 
				
			||||||
@@ -61,7 +61,7 @@ class TfNewsElement extends LitElement {
 | 
				
			|||||||
				message.parent_message = message.content.vote.link;
 | 
									message.parent_message = message.content.vote.link;
 | 
				
			||||||
			} else if (message.content.type == 'post') {
 | 
								} else if (message.content.type == 'post') {
 | 
				
			||||||
				if (message.content.root) {
 | 
									if (message.content.root) {
 | 
				
			||||||
					if (typeof(message.content.root) === 'string') {
 | 
										if (typeof message.content.root === 'string') {
 | 
				
			||||||
						let m = ensure_message(message.content.root);
 | 
											let m = ensure_message(message.content.root);
 | 
				
			||||||
						if (!m.child_messages) {
 | 
											if (!m.child_messages) {
 | 
				
			||||||
							m.child_messages = [];
 | 
												m.child_messages = [];
 | 
				
			||||||
@@ -89,8 +89,7 @@ class TfNewsElement extends LitElement {
 | 
				
			|||||||
		for (let message of messages) {
 | 
							for (let message of messages) {
 | 
				
			||||||
			try {
 | 
								try {
 | 
				
			||||||
				message.content = JSON.parse(message.content);
 | 
									message.content = JSON.parse(message.content);
 | 
				
			||||||
			} catch {
 | 
								} catch {}
 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
			if (!messages_by_id[message.id]) {
 | 
								if (!messages_by_id[message.id]) {
 | 
				
			||||||
				messages_by_id[message.id] = message;
 | 
									messages_by_id[message.id] = message;
 | 
				
			||||||
				link_message(message);
 | 
									link_message(message);
 | 
				
			||||||
@@ -100,8 +99,12 @@ class TfNewsElement extends LitElement {
 | 
				
			|||||||
				message.parent_message = placeholder.parent_message;
 | 
									message.parent_message = placeholder.parent_message;
 | 
				
			||||||
				message.child_messages = placeholder.child_messages;
 | 
									message.child_messages = placeholder.child_messages;
 | 
				
			||||||
				message.votes = placeholder.votes;
 | 
									message.votes = placeholder.votes;
 | 
				
			||||||
				if (placeholder.parent_message && messages_by_id[placeholder.parent_message]) {
 | 
									if (
 | 
				
			||||||
					let children = messages_by_id[placeholder.parent_message].child_messages;
 | 
										placeholder.parent_message &&
 | 
				
			||||||
 | 
										messages_by_id[placeholder.parent_message]
 | 
				
			||||||
 | 
									) {
 | 
				
			||||||
 | 
										let children =
 | 
				
			||||||
 | 
											messages_by_id[placeholder.parent_message].child_messages;
 | 
				
			||||||
					children.splice(children.indexOf(placeholder), 1);
 | 
										children.splice(children.indexOf(placeholder), 1);
 | 
				
			||||||
					children.push(message);
 | 
										children.push(message);
 | 
				
			||||||
				}
 | 
									}
 | 
				
			||||||
@@ -116,7 +119,10 @@ class TfNewsElement extends LitElement {
 | 
				
			|||||||
		let latest = 0;
 | 
							let latest = 0;
 | 
				
			||||||
		for (let message of messages || []) {
 | 
							for (let message of messages || []) {
 | 
				
			||||||
			if (message.latest_subtree_timestamp === undefined) {
 | 
								if (message.latest_subtree_timestamp === undefined) {
 | 
				
			||||||
				message.latest_subtree_timestamp = Math.max(message.timestamp ?? 0, this.update_latest_subtree_timestamp(message.child_messages));
 | 
									message.latest_subtree_timestamp = Math.max(
 | 
				
			||||||
 | 
										message.timestamp ?? 0,
 | 
				
			||||||
 | 
										this.update_latest_subtree_timestamp(message.child_messages)
 | 
				
			||||||
 | 
									);
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
			latest = Math.max(latest, message.latest_subtree_timestamp);
 | 
								latest = Math.max(latest, message.latest_subtree_timestamp);
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
@@ -127,20 +133,22 @@ class TfNewsElement extends LitElement {
 | 
				
			|||||||
		function recursive_sort(messages, top) {
 | 
							function recursive_sort(messages, top) {
 | 
				
			||||||
			if (messages) {
 | 
								if (messages) {
 | 
				
			||||||
				if (top) {
 | 
									if (top) {
 | 
				
			||||||
					messages.sort((a, b) => b.latest_subtree_timestamp - a.latest_subtree_timestamp);
 | 
										messages.sort(
 | 
				
			||||||
 | 
											(a, b) => b.latest_subtree_timestamp - a.latest_subtree_timestamp
 | 
				
			||||||
 | 
										);
 | 
				
			||||||
				} else {
 | 
									} else {
 | 
				
			||||||
					messages.sort((a, b) => a.timestamp - b.timestamp);
 | 
										messages.sort((a, b) => a.timestamp - b.timestamp);
 | 
				
			||||||
				}
 | 
									}
 | 
				
			||||||
				for (let message of messages) {
 | 
									for (let message of messages) {
 | 
				
			||||||
					recursive_sort(message.child_messages, false);
 | 
										recursive_sort(message.child_messages, false);
 | 
				
			||||||
				}
 | 
									}
 | 
				
			||||||
				return messages.map(x => Object.assign({}, x));
 | 
									return messages.map((x) => Object.assign({}, x));
 | 
				
			||||||
			} else {
 | 
								} else {
 | 
				
			||||||
				return {};
 | 
									return {};
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		let roots = Object.values(messages_by_id).filter(x => !x.parent_message);
 | 
							let roots = Object.values(messages_by_id).filter((x) => !x.parent_message);
 | 
				
			||||||
		this.update_latest_subtree_timestamp(roots);
 | 
							this.update_latest_subtree_timestamp(roots);
 | 
				
			||||||
		return recursive_sort(roots, true);
 | 
							return recursive_sort(roots, true);
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
@@ -167,10 +175,22 @@ class TfNewsElement extends LitElement {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
	load_and_render(messages) {
 | 
						load_and_render(messages) {
 | 
				
			||||||
		let messages_by_id = this.process_messages(messages);
 | 
							let messages_by_id = this.process_messages(messages);
 | 
				
			||||||
		let final_messages = this.group_following(this.finalize_messages(messages_by_id));
 | 
							let final_messages = this.group_following(
 | 
				
			||||||
 | 
								this.finalize_messages(messages_by_id)
 | 
				
			||||||
 | 
							);
 | 
				
			||||||
		return html`
 | 
							return html`
 | 
				
			||||||
			<div style="display: flex; flex-direction: column">
 | 
								<div style="display: flex; flex-direction: column">
 | 
				
			||||||
				${final_messages.map(x => html`<tf-message .message=${x} whoami=${this.whoami} .users=${this.users} .drafts=${this.drafts} .expanded=${this.expanded} collapsed=true></tf-message>`)}
 | 
									${final_messages.map(
 | 
				
			||||||
 | 
										(x) =>
 | 
				
			||||||
 | 
											html`<tf-message
 | 
				
			||||||
 | 
												.message=${x}
 | 
				
			||||||
 | 
												whoami=${this.whoami}
 | 
				
			||||||
 | 
												.users=${this.users}
 | 
				
			||||||
 | 
												.drafts=${this.drafts}
 | 
				
			||||||
 | 
												.expanded=${this.expanded}
 | 
				
			||||||
 | 
												collapsed="true"
 | 
				
			||||||
 | 
											></tf-message>`
 | 
				
			||||||
 | 
									)}
 | 
				
			||||||
			</div>
 | 
								</div>
 | 
				
			||||||
		`;
 | 
							`;
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -11,7 +11,10 @@ class TfProfileElement extends LitElement {
 | 
				
			|||||||
			id: {type: String},
 | 
								id: {type: String},
 | 
				
			||||||
			users: {type: Object},
 | 
								users: {type: Object},
 | 
				
			||||||
			size: {type: Number},
 | 
								size: {type: Number},
 | 
				
			||||||
		}
 | 
								server_follows_me: {type: Boolean},
 | 
				
			||||||
 | 
								following: {type: Boolean},
 | 
				
			||||||
 | 
								blocking: {type: Boolean},
 | 
				
			||||||
 | 
							};
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	static styles = styles;
 | 
						static styles = styles;
 | 
				
			||||||
@@ -24,16 +27,77 @@ class TfProfileElement extends LitElement {
 | 
				
			|||||||
		this.id = null;
 | 
							this.id = null;
 | 
				
			||||||
		this.users = {};
 | 
							this.users = {};
 | 
				
			||||||
		this.size = 0;
 | 
							this.size = 0;
 | 
				
			||||||
 | 
							this.server_follows_me = undefined;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						async load() {
 | 
				
			||||||
 | 
							if (this.whoami !== this._follow_whoami) {
 | 
				
			||||||
 | 
								this._follow_whoami = this.whoami;
 | 
				
			||||||
 | 
								this.following = undefined;
 | 
				
			||||||
 | 
								this.blocking = undefined;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								let result = 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') = ? AND
 | 
				
			||||||
 | 
									following IS NOT NULL
 | 
				
			||||||
 | 
									ORDER BY sequence DESC LIMIT 1
 | 
				
			||||||
 | 
								`,
 | 
				
			||||||
 | 
									[this.whoami, this.id]
 | 
				
			||||||
 | 
								);
 | 
				
			||||||
 | 
								this.following = result?.[0]?.following ?? false;
 | 
				
			||||||
 | 
								result = await tfrpc.rpc.query(
 | 
				
			||||||
 | 
									`
 | 
				
			||||||
 | 
									SELECT json_extract(content, '$.blocking') AS blocking
 | 
				
			||||||
 | 
									FROM messages WHERE author = ? AND
 | 
				
			||||||
 | 
									json_extract(content, '$.type') = 'contact' AND
 | 
				
			||||||
 | 
									json_extract(content, '$.contact') = ? AND
 | 
				
			||||||
 | 
									blocking IS NOT NULL
 | 
				
			||||||
 | 
									ORDER BY sequence DESC LIMIT 1
 | 
				
			||||||
 | 
								`,
 | 
				
			||||||
 | 
									[this.whoami, this.id]
 | 
				
			||||||
 | 
								);
 | 
				
			||||||
 | 
								this.blocking = result?.[0]?.blocking ?? false;
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						async initial_load() {
 | 
				
			||||||
 | 
							this.server_follows_me = undefined;
 | 
				
			||||||
 | 
							let server_id = await tfrpc.rpc.getServerIdentity();
 | 
				
			||||||
 | 
							let followed = await tfrpc.rpc.query(
 | 
				
			||||||
 | 
								`
 | 
				
			||||||
 | 
								SELECT json_extract(content, '$.following') AS following
 | 
				
			||||||
 | 
								FROM messages
 | 
				
			||||||
 | 
								WHERE author = ? AND
 | 
				
			||||||
 | 
								json_extract(content, '$.type') = 'contact' AND
 | 
				
			||||||
 | 
								json_extract(content, '$.contact') = ? ORDER BY sequence DESC LIMIT 1
 | 
				
			||||||
 | 
							`,
 | 
				
			||||||
 | 
								[server_id, this.whoami]
 | 
				
			||||||
 | 
							);
 | 
				
			||||||
 | 
							let is_followed = false;
 | 
				
			||||||
 | 
							for (let row of followed) {
 | 
				
			||||||
 | 
								is_followed = row.following != 0;
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							this.server_follows_me = is_followed;
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	modify(change) {
 | 
						modify(change) {
 | 
				
			||||||
		tfrpc.rpc.appendMessage(this.whoami,
 | 
							tfrpc.rpc
 | 
				
			||||||
			Object.assign({
 | 
								.appendMessage(
 | 
				
			||||||
				type: 'contact',
 | 
									this.whoami,
 | 
				
			||||||
				contact: this.id,
 | 
									Object.assign(
 | 
				
			||||||
			}, change)).catch(function(error) {
 | 
										{
 | 
				
			||||||
 | 
											type: 'contact',
 | 
				
			||||||
 | 
											contact: this.id,
 | 
				
			||||||
 | 
										},
 | 
				
			||||||
 | 
										change
 | 
				
			||||||
 | 
									)
 | 
				
			||||||
 | 
								)
 | 
				
			||||||
 | 
								.catch(function (error) {
 | 
				
			||||||
				alert(error?.message);
 | 
									alert(error?.message);
 | 
				
			||||||
			})
 | 
								});
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	follow() {
 | 
						follow() {
 | 
				
			||||||
@@ -58,6 +122,7 @@ class TfProfileElement extends LitElement {
 | 
				
			|||||||
			name: original.name,
 | 
								name: original.name,
 | 
				
			||||||
			description: original.description,
 | 
								description: original.description,
 | 
				
			||||||
			image: original.image,
 | 
								image: original.image,
 | 
				
			||||||
 | 
								publicWebHosting: original.publicWebHosting,
 | 
				
			||||||
		};
 | 
							};
 | 
				
			||||||
		console.log(this.editing);
 | 
							console.log(this.editing);
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
@@ -73,11 +138,14 @@ class TfProfileElement extends LitElement {
 | 
				
			|||||||
				message[key] = this.editing[key];
 | 
									message[key] = this.editing[key];
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
		tfrpc.rpc.appendMessage(this.whoami, message).then(function() {
 | 
							tfrpc.rpc
 | 
				
			||||||
			self.editing = null;
 | 
								.appendMessage(this.whoami, message)
 | 
				
			||||||
		}).catch(function(error) {
 | 
								.then(function () {
 | 
				
			||||||
			alert(error?.message);
 | 
									self.editing = null;
 | 
				
			||||||
		});
 | 
								})
 | 
				
			||||||
 | 
								.catch(function (error) {
 | 
				
			||||||
 | 
									alert(error?.message);
 | 
				
			||||||
 | 
								});
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	discard_edits() {
 | 
						discard_edits() {
 | 
				
			||||||
@@ -88,27 +156,55 @@ class TfProfileElement extends LitElement {
 | 
				
			|||||||
		let self = this;
 | 
							let self = this;
 | 
				
			||||||
		let input = document.createElement('input');
 | 
							let input = document.createElement('input');
 | 
				
			||||||
		input.type = 'file';
 | 
							input.type = 'file';
 | 
				
			||||||
		input.onchange = function(event) {
 | 
							input.onchange = function (event) {
 | 
				
			||||||
			let file = event.target.files[0];
 | 
								let file = event.target.files[0];
 | 
				
			||||||
			file.arrayBuffer().then(function(buffer) {
 | 
								file
 | 
				
			||||||
				let bin = Array.from(new Uint8Array(buffer));
 | 
									.arrayBuffer()
 | 
				
			||||||
				return tfrpc.rpc.store_blob(bin);
 | 
									.then(function (buffer) {
 | 
				
			||||||
			}).then(function(id) {
 | 
										let bin = Array.from(new Uint8Array(buffer));
 | 
				
			||||||
				self.editing = Object.assign({}, self.editing, {image: id});
 | 
										return tfrpc.rpc.store_blob(bin);
 | 
				
			||||||
				console.log(self.editing);
 | 
									})
 | 
				
			||||||
			}).catch(function(e) {
 | 
									.then(function (id) {
 | 
				
			||||||
				alert(e.message);
 | 
										self.editing = Object.assign({}, self.editing, {image: id});
 | 
				
			||||||
			});
 | 
										console.log(self.editing);
 | 
				
			||||||
 | 
									})
 | 
				
			||||||
 | 
									.catch(function (e) {
 | 
				
			||||||
 | 
										alert(e.message);
 | 
				
			||||||
 | 
									});
 | 
				
			||||||
		};
 | 
							};
 | 
				
			||||||
		input.click();
 | 
							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);
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	render() {
 | 
						render() {
 | 
				
			||||||
 | 
							if (
 | 
				
			||||||
 | 
								this.id == this.whoami &&
 | 
				
			||||||
 | 
								this.editing &&
 | 
				
			||||||
 | 
								this.server_follows_me === undefined
 | 
				
			||||||
 | 
							) {
 | 
				
			||||||
 | 
								this.initial_load();
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							this.load();
 | 
				
			||||||
		let self = this;
 | 
							let self = this;
 | 
				
			||||||
		let profile = this.users[this.id] || {};
 | 
							let profile = this.users[this.id] || {};
 | 
				
			||||||
		tfrpc.rpc.query(
 | 
							tfrpc.rpc
 | 
				
			||||||
			`SELECT SUM(LENGTH(content)) AS size FROM messages WHERE author = ?`,
 | 
								.query(
 | 
				
			||||||
			[this.id]).then(function(result) {
 | 
									`SELECT SUM(LENGTH(content)) AS size FROM messages WHERE author = ?`,
 | 
				
			||||||
 | 
									[this.id]
 | 
				
			||||||
 | 
								)
 | 
				
			||||||
 | 
								.then(function (result) {
 | 
				
			||||||
				self.size = result[0].size;
 | 
									self.size = result[0].size;
 | 
				
			||||||
			});
 | 
								});
 | 
				
			||||||
		let edit;
 | 
							let edit;
 | 
				
			||||||
@@ -116,46 +212,82 @@ class TfProfileElement extends LitElement {
 | 
				
			|||||||
		let block;
 | 
							let block;
 | 
				
			||||||
		if (this.id === this.whoami) {
 | 
							if (this.id === this.whoami) {
 | 
				
			||||||
			if (this.editing) {
 | 
								if (this.editing) {
 | 
				
			||||||
 | 
									let server_follow;
 | 
				
			||||||
 | 
									if (this.server_follows_me === true) {
 | 
				
			||||||
 | 
										server_follow = html`<button
 | 
				
			||||||
 | 
											class="w3-button w3-theme-d1"
 | 
				
			||||||
 | 
											@click=${() => this.server_follow_me(false)}
 | 
				
			||||||
 | 
										>
 | 
				
			||||||
 | 
											Server, Stop Following Me
 | 
				
			||||||
 | 
										</button>`;
 | 
				
			||||||
 | 
									} else if (this.server_follows_me === false) {
 | 
				
			||||||
 | 
										server_follow = html`<button
 | 
				
			||||||
 | 
											class="w3-button w3-theme-d1"
 | 
				
			||||||
 | 
											@click=${() => this.server_follow_me(true)}
 | 
				
			||||||
 | 
										>
 | 
				
			||||||
 | 
											Server, Follow Me
 | 
				
			||||||
 | 
										</button>`;
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
				edit = html`
 | 
									edit = html`
 | 
				
			||||||
					<input type="button" value="Save Profile" @click=${this.save_edits}></input>
 | 
										<button class="w3-button w3-theme-d1" @click=${this.save_edits}>
 | 
				
			||||||
					<input type="button" value="Discard" @click=${this.discard_edits}></input>
 | 
											Save Profile
 | 
				
			||||||
 | 
										</button>
 | 
				
			||||||
 | 
										<button class="w3-button w3-theme-d1" @click=${this.discard_edits}>
 | 
				
			||||||
 | 
											Discard
 | 
				
			||||||
 | 
										</button>
 | 
				
			||||||
 | 
										${server_follow}
 | 
				
			||||||
				`;
 | 
									`;
 | 
				
			||||||
			} else {
 | 
								} else {
 | 
				
			||||||
				edit = html`<input type="button" value="Edit Profile" @click=${this.edit}></input>`;
 | 
									edit = html`<button class="w3-button w3-theme-d1" @click=${this.edit}>
 | 
				
			||||||
 | 
										Edit Profile
 | 
				
			||||||
 | 
									</button>`;
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
		if (this.id !== this.whoami &&
 | 
							if (this.id !== this.whoami && this.following !== undefined) {
 | 
				
			||||||
			this.users[this.whoami]?.following) {
 | 
								follow = this.following
 | 
				
			||||||
			follow =
 | 
									? html`<button class="w3-button w3-theme-d1" @click=${this.unfollow}>
 | 
				
			||||||
				this.users[this.whoami].following[this.id] ?
 | 
											Unfollow
 | 
				
			||||||
				html`<input type="button" value="Unfollow" @click=${this.unfollow}></input>` :
 | 
										</button>`
 | 
				
			||||||
				html`<input type="button" value="Follow" @click=${this.follow}></input>`;
 | 
									: html`<button class="w3-button w3-theme-d1" @click=${this.follow}>
 | 
				
			||||||
 | 
											Follow
 | 
				
			||||||
 | 
										</button>`;
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
		if (this.id !== this.whoami &&
 | 
							if (this.id !== this.whoami && this.blocking !== undefined) {
 | 
				
			||||||
			this.users[this.whoami]?.blocking) {
 | 
								block = this.blocking
 | 
				
			||||||
			block =
 | 
									? html`<button class="w3-button w3-theme-d1" @click=${this.unblock}>
 | 
				
			||||||
				this.users[this.whoami].blocking[this.id] ?
 | 
											Unblock
 | 
				
			||||||
				html`<input type="button" value="Unblock" @click=${this.unblock}></input>` :
 | 
										</button>`
 | 
				
			||||||
				html`<input type="button" value="Block" @click=${this.block}></input>`;
 | 
									: html`<button class="w3-button w3-theme-d1" @click=${this.block}>
 | 
				
			||||||
 | 
											Block
 | 
				
			||||||
 | 
										</button>`;
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
		let edit_profile = this.editing ? html`
 | 
							let edit_profile = this.editing
 | 
				
			||||||
			<div style="flex: 1 0 50%">
 | 
								? html`
 | 
				
			||||||
				<div>
 | 
								<div style="flex: 1 0 50%; display: flex; flex-direction: column; gap: 8px">
 | 
				
			||||||
					<label for="name">Name:</label>
 | 
									<div class="w3-container">
 | 
				
			||||||
					<input 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="name">Name:</label>
 | 
				
			||||||
				<div>
 | 
											<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>
 | 
										<div><label for="description">Description:</label></div>
 | 
				
			||||||
					<textarea id="description" @input=${event => this.editing = Object.assign({}, this.editing, {description: event.srcElement.value})}>${this.editing.description}</textarea>
 | 
										<textarea class="w3-input w3-theme-d1" style="resize: vertical" rows="8" id="description" @input=${(event) => (this.editing = Object.assign({}, this.editing, {description: event.srcElement.value}))}>${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>
 | 
									</div>
 | 
				
			||||||
				<input type="button" value="Attach Image" @click=${this.attach_image}></input>
 | 
								</div>`
 | 
				
			||||||
			</div>` : null;
 | 
								: null;
 | 
				
			||||||
		let image = typeof(profile.image) == 'string' ? profile.image : profile.image?.link;
 | 
							let image =
 | 
				
			||||||
 | 
								typeof profile.image == 'string' ? profile.image : profile.image?.link;
 | 
				
			||||||
		image = this.editing?.image ?? image;
 | 
							image = this.editing?.image ?? image;
 | 
				
			||||||
		let description = this.editing?.description ?? profile.description;
 | 
							let description = this.editing?.description ?? profile.description;
 | 
				
			||||||
		return html`<div style="border: 2px solid black; background-color: rgba(255, 255, 255, 0.2); padding: 16px">
 | 
							return html`<div style="border: 2px solid black; background-color: rgba(255, 255, 255, 0.2); padding: 16px">
 | 
				
			||||||
			<tf-user id=${this.id} .users=${this.users}></tf-user> (${tfutils.human_readable_size(this.size)})
 | 
								<tf-user id=${this.id} .users=${this.users}></tf-user> (${tfutils.human_readable_size(this.size)})
 | 
				
			||||||
			<div style="display: flex; flex-direction: row">
 | 
								<div style="display: flex; flex-direction: row; gap: 1em">
 | 
				
			||||||
				${edit_profile}
 | 
									${edit_profile}
 | 
				
			||||||
				<div style="flex: 1 0 50%">
 | 
									<div style="flex: 1 0 50%">
 | 
				
			||||||
					<div><img src=${'/' + image + '/view'} style="width: 256px; height: auto"></img></div>
 | 
										<div><img src=${'/' + image + '/view'} style="width: 256px; height: auto"></img></div>
 | 
				
			||||||
@@ -163,10 +295,10 @@ class TfProfileElement extends LitElement {
 | 
				
			|||||||
				</div>
 | 
									</div>
 | 
				
			||||||
			</div>
 | 
								</div>
 | 
				
			||||||
			<div>
 | 
								<div>
 | 
				
			||||||
				Following ${Object.keys(profile.following || {}).length} identities.
 | 
									Following ${profile.following} identities.
 | 
				
			||||||
				Followed by ${Object.values(self.users).filter(x => (x.following || {})[self.id]).length} identities.
 | 
									Followed by ${profile.followed} identities.
 | 
				
			||||||
				Blocking ${Object.keys(profile.blocking || {}).length} identities.
 | 
									Blocking ${profile.blocking} identities.
 | 
				
			||||||
				Blocked by ${Object.values(self.users).filter(x => (x.blocking || {})[self.id]).length} identities.
 | 
									Blocked by ${profile.blocked} identities.
 | 
				
			||||||
			</div>
 | 
								</div>
 | 
				
			||||||
			<div>
 | 
								<div>
 | 
				
			||||||
				${edit}
 | 
									${edit}
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										68
									
								
								apps/ssb/tf-reactions-modal.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										68
									
								
								apps/ssb/tf-reactions-modal.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,68 @@
 | 
				
			|||||||
 | 
					import {LitElement, html, unsafeHTML} from './lit-all.min.js';
 | 
				
			||||||
 | 
					import {styles} from './tf-styles.js';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class TfReactionsModalElement extends LitElement {
 | 
				
			||||||
 | 
						static get properties() {
 | 
				
			||||||
 | 
							return {
 | 
				
			||||||
 | 
								users: {type: Object},
 | 
				
			||||||
 | 
								votes: {type: Array},
 | 
				
			||||||
 | 
							};
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						static styles = styles;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						constructor() {
 | 
				
			||||||
 | 
							super();
 | 
				
			||||||
 | 
							this.votes = [];
 | 
				
			||||||
 | 
							this.users = {};
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						clear() {
 | 
				
			||||||
 | 
							this.votes = [];
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						render() {
 | 
				
			||||||
 | 
							let self = this;
 | 
				
			||||||
 | 
							return this.votes?.length
 | 
				
			||||||
 | 
								? html` <div
 | 
				
			||||||
 | 
										class="w3-modal w3-animate-opacity"
 | 
				
			||||||
 | 
										style="display: block; box-sizing: border-box"
 | 
				
			||||||
 | 
									>
 | 
				
			||||||
 | 
										<div class="w3-modal-content w3-card-4 w3-theme-d1">
 | 
				
			||||||
 | 
											<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>
 | 
				
			||||||
 | 
										</div>
 | 
				
			||||||
 | 
									</div>`
 | 
				
			||||||
 | 
								: undefined;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					customElements.define('tf-reactions-modal', TfReactionsModalElement);
 | 
				
			||||||
@@ -1,39 +1,314 @@
 | 
				
			|||||||
import {css} from './lit-all.min.js';
 | 
					import {css} from './lit-all.min.js';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export let styles = css`
 | 
					const tf = css`
 | 
				
			||||||
a:link {
 | 
						img {
 | 
				
			||||||
	color: #bbf;
 | 
							max-width: min(640px, 100%);
 | 
				
			||||||
}
 | 
							max-height: min(480px, auto);
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
a:visited {
 | 
						.tab {
 | 
				
			||||||
	color: #ddd;
 | 
							border: 0;
 | 
				
			||||||
}
 | 
							padding: 8px;
 | 
				
			||||||
 | 
							margin: 0px;
 | 
				
			||||||
 | 
							cursor: pointer;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
a:hover {
 | 
						.tab:disabled {
 | 
				
			||||||
	color: #ddf;
 | 
							color: #088;
 | 
				
			||||||
}
 | 
							background-color: #fff;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
img {
 | 
						.content_warning {
 | 
				
			||||||
	max-width: min(640px, 100%);
 | 
							border: 1px solid #fff;
 | 
				
			||||||
	max-height: min(480px, auto);
 | 
							border-radius: 1em;
 | 
				
			||||||
}
 | 
							padding: 8px;
 | 
				
			||||||
 | 
							margin: 4px;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.tab {
 | 
						div.img_caption {
 | 
				
			||||||
	border: 0;
 | 
							color: #888;
 | 
				
			||||||
	padding: 8px;
 | 
							cursor: pointer;
 | 
				
			||||||
	margin: 0px;
 | 
						}
 | 
				
			||||||
	cursor: pointer;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
.tab:disabled {
 | 
						div.img_caption::after {
 | 
				
			||||||
	color: #088;
 | 
							content: ' ±';
 | 
				
			||||||
	background-color: #fff;
 | 
						}
 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
.content_warning {
 | 
						pre code {
 | 
				
			||||||
	border: 1px solid #fff;
 | 
							display: block;
 | 
				
			||||||
	border-radius: 1em;
 | 
							padding: 8px;
 | 
				
			||||||
	padding: 8px;
 | 
						}
 | 
				
			||||||
	margin: 4px;
 | 
					
 | 
				
			||||||
}
 | 
						blockquote {
 | 
				
			||||||
 | 
							border-left: 4px solid #fff;
 | 
				
			||||||
 | 
							padding: 8px;
 | 
				
			||||||
 | 
							padding-left: 12px;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
`;
 | 
					`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// prettier-ignore
 | 
				
			||||||
 | 
					const w3 = css`
 | 
				
			||||||
 | 
					/* W3.CSS 4.15 December 2020 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-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}
 | 
				
			||||||
 | 
					/* 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-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}
 | 
				
			||||||
 | 
					`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// prettier-ignore
 | 
				
			||||||
 | 
					const w3_2016_riverside = css`
 | 
				
			||||||
 | 
					.w3-theme-l5 {color:#000 !important; background-color:#f4f6f9 !important}
 | 
				
			||||||
 | 
					.w3-theme-l4 {color:#000 !important; background-color:#d9e1ec !important}
 | 
				
			||||||
 | 
					.w3-theme-l3 {color:#000 !important; background-color:#b4c3d8 !important}
 | 
				
			||||||
 | 
					.w3-theme-l2 {color:#fff !important; background-color:#8ea6c5 !important}
 | 
				
			||||||
 | 
					.w3-theme-l1 {color:#fff !important; background-color:#6888b1 !important}
 | 
				
			||||||
 | 
					.w3-theme-d1 {color:#fff !important; background-color:#456185 !important}
 | 
				
			||||||
 | 
					.w3-theme-d2 {color:#fff !important; background-color:#3d5676 !important}
 | 
				
			||||||
 | 
					.w3-theme-d3 {color:#fff !important; background-color:#354b68 !important}
 | 
				
			||||||
 | 
					.w3-theme-d4 {color:#fff !important; background-color:#2e4059 !important}
 | 
				
			||||||
 | 
					.w3-theme-d5 {color:#fff !important; background-color:#26364a !important}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.w3-theme-light {color:#000 !important; background-color:#f4f6f9 !important}
 | 
				
			||||||
 | 
					.w3-theme-dark {color:#fff !important; background-color:#26364a !important}
 | 
				
			||||||
 | 
					.w3-theme-action {color:#fff !important; background-color:#26364a !important}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.w3-theme {color:#fff !important; background-color:#4c6a92 !important}
 | 
				
			||||||
 | 
					.w3-text-theme {color:#4c6a92 !important}
 | 
				
			||||||
 | 
					.w3-border-theme {border-color:#4c6a92 !important}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.w3-hover-theme:hover {color:#fff !important; background-color:#4c6a92 !important}
 | 
				
			||||||
 | 
					.w3-hover-text-theme:hover {color:#4c6a92 !important}
 | 
				
			||||||
 | 
					.w3-hover-border-theme:hover {border-color:#4c6a92 !important}
 | 
				
			||||||
 | 
					`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export let styles = [tf, w3, w3_2016_riverside];
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,38 +1,52 @@
 | 
				
			|||||||
import {LitElement, html} from './lit-all.min.js';
 | 
					import {LitElement, html} from './lit-all.min.js';
 | 
				
			||||||
import * as tfrpc from '/static/tfrpc.js';
 | 
					import * as tfrpc from '/static/tfrpc.js';
 | 
				
			||||||
 | 
					import {styles} from './tf-styles.js';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class TfTabConnectionsElement extends LitElement {
 | 
					class TfTabConnectionsElement extends LitElement {
 | 
				
			||||||
	static get properties() {
 | 
						static get properties() {
 | 
				
			||||||
		return {
 | 
							return {
 | 
				
			||||||
			broadcasts: {type: Array},
 | 
								broadcasts: {type: Array},
 | 
				
			||||||
			identities: {type: Array},
 | 
								identities: {type: Array},
 | 
				
			||||||
 | 
								my_identities: {type: Array},
 | 
				
			||||||
			connections: {type: Array},
 | 
								connections: {type: Array},
 | 
				
			||||||
			stored_connections: {type: Array},
 | 
								stored_connections: {type: Array},
 | 
				
			||||||
			users: {type: Object},
 | 
								users: {type: Object},
 | 
				
			||||||
		}
 | 
								server_identity: {type: String},
 | 
				
			||||||
 | 
							};
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						static styles = styles;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	constructor() {
 | 
						constructor() {
 | 
				
			||||||
		super();
 | 
							super();
 | 
				
			||||||
		let self = this;
 | 
							let self = this;
 | 
				
			||||||
		this.broadcasts = [];
 | 
							this.broadcasts = [];
 | 
				
			||||||
		this.identities = [];
 | 
							this.identities = [];
 | 
				
			||||||
 | 
							this.my_identities = [];
 | 
				
			||||||
		this.connections = [];
 | 
							this.connections = [];
 | 
				
			||||||
		this.stored_connections = [];
 | 
							this.stored_connections = [];
 | 
				
			||||||
		this.users = {};
 | 
							this.users = {};
 | 
				
			||||||
		tfrpc.rpc.getAllIdentities().then(function(identities) {
 | 
							tfrpc.rpc.getIdentities().then(function (identities) {
 | 
				
			||||||
 | 
								self.my_identities = identities || [];
 | 
				
			||||||
 | 
							});
 | 
				
			||||||
 | 
							tfrpc.rpc.getAllIdentities().then(function (identities) {
 | 
				
			||||||
			self.identities = identities || [];
 | 
								self.identities = identities || [];
 | 
				
			||||||
		});
 | 
							});
 | 
				
			||||||
		tfrpc.rpc.getStoredConnections().then(function(connections) {
 | 
							tfrpc.rpc.getStoredConnections().then(function (connections) {
 | 
				
			||||||
			self.stored_connections = connections || [];
 | 
								self.stored_connections = connections || [];
 | 
				
			||||||
		});
 | 
							});
 | 
				
			||||||
 | 
							tfrpc.rpc.getServerIdentity().then(function (identity) {
 | 
				
			||||||
 | 
								self.server_identity = identity;
 | 
				
			||||||
 | 
							});
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	render_connection_summary(connection) {
 | 
						render_connection_summary(connection) {
 | 
				
			||||||
		if (connection.address && connection.port) {
 | 
							if (connection.address && connection.port) {
 | 
				
			||||||
			return html`(<small>${connection.address}:${connection.port}</small>)`;
 | 
								return html`<div>
 | 
				
			||||||
 | 
									<small>${connection.address}:${connection.port}</small>
 | 
				
			||||||
 | 
								</div>`;
 | 
				
			||||||
		} else if (connection.tunnel) {
 | 
							} else if (connection.tunnel) {
 | 
				
			||||||
			return html`(room peer)`;
 | 
								return html`<div>room peer</div>`;
 | 
				
			||||||
		} else {
 | 
							} else {
 | 
				
			||||||
			return JSON.stringify(connection);
 | 
								return JSON.stringify(connection);
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
@@ -40,13 +54,12 @@ class TfTabConnectionsElement extends LitElement {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
	render_room_peers(connection) {
 | 
						render_room_peers(connection) {
 | 
				
			||||||
		let self = this;
 | 
							let self = this;
 | 
				
			||||||
		let peers = this.broadcasts.filter(x => x.tunnel?.id == connection);
 | 
							let peers = this.broadcasts.filter((x) => x.tunnel?.id == connection);
 | 
				
			||||||
		if (peers.length) {
 | 
							if (peers.length) {
 | 
				
			||||||
			return html`
 | 
								let connections = this.connections.map((x) => x.id);
 | 
				
			||||||
				<ul>
 | 
								return html`${peers
 | 
				
			||||||
					${peers.map(x => html`${self.render_room_peer(x)}`)}
 | 
									.filter((x) => connections.indexOf(x.pubkey) == -1)
 | 
				
			||||||
				</ul>
 | 
									.map((x) => html`${self.render_room_peer(x)}`)}`;
 | 
				
			||||||
			`;
 | 
					 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -58,20 +71,32 @@ class TfTabConnectionsElement extends LitElement {
 | 
				
			|||||||
		let self = this;
 | 
							let self = this;
 | 
				
			||||||
		return html`
 | 
							return html`
 | 
				
			||||||
			<li>
 | 
								<li>
 | 
				
			||||||
				<input type="button" @click=${() => self._tunnel(connection.tunnel.id, connection.pubkey)} value="Connect"></input>
 | 
									<button
 | 
				
			||||||
				<tf-user id=${connection.pubkey} .users=${this.users}></tf-user>
 | 
										class="w3-button w3-theme-d1"
 | 
				
			||||||
 | 
										@click=${() => self._tunnel(connection.tunnel.id, connection.pubkey)}
 | 
				
			||||||
 | 
									>
 | 
				
			||||||
 | 
										Connect
 | 
				
			||||||
 | 
									</button>
 | 
				
			||||||
 | 
									<tf-user id=${connection.pubkey} .users=${this.users}></tf-user> 📡
 | 
				
			||||||
			</li>
 | 
								</li>
 | 
				
			||||||
		`;
 | 
							`;
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	render_broadcast(connection) {
 | 
						render_broadcast(connection) {
 | 
				
			||||||
		return html`
 | 
							return html`
 | 
				
			||||||
			<li>
 | 
								<li class="w3-bar" style="overflow: hidden; overflow-wrap: nowrap">
 | 
				
			||||||
				<input type="button" @click=${() => tfrpc.rpc.connect(connection)} value="Connect"></input>
 | 
									<button
 | 
				
			||||||
				<tf-user id=${connection.pubkey} .users=${this.users}></tf-user>
 | 
										class="w3-bar-item w3-button w3-theme-d1"
 | 
				
			||||||
				${this.render_connection_summary(connection)}
 | 
										@click=${() => tfrpc.rpc.connect(connection)}
 | 
				
			||||||
 | 
									>
 | 
				
			||||||
 | 
										Connect
 | 
				
			||||||
 | 
									</button>
 | 
				
			||||||
 | 
									<div class="w3-bar-item">
 | 
				
			||||||
 | 
										<tf-user id=${connection.pubkey} .users=${this.users}></tf-user>
 | 
				
			||||||
 | 
										${this.render_connection_summary(connection)}
 | 
				
			||||||
 | 
									</div>
 | 
				
			||||||
			</li>
 | 
								</li>
 | 
				
			||||||
		`
 | 
							`;
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	async forget_stored_connection(connection) {
 | 
						async forget_stored_connection(connection) {
 | 
				
			||||||
@@ -79,42 +104,125 @@ class TfTabConnectionsElement extends LitElement {
 | 
				
			|||||||
		this.stored_connections = (await tfrpc.rpc.getStoredConnections()) || [];
 | 
							this.stored_connections = (await tfrpc.rpc.getStoredConnections()) || [];
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						render_connection(connection) {
 | 
				
			||||||
 | 
							let requests = Object.values(
 | 
				
			||||||
 | 
								connection.requests.reduce(function (accumulator, value) {
 | 
				
			||||||
 | 
									let key = `${value.name}:${Math.sign(value.request_number)}`;
 | 
				
			||||||
 | 
									if (!accumulator[key]) {
 | 
				
			||||||
 | 
										accumulator[key] = Object.assign({count: 0}, value);
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
									accumulator[key].count++;
 | 
				
			||||||
 | 
									return accumulator;
 | 
				
			||||||
 | 
								}, {})
 | 
				
			||||||
 | 
							);
 | 
				
			||||||
 | 
							return html`
 | 
				
			||||||
 | 
								<button
 | 
				
			||||||
 | 
									class="w3-button w3-theme-d1"
 | 
				
			||||||
 | 
									@click=${() => tfrpc.rpc.closeConnection(connection.id)}
 | 
				
			||||||
 | 
								>
 | 
				
			||||||
 | 
									Close
 | 
				
			||||||
 | 
								</button>
 | 
				
			||||||
 | 
								<tf-user id=${connection.id} .users=${this.users}></tf-user>
 | 
				
			||||||
 | 
								${connection.tunnel !== undefined
 | 
				
			||||||
 | 
									? '🚇'
 | 
				
			||||||
 | 
									: html`(${connection.host}:${connection.port})`}
 | 
				
			||||||
 | 
								<div>
 | 
				
			||||||
 | 
									${requests.map(
 | 
				
			||||||
 | 
										(x) => html`
 | 
				
			||||||
 | 
											<span class="w3-tag w3-small"
 | 
				
			||||||
 | 
												>${x.request_number > 0 ? '🟩' : '🟥'} ${x.name}
 | 
				
			||||||
 | 
												<span
 | 
				
			||||||
 | 
													class="w3-badge w3-white"
 | 
				
			||||||
 | 
													style=${x.count > 1 ? undefined : 'display: none'}
 | 
				
			||||||
 | 
													>${x.count}</span
 | 
				
			||||||
 | 
												></span
 | 
				
			||||||
 | 
											>
 | 
				
			||||||
 | 
										`
 | 
				
			||||||
 | 
									)}
 | 
				
			||||||
 | 
								</div>
 | 
				
			||||||
 | 
								<ul>
 | 
				
			||||||
 | 
									${this.connections
 | 
				
			||||||
 | 
										.filter((x) => x.tunnel === this.connections.indexOf(connection))
 | 
				
			||||||
 | 
										.map((x) => html`<li>${this.render_connection(x)}</li>`)}
 | 
				
			||||||
 | 
									${this.render_room_peers(connection.id)}
 | 
				
			||||||
 | 
								</ul>
 | 
				
			||||||
 | 
							`;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	render() {
 | 
						render() {
 | 
				
			||||||
		let self = this;
 | 
							let self = this;
 | 
				
			||||||
		return html`
 | 
							return html`
 | 
				
			||||||
			<h2>New Connection</h2>
 | 
								<div class="w3-container" style="box-sizing: border-box">
 | 
				
			||||||
			<div style="display: flex; flex-direction: column">
 | 
									<h2>New Connection</h2>
 | 
				
			||||||
				<textarea id="code"></textarea>
 | 
									<textarea class="w3-input w3-theme-d1" id="code"></textarea>
 | 
				
			||||||
 | 
									<button
 | 
				
			||||||
 | 
										class="w3-button w3-theme-d1"
 | 
				
			||||||
 | 
										@click=${() =>
 | 
				
			||||||
 | 
											tfrpc.rpc.connect(self.renderRoot.getElementById('code').value)}
 | 
				
			||||||
 | 
									>
 | 
				
			||||||
 | 
										Connect
 | 
				
			||||||
 | 
									</button>
 | 
				
			||||||
 | 
									<h2>Broadcasts</h2>
 | 
				
			||||||
 | 
									<ul class="w3-ul w3-border">
 | 
				
			||||||
 | 
										${this.broadcasts
 | 
				
			||||||
 | 
											.filter((x) => x.address)
 | 
				
			||||||
 | 
											.map((x) => self.render_broadcast(x))}
 | 
				
			||||||
 | 
									</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>
 | 
				
			||||||
 | 
												`
 | 
				
			||||||
 | 
											)}
 | 
				
			||||||
 | 
									</ul>
 | 
				
			||||||
 | 
									<h2>Stored Connections</h2>
 | 
				
			||||||
 | 
									<ul class="w3-ul w3-border">
 | 
				
			||||||
 | 
										${this.stored_connections.map(
 | 
				
			||||||
 | 
											(x) => html`
 | 
				
			||||||
 | 
												<li class="w3-bar">
 | 
				
			||||||
 | 
													<button
 | 
				
			||||||
 | 
														class="w3-bar-item w3-button w3-theme-d1"
 | 
				
			||||||
 | 
														@click=${() => self.forget_stored_connection(x)}
 | 
				
			||||||
 | 
													>
 | 
				
			||||||
 | 
														Forget
 | 
				
			||||||
 | 
													</button>
 | 
				
			||||||
 | 
													<button
 | 
				
			||||||
 | 
														class="w3-bar-item w3-button w3-theme-d1"
 | 
				
			||||||
 | 
														@click=${() => tfrpc.rpc.connect(x)}
 | 
				
			||||||
 | 
													>
 | 
				
			||||||
 | 
														Connect
 | 
				
			||||||
 | 
													</button>
 | 
				
			||||||
 | 
													<div class="w3-bar-item">
 | 
				
			||||||
 | 
														<tf-user id=${x.pubkey} .users=${self.users}></tf-user>
 | 
				
			||||||
 | 
														<div><small>${x.address}:${x.port}</small></div>
 | 
				
			||||||
 | 
													</div>
 | 
				
			||||||
 | 
												</li>
 | 
				
			||||||
 | 
											`
 | 
				
			||||||
 | 
										)}
 | 
				
			||||||
 | 
									</ul>
 | 
				
			||||||
 | 
									<h2>Local Accounts</h2>
 | 
				
			||||||
 | 
									<ul class="w3-ul w3-border">
 | 
				
			||||||
 | 
										${this.identities.map(
 | 
				
			||||||
 | 
											(x) =>
 | 
				
			||||||
 | 
												html`<li class="w3-bar">
 | 
				
			||||||
 | 
													${x == this.server_identity
 | 
				
			||||||
 | 
														? html`<span class="w3-tag w3-medium w3-round w3-theme-l1"
 | 
				
			||||||
 | 
																>🖥 local server</span
 | 
				
			||||||
 | 
															>`
 | 
				
			||||||
 | 
														: undefined}
 | 
				
			||||||
 | 
													${this.my_identities.indexOf(x) != -1
 | 
				
			||||||
 | 
														? html`<span class="w3-tag w3-medium w3-round w3-theme-d1"
 | 
				
			||||||
 | 
																>😎 you</span
 | 
				
			||||||
 | 
															>`
 | 
				
			||||||
 | 
														: undefined}
 | 
				
			||||||
 | 
													<tf-user id=${x} .users=${this.users}></tf-user>
 | 
				
			||||||
 | 
												</li>`
 | 
				
			||||||
 | 
										)}
 | 
				
			||||||
 | 
									</ul>
 | 
				
			||||||
			</div>
 | 
								</div>
 | 
				
			||||||
			<input type="button" @click=${() => tfrpc.rpc.connect(self.renderRoot.getElementById('code').value)} value="Connect"></input>
 | 
					 | 
				
			||||||
			<h2>Broadcasts</h2>
 | 
					 | 
				
			||||||
			<ul>
 | 
					 | 
				
			||||||
				${this.broadcasts.filter(x => x.address).map(x => self.render_broadcast(x))}
 | 
					 | 
				
			||||||
			</ul>
 | 
					 | 
				
			||||||
			<h2>Connections</h2>
 | 
					 | 
				
			||||||
			<ul>
 | 
					 | 
				
			||||||
				${this.connections.map(x => html`
 | 
					 | 
				
			||||||
					<li>
 | 
					 | 
				
			||||||
						<input type="button" @click=${() => tfrpc.rpc.closeConnection(x)} value="Close"></input>
 | 
					 | 
				
			||||||
						<tf-user id=${x} .users=${this.users}></tf-user>
 | 
					 | 
				
			||||||
						${self.render_room_peers(x)}
 | 
					 | 
				
			||||||
					</li>
 | 
					 | 
				
			||||||
				`)}
 | 
					 | 
				
			||||||
			</ul>
 | 
					 | 
				
			||||||
			<h2>Stored Connections (WIP)</h2>
 | 
					 | 
				
			||||||
			<ul>
 | 
					 | 
				
			||||||
				${this.stored_connections.map(x => html`
 | 
					 | 
				
			||||||
					<li>
 | 
					 | 
				
			||||||
						<input type="button" @click=${() => self.forget_stored_connection(x)} value="Forget"></input>
 | 
					 | 
				
			||||||
						<input type="button" @click=${() => tfrpc.rpc.connect(x)} value="Connect"></input>
 | 
					 | 
				
			||||||
						${x.address}:${x.port} <tf-user id=${x.pubkey} .users=${self.users}></tf-user>
 | 
					 | 
				
			||||||
					</li>
 | 
					 | 
				
			||||||
				`)}
 | 
					 | 
				
			||||||
			</ul>
 | 
					 | 
				
			||||||
			<h2>Local Accounts</h2>
 | 
					 | 
				
			||||||
			<ul>
 | 
					 | 
				
			||||||
				${this.identities.map(x => html`<li><tf-user id=${x} .users=${this.users}></tf-user></li>`)}
 | 
					 | 
				
			||||||
			</ul>
 | 
					 | 
				
			||||||
		`;
 | 
							`;
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										78
									
								
								apps/ssb/tf-tab-mentions.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										78
									
								
								apps/ssb/tf-tab-mentions.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,78 @@
 | 
				
			|||||||
 | 
					import {LitElement, html, unsafeHTML} from './lit-all.min.js';
 | 
				
			||||||
 | 
					import * as tfrpc from '/static/tfrpc.js';
 | 
				
			||||||
 | 
					import {styles} from './tf-styles.js';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class TfTabMentionsElement extends LitElement {
 | 
				
			||||||
 | 
						static get properties() {
 | 
				
			||||||
 | 
							return {
 | 
				
			||||||
 | 
								whoami: {type: String},
 | 
				
			||||||
 | 
								users: {type: Object},
 | 
				
			||||||
 | 
								following: {type: Array},
 | 
				
			||||||
 | 
								expanded: {type: Object},
 | 
				
			||||||
 | 
								messages: {type: Array},
 | 
				
			||||||
 | 
							};
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						static styles = styles;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						constructor() {
 | 
				
			||||||
 | 
							super();
 | 
				
			||||||
 | 
							let self = this;
 | 
				
			||||||
 | 
							this.whoami = null;
 | 
				
			||||||
 | 
							this.users = {};
 | 
				
			||||||
 | 
							this.following = [];
 | 
				
			||||||
 | 
							this.expanded = {};
 | 
				
			||||||
 | 
							this.messages = [];
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						async load() {
 | 
				
			||||||
 | 
							console.log('Loading...', this.whoami);
 | 
				
			||||||
 | 
							let results = await tfrpc.rpc.query(
 | 
				
			||||||
 | 
								`
 | 
				
			||||||
 | 
									SELECT messages.id, messages.previous, messages.author, messages.sequence, messages.timestamp, messages.hash, json(messages.content) AS content, messages.signature
 | 
				
			||||||
 | 
									FROM messages_fts(?)
 | 
				
			||||||
 | 
									JOIN messages ON messages.rowid = messages_fts.rowid
 | 
				
			||||||
 | 
									JOIN json_each(?) AS following ON messages.author = following.value
 | 
				
			||||||
 | 
									WHERE messages.author != ?
 | 
				
			||||||
 | 
									ORDER BY timestamp DESC limit 20
 | 
				
			||||||
 | 
								`,
 | 
				
			||||||
 | 
								[
 | 
				
			||||||
 | 
									'"' + this.whoami.replace('"', '""') + '"',
 | 
				
			||||||
 | 
									JSON.stringify(this.following),
 | 
				
			||||||
 | 
									this.whoami,
 | 
				
			||||||
 | 
								]
 | 
				
			||||||
 | 
							);
 | 
				
			||||||
 | 
							console.log('Done.');
 | 
				
			||||||
 | 
							this.messages = results;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						on_expand(event) {
 | 
				
			||||||
 | 
							if (event.detail.expanded) {
 | 
				
			||||||
 | 
								let expand = {};
 | 
				
			||||||
 | 
								expand[event.detail.id] = true;
 | 
				
			||||||
 | 
								this.expanded = Object.assign({}, this.expanded, expand);
 | 
				
			||||||
 | 
							} else {
 | 
				
			||||||
 | 
								delete this.expanded[event.detail.id];
 | 
				
			||||||
 | 
								this.expanded = Object.assign({}, this.expanded);
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						render() {
 | 
				
			||||||
 | 
							let self = this;
 | 
				
			||||||
 | 
							if (!this.loading) {
 | 
				
			||||||
 | 
								this.loading = true;
 | 
				
			||||||
 | 
								this.load();
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							return html`
 | 
				
			||||||
 | 
								<tf-news
 | 
				
			||||||
 | 
									id="news"
 | 
				
			||||||
 | 
									whoami=${this.whoami}
 | 
				
			||||||
 | 
									.messages=${this.messages}
 | 
				
			||||||
 | 
									.users=${this.users}
 | 
				
			||||||
 | 
									.expanded=${this.expanded}
 | 
				
			||||||
 | 
									@tf-expand=${this.on_expand}
 | 
				
			||||||
 | 
								></tf-news>
 | 
				
			||||||
 | 
							`;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					customElements.define('tf-tab-mentions', TfTabMentionsElement);
 | 
				
			||||||
							
								
								
									
										211
									
								
								apps/ssb/tf-tab-news-feed.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										211
									
								
								apps/ssb/tf-tab-news-feed.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,211 @@
 | 
				
			|||||||
 | 
					import {LitElement, html, unsafeHTML, until} from './lit-all.min.js';
 | 
				
			||||||
 | 
					import * as tfrpc from '/static/tfrpc.js';
 | 
				
			||||||
 | 
					import {styles} from './tf-styles.js';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class TfTabNewsFeedElement extends LitElement {
 | 
				
			||||||
 | 
						static get properties() {
 | 
				
			||||||
 | 
							return {
 | 
				
			||||||
 | 
								whoami: {type: String},
 | 
				
			||||||
 | 
								users: {type: Object},
 | 
				
			||||||
 | 
								hash: {type: String},
 | 
				
			||||||
 | 
								following: {type: Array},
 | 
				
			||||||
 | 
								messages: {type: Array},
 | 
				
			||||||
 | 
								drafts: {type: Object},
 | 
				
			||||||
 | 
								expanded: {type: Object},
 | 
				
			||||||
 | 
							};
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						static styles = styles;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						constructor() {
 | 
				
			||||||
 | 
							super();
 | 
				
			||||||
 | 
							let self = this;
 | 
				
			||||||
 | 
							this.whoami = null;
 | 
				
			||||||
 | 
							this.users = {};
 | 
				
			||||||
 | 
							this.hash = '#';
 | 
				
			||||||
 | 
							this.following = [];
 | 
				
			||||||
 | 
							this.drafts = {};
 | 
				
			||||||
 | 
							this.expanded = {};
 | 
				
			||||||
 | 
							this.start_time = new Date().valueOf() - 24 * 60 * 60 * 1000;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						async fetch_messages() {
 | 
				
			||||||
 | 
							if (this.hash.startsWith('#@')) {
 | 
				
			||||||
 | 
								let r = await tfrpc.rpc.query(
 | 
				
			||||||
 | 
									`
 | 
				
			||||||
 | 
										WITH mine AS (SELECT id, previous, author, sequence, timestamp, hash, json(content) AS content, signature
 | 
				
			||||||
 | 
											FROM messages
 | 
				
			||||||
 | 
											WHERE messages.author = ?
 | 
				
			||||||
 | 
											ORDER BY sequence DESC
 | 
				
			||||||
 | 
											LIMIT 20)
 | 
				
			||||||
 | 
										SELECT 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
 | 
				
			||||||
 | 
											JOIN messages ON messages_refs.message = messages.id
 | 
				
			||||||
 | 
										UNION
 | 
				
			||||||
 | 
										SELECT * FROM mine
 | 
				
			||||||
 | 
									`,
 | 
				
			||||||
 | 
									[this.hash.substring(1)]
 | 
				
			||||||
 | 
								);
 | 
				
			||||||
 | 
								return r;
 | 
				
			||||||
 | 
							} else if (this.hash.startsWith('#%')) {
 | 
				
			||||||
 | 
								return await tfrpc.rpc.query(
 | 
				
			||||||
 | 
									`
 | 
				
			||||||
 | 
										SELECT id, previous, author, sequence, timestamp, hash, json(content) AS content, signature
 | 
				
			||||||
 | 
										FROM messages
 | 
				
			||||||
 | 
										WHERE id = ?1
 | 
				
			||||||
 | 
										UNION
 | 
				
			||||||
 | 
										SELECT 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
 | 
				
			||||||
 | 
									`,
 | 
				
			||||||
 | 
									[this.hash.substring(1)]
 | 
				
			||||||
 | 
								);
 | 
				
			||||||
 | 
							} 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.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.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.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)),
 | 
				
			||||||
 | 
												this.start_time,
 | 
				
			||||||
 | 
												/*
 | 
				
			||||||
 | 
												 ** Don't show messages more than a day into the future to prevent
 | 
				
			||||||
 | 
												 ** messages with far-future timestamps from staying at the top forever.
 | 
				
			||||||
 | 
												 */
 | 
				
			||||||
 | 
												new Date().valueOf() + 24 * 60 * 60 * 1000,
 | 
				
			||||||
 | 
											]
 | 
				
			||||||
 | 
										)
 | 
				
			||||||
 | 
									);
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								return [].concat(...(await Promise.all(promises)));
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						async load_more() {
 | 
				
			||||||
 | 
							let last_start_time = this.start_time;
 | 
				
			||||||
 | 
							this.start_time = last_start_time - 24 * 60 * 60 * 1000;
 | 
				
			||||||
 | 
							let more = await tfrpc.rpc.query(
 | 
				
			||||||
 | 
								`
 | 
				
			||||||
 | 
									WITH news AS (SELECT messages.id, messages.previous, messages.author, messages.sequence, messages.timestamp, messages.hash, json(messages.content) AS content, messages.signature
 | 
				
			||||||
 | 
									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.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.id, messages.previous, messages.author, messages.sequence, messages.timestamp, messages.hash, json(messages.content) AS content, messages.signature
 | 
				
			||||||
 | 
										FROM news
 | 
				
			||||||
 | 
										JOIN messages_refs ON news.id = messages_refs.message
 | 
				
			||||||
 | 
										JOIN messages ON messages_refs.ref = messages.id
 | 
				
			||||||
 | 
									UNION
 | 
				
			||||||
 | 
									SELECT news.* FROM news
 | 
				
			||||||
 | 
								`,
 | 
				
			||||||
 | 
								[JSON.stringify(this.following), this.start_time, last_start_time]
 | 
				
			||||||
 | 
							);
 | 
				
			||||||
 | 
							this.messages = await this.decrypt([...more, ...this.messages]);
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						async decrypt(messages) {
 | 
				
			||||||
 | 
							console.log('decrypt');
 | 
				
			||||||
 | 
							let result = [];
 | 
				
			||||||
 | 
							for (let message of messages) {
 | 
				
			||||||
 | 
								let content;
 | 
				
			||||||
 | 
								try {
 | 
				
			||||||
 | 
									content = JSON.parse(message?.content);
 | 
				
			||||||
 | 
								} catch {}
 | 
				
			||||||
 | 
								if (typeof content === 'string') {
 | 
				
			||||||
 | 
									let decrypted;
 | 
				
			||||||
 | 
									try {
 | 
				
			||||||
 | 
										decrypted = await tfrpc.rpc.try_decrypt(this.whoami, content);
 | 
				
			||||||
 | 
									} catch {}
 | 
				
			||||||
 | 
									if (decrypted) {
 | 
				
			||||||
 | 
										try {
 | 
				
			||||||
 | 
											message.decrypted = JSON.parse(decrypted);
 | 
				
			||||||
 | 
										} catch {
 | 
				
			||||||
 | 
											message.decrypted = decrypted;
 | 
				
			||||||
 | 
										}
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								result.push(message);
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							return result;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						async add_messages(messages) {
 | 
				
			||||||
 | 
							this.messages = await this.decrypt([...messages, ...this.messages]);
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						render() {
 | 
				
			||||||
 | 
							if (
 | 
				
			||||||
 | 
								!this.messages ||
 | 
				
			||||||
 | 
								this._messages_hash !== this.hash ||
 | 
				
			||||||
 | 
								this._messages_following !== this.following
 | 
				
			||||||
 | 
							) {
 | 
				
			||||||
 | 
								console.log(
 | 
				
			||||||
 | 
									`loading messages for ${this.whoami} (following ${this.following.length})`
 | 
				
			||||||
 | 
								);
 | 
				
			||||||
 | 
								let self = this;
 | 
				
			||||||
 | 
								this.messages = [];
 | 
				
			||||||
 | 
								this._messages_hash = this.hash;
 | 
				
			||||||
 | 
								this._messages_following = this.following;
 | 
				
			||||||
 | 
								this.fetch_messages()
 | 
				
			||||||
 | 
									.then(this.decrypt.bind(this))
 | 
				
			||||||
 | 
									.then(function (messages) {
 | 
				
			||||||
 | 
										self.messages = messages;
 | 
				
			||||||
 | 
										console.log(`loading mesages done for ${self.whoami}`);
 | 
				
			||||||
 | 
									})
 | 
				
			||||||
 | 
									.catch(function (error) {
 | 
				
			||||||
 | 
										alert(JSON.stringify(error, null, 2));
 | 
				
			||||||
 | 
									});
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							let more;
 | 
				
			||||||
 | 
							if (!this.hash.startsWith('#@') && !this.hash.startsWith('#%')) {
 | 
				
			||||||
 | 
								more = html`
 | 
				
			||||||
 | 
									<p>
 | 
				
			||||||
 | 
										<button class="w3-button w3-theme-d1" @click=${this.load_more}>
 | 
				
			||||||
 | 
											Load More
 | 
				
			||||||
 | 
										</button>
 | 
				
			||||||
 | 
									</p>
 | 
				
			||||||
 | 
								`;
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							return html`
 | 
				
			||||||
 | 
								<tf-news
 | 
				
			||||||
 | 
									id="news"
 | 
				
			||||||
 | 
									whoami=${this.whoami}
 | 
				
			||||||
 | 
									.users=${this.users}
 | 
				
			||||||
 | 
									.messages=${this.messages}
 | 
				
			||||||
 | 
									.following=${this.following}
 | 
				
			||||||
 | 
									.drafts=${this.drafts}
 | 
				
			||||||
 | 
									.expanded=${this.expanded}
 | 
				
			||||||
 | 
								></tf-news>
 | 
				
			||||||
 | 
								${more}
 | 
				
			||||||
 | 
							`;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					customElements.define('tf-tab-news-feed', TfTabNewsFeedElement);
 | 
				
			||||||
@@ -2,114 +2,6 @@ import {LitElement, html, unsafeHTML, until} from './lit-all.min.js';
 | 
				
			|||||||
import * as tfrpc from '/static/tfrpc.js';
 | 
					import * as tfrpc from '/static/tfrpc.js';
 | 
				
			||||||
import {styles} from './tf-styles.js';
 | 
					import {styles} from './tf-styles.js';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class TfTabNewsFeedElement extends LitElement {
 | 
					 | 
				
			||||||
	static get properties() {
 | 
					 | 
				
			||||||
		return {
 | 
					 | 
				
			||||||
			whoami: {type: String},
 | 
					 | 
				
			||||||
			users: {type: Object},
 | 
					 | 
				
			||||||
			hash: {type: String},
 | 
					 | 
				
			||||||
			following: {type: Array},
 | 
					 | 
				
			||||||
			messages: {type: Array},
 | 
					 | 
				
			||||||
			drafts: {type: Object},
 | 
					 | 
				
			||||||
			expanded: {type: Object},
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	static styles = styles;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	constructor() {
 | 
					 | 
				
			||||||
		super();
 | 
					 | 
				
			||||||
		let self = this;
 | 
					 | 
				
			||||||
		this.whoami = null;
 | 
					 | 
				
			||||||
		this.users = {};
 | 
					 | 
				
			||||||
		this.hash = '#';
 | 
					 | 
				
			||||||
		this.following = [];
 | 
					 | 
				
			||||||
		this.drafts = {};
 | 
					 | 
				
			||||||
		this.expanded = {};
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	async fetch_messages() {
 | 
					 | 
				
			||||||
		if (this.hash.startsWith('#@')) {
 | 
					 | 
				
			||||||
			let r = await tfrpc.rpc.query(
 | 
					 | 
				
			||||||
				`
 | 
					 | 
				
			||||||
					WITH mine AS (SELECT messages.*
 | 
					 | 
				
			||||||
						FROM messages
 | 
					 | 
				
			||||||
						WHERE messages.author = ?
 | 
					 | 
				
			||||||
						ORDER BY sequence DESC
 | 
					 | 
				
			||||||
						LIMIT 20)
 | 
					 | 
				
			||||||
					SELECT messages.*
 | 
					 | 
				
			||||||
						FROM mine
 | 
					 | 
				
			||||||
						JOIN messages_refs ON mine.id = messages_refs.ref
 | 
					 | 
				
			||||||
						JOIN messages ON messages_refs.message = messages.id
 | 
					 | 
				
			||||||
					UNION
 | 
					 | 
				
			||||||
					SELECT * FROM mine
 | 
					 | 
				
			||||||
				`,
 | 
					 | 
				
			||||||
				[
 | 
					 | 
				
			||||||
					this.hash.substring(1),
 | 
					 | 
				
			||||||
				]);
 | 
					 | 
				
			||||||
			return r;
 | 
					 | 
				
			||||||
		} else if (this.hash.startsWith('#%')) {
 | 
					 | 
				
			||||||
			return await tfrpc.rpc.query(
 | 
					 | 
				
			||||||
				`
 | 
					 | 
				
			||||||
					SELECT messages.*
 | 
					 | 
				
			||||||
					FROM messages
 | 
					 | 
				
			||||||
					WHERE id = ?1
 | 
					 | 
				
			||||||
					UNION
 | 
					 | 
				
			||||||
					SELECT messages.*
 | 
					 | 
				
			||||||
					FROM messages JOIN messages_refs
 | 
					 | 
				
			||||||
					ON messages.id = messages_refs.message
 | 
					 | 
				
			||||||
					WHERE messages_refs.ref = ?1
 | 
					 | 
				
			||||||
				`,
 | 
					 | 
				
			||||||
				[
 | 
					 | 
				
			||||||
					this.hash.substring(1),
 | 
					 | 
				
			||||||
				]);
 | 
					 | 
				
			||||||
		} else {
 | 
					 | 
				
			||||||
			return await tfrpc.rpc.query(
 | 
					 | 
				
			||||||
				`
 | 
					 | 
				
			||||||
					WITH news AS (SELECT messages.*
 | 
					 | 
				
			||||||
					FROM messages
 | 
					 | 
				
			||||||
					JOIN json_each(?) AS following ON messages.author = following.value
 | 
					 | 
				
			||||||
					WHERE messages.timestamp > ?
 | 
					 | 
				
			||||||
					ORDER BY messages.timestamp DESC)
 | 
					 | 
				
			||||||
					SELECT messages.*
 | 
					 | 
				
			||||||
						FROM news
 | 
					 | 
				
			||||||
						JOIN messages_refs ON news.id = messages_refs.ref
 | 
					 | 
				
			||||||
						JOIN messages ON messages_refs.message = messages.id
 | 
					 | 
				
			||||||
					UNION
 | 
					 | 
				
			||||||
					SELECT messages.*
 | 
					 | 
				
			||||||
						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),
 | 
					 | 
				
			||||||
					new Date().valueOf() - 24 * 60 * 60 * 1000,
 | 
					 | 
				
			||||||
				]);
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	render() {
 | 
					 | 
				
			||||||
		if (!this.messages ||
 | 
					 | 
				
			||||||
			this._messages_hash !== this.hash ||
 | 
					 | 
				
			||||||
			this._messages_following !== this.following) {
 | 
					 | 
				
			||||||
			console.log(`loading messages for ${this.whoami}`);
 | 
					 | 
				
			||||||
			let self = this;
 | 
					 | 
				
			||||||
			this.messages = [];
 | 
					 | 
				
			||||||
			this._messages_hash = this.hash;
 | 
					 | 
				
			||||||
			this._messages_following = this.following;
 | 
					 | 
				
			||||||
			this.fetch_messages().then(function(messages) {
 | 
					 | 
				
			||||||
				self.messages = messages;
 | 
					 | 
				
			||||||
				console.log(`loading mesages done for ${self.whoami}`);
 | 
					 | 
				
			||||||
			}).catch(function(error) {
 | 
					 | 
				
			||||||
				alert(JSON.stringify(error, null, 2));
 | 
					 | 
				
			||||||
			});
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
		return html`<tf-news id="news" whoami=${this.whoami} .users=${this.users} .messages=${this.messages} .following=${this.following} .drafts=${this.drafts} .expanded=${this.expanded}></tf-news>`;
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
class TfTabNewsElement extends LitElement {
 | 
					class TfTabNewsElement extends LitElement {
 | 
				
			||||||
	static get properties() {
 | 
						static get properties() {
 | 
				
			||||||
		return {
 | 
							return {
 | 
				
			||||||
@@ -120,7 +12,8 @@ class TfTabNewsElement extends LitElement {
 | 
				
			|||||||
			following: {type: Array},
 | 
								following: {type: Array},
 | 
				
			||||||
			drafts: {type: Object},
 | 
								drafts: {type: Object},
 | 
				
			||||||
			expanded: {type: Object},
 | 
								expanded: {type: Object},
 | 
				
			||||||
		}
 | 
								loading: {type: Boolean},
 | 
				
			||||||
 | 
							};
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	static styles = styles;
 | 
						static styles = styles;
 | 
				
			||||||
@@ -136,17 +29,29 @@ class TfTabNewsElement extends LitElement {
 | 
				
			|||||||
		this.cache = {};
 | 
							this.cache = {};
 | 
				
			||||||
		this.drafts = {};
 | 
							this.drafts = {};
 | 
				
			||||||
		this.expanded = {};
 | 
							this.expanded = {};
 | 
				
			||||||
		tfrpc.rpc.localStorageGet('drafts').then(function(d) {
 | 
							tfrpc.rpc.localStorageGet('drafts').then(function (d) {
 | 
				
			||||||
			self.drafts = JSON.parse(d || '{}');
 | 
								self.drafts = JSON.parse(d || '{}');
 | 
				
			||||||
		});
 | 
							});
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						connectedCallback() {
 | 
				
			||||||
 | 
							super.connectedCallback();
 | 
				
			||||||
 | 
							document.body.addEventListener('keypress', this.on_keypress.bind(this));
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						disconnectedCallback() {
 | 
				
			||||||
 | 
							super.disconnectedCallback();
 | 
				
			||||||
 | 
							document.body.removeEventListener('keypress', this.on_keypress.bind(this));
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	show_more() {
 | 
						show_more() {
 | 
				
			||||||
		let unread = this.unread;
 | 
							let unread = this.unread;
 | 
				
			||||||
		let news = this.renderRoot?.getElementById('news');
 | 
							let news = this.shadowRoot?.getElementById('news');
 | 
				
			||||||
		if (news) {
 | 
							if (news) {
 | 
				
			||||||
			console.log('injecting messages', news.messages);
 | 
								console.log('injecting messages', news.messages);
 | 
				
			||||||
			news.messages = Object.values(Object.fromEntries([...this.unread, ...news.messages].map(x => [x.id, x])));
 | 
								news.add_messages(
 | 
				
			||||||
 | 
									Object.values(Object.fromEntries(this.unread.map((x) => [x.id, x])))
 | 
				
			||||||
 | 
								);
 | 
				
			||||||
			this.dispatchEvent(new CustomEvent('refresh'));
 | 
								this.dispatchEvent(new CustomEvent('refresh'));
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
@@ -160,11 +65,16 @@ class TfTabNewsElement extends LitElement {
 | 
				
			|||||||
			let type = 'private';
 | 
								let type = 'private';
 | 
				
			||||||
			try {
 | 
								try {
 | 
				
			||||||
				type = JSON.parse(message.content).type || type;
 | 
									type = JSON.parse(message.content).type || type;
 | 
				
			||||||
			} catch {
 | 
								} catch {}
 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
			counts[type] = (counts[type] || 0) + 1;
 | 
								counts[type] = (counts[type] || 0) + 1;
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
		return 'Show New: ' + Object.keys(counts).sort().map(x => (counts[x].toString() + ' ' + x + 's')).join(', ');
 | 
							return (
 | 
				
			||||||
 | 
								'↻ Show New: ' +
 | 
				
			||||||
 | 
								Object.keys(counts)
 | 
				
			||||||
 | 
									.sort()
 | 
				
			||||||
 | 
									.map((x) => counts[x].toString() + ' ' + x + 's')
 | 
				
			||||||
 | 
									.join(', ')
 | 
				
			||||||
 | 
							);
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	draft(event) {
 | 
						draft(event) {
 | 
				
			||||||
@@ -175,10 +85,7 @@ class TfTabNewsElement extends LitElement {
 | 
				
			|||||||
		} else {
 | 
							} else {
 | 
				
			||||||
			delete this.drafts[id];
 | 
								delete this.drafts[id];
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
		/* Only trigger a re-render if we're creating a new draft or discarding an old one. */
 | 
							this.drafts = Object.assign({}, this.drafts);
 | 
				
			||||||
		if ((previous !== undefined) != (event.detail.draft !== undefined)) {
 | 
					 | 
				
			||||||
			this.drafts = Object.assign({}, this.drafts);
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
		tfrpc.rpc.localStorageSet('drafts', JSON.stringify(this.drafts));
 | 
							tfrpc.rpc.localStorageSet('drafts', JSON.stringify(this.drafts));
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -193,19 +100,69 @@ class TfTabNewsElement extends LitElement {
 | 
				
			|||||||
		}
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						on_keypress(event) {
 | 
				
			||||||
 | 
							if (event.target === document.body && event.key == '.') {
 | 
				
			||||||
 | 
								this.show_more();
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	render() {
 | 
						render() {
 | 
				
			||||||
		let profile = this.hash.startsWith('#@') ?
 | 
							let profile = this.hash.startsWith('#@')
 | 
				
			||||||
			html`<tf-profile id=${this.hash.substring(1)} whoami=${this.whoami} .users=${this.users}></tf-profile>` : undefined;
 | 
								? html`<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 html`
 | 
							return html`
 | 
				
			||||||
			<div><input type="button" value=${this.new_messages_text()} @click=${this.show_more}></input></div>
 | 
								<p class="w3-bar">
 | 
				
			||||||
			<a target="_top" href="#" ?hidden=${this.hash.length <= 1}>🏠Home</a>
 | 
									<button
 | 
				
			||||||
			<div>Welcome, <tf-user id=${this.whoami} .users=${this.users}></tf-user>!</div>
 | 
										class="w3-bar-item w3-button w3-theme-d1"
 | 
				
			||||||
			<div><tf-compose whoami=${this.whoami} .users=${this.users} .drafts=${this.drafts} @tf-draft=${this.draft}></tf-compose></div>
 | 
										@click=${this.show_more}
 | 
				
			||||||
 | 
									>
 | 
				
			||||||
 | 
										${this.new_messages_text()}
 | 
				
			||||||
 | 
									</button>
 | 
				
			||||||
 | 
								</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"
 | 
				
			||||||
 | 
										whoami=${this.whoami}
 | 
				
			||||||
 | 
										.users=${this.users}
 | 
				
			||||||
 | 
										.drafts=${this.drafts}
 | 
				
			||||||
 | 
										@tf-draft=${this.draft}
 | 
				
			||||||
 | 
									></tf-compose>
 | 
				
			||||||
 | 
								</div>
 | 
				
			||||||
			${profile}
 | 
								${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}></tf-tab-news-feed>
 | 
								<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}
 | 
				
			||||||
 | 
								></tf-tab-news-feed>
 | 
				
			||||||
		`;
 | 
							`;
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
customElements.define('tf-tab-news-feed', TfTabNewsFeedElement);
 | 
					 | 
				
			||||||
customElements.define('tf-tab-news', TfTabNewsElement);
 | 
					customElements.define('tf-tab-news', TfTabNewsElement);
 | 
				
			||||||
							
								
								
									
										136
									
								
								apps/ssb/tf-tab-query.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										136
									
								
								apps/ssb/tf-tab-query.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,136 @@
 | 
				
			|||||||
 | 
					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);
 | 
				
			||||||
@@ -9,7 +9,8 @@ class TfTabSearchElement extends LitElement {
 | 
				
			|||||||
			users: {type: Object},
 | 
								users: {type: Object},
 | 
				
			||||||
			following: {type: Array},
 | 
								following: {type: Array},
 | 
				
			||||||
			query: {type: String},
 | 
								query: {type: String},
 | 
				
			||||||
		}
 | 
								expanded: {type: Object},
 | 
				
			||||||
 | 
							};
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	static styles = styles;
 | 
						static styles = styles;
 | 
				
			||||||
@@ -20,28 +21,31 @@ class TfTabSearchElement extends LitElement {
 | 
				
			|||||||
		this.whoami = null;
 | 
							this.whoami = null;
 | 
				
			||||||
		this.users = {};
 | 
							this.users = {};
 | 
				
			||||||
		this.following = [];
 | 
							this.following = [];
 | 
				
			||||||
 | 
							this.expanded = {};
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	async search(query) {
 | 
						async search(query) {
 | 
				
			||||||
		console.log('Searching...', this.whoami, query);
 | 
							console.log('Searching...', this.whoami, query);
 | 
				
			||||||
		let search = this.renderRoot.getElementById('search');
 | 
							let search = this.renderRoot.getElementById('search');
 | 
				
			||||||
		if (search ) {
 | 
							if (search) {
 | 
				
			||||||
			search.value = query;
 | 
								search.value = query;
 | 
				
			||||||
			search.focus();
 | 
								search.focus();
 | 
				
			||||||
			search.select();
 | 
								search.select();
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
		await tfrpc.rpc.setHash('#q=' + encodeURIComponent(query));
 | 
							await tfrpc.rpc.setHash('#q=' + encodeURIComponent(query));
 | 
				
			||||||
		let results = await tfrpc.rpc.query(`
 | 
							let results = await tfrpc.rpc.query(
 | 
				
			||||||
				SELECT messages.*
 | 
								`
 | 
				
			||||||
 | 
									SELECT messages.id, messages.previous, messages.author, messages.sequence, messages.timestamp, messages.hash, json(messages.content) AS content, messages.signature
 | 
				
			||||||
				FROM messages_fts(?)
 | 
									FROM messages_fts(?)
 | 
				
			||||||
				JOIN messages ON messages.rowid = messages_fts.rowid
 | 
									JOIN messages ON messages.rowid = messages_fts.rowid
 | 
				
			||||||
				JOIN json_each(?) AS following ON messages.author = following.value
 | 
									JOIN json_each(?) AS following ON messages.author = following.value
 | 
				
			||||||
				ORDER BY timestamp DESC limit 100
 | 
									ORDER BY timestamp DESC limit 100
 | 
				
			||||||
			`,
 | 
								`,
 | 
				
			||||||
			['"' + query.replace('"', '""') + '"', JSON.stringify(this.following)]);
 | 
								['"' + query.replace('"', '""') + '"', JSON.stringify(this.following)]
 | 
				
			||||||
 | 
							);
 | 
				
			||||||
		console.log('Done.');
 | 
							console.log('Done.');
 | 
				
			||||||
		search = this.renderRoot.getElementById('search');
 | 
							search = this.renderRoot.getElementById('search');
 | 
				
			||||||
		if (search ) {
 | 
							if (search) {
 | 
				
			||||||
			search.value = query;
 | 
								search.value = query;
 | 
				
			||||||
			search.focus();
 | 
								search.focus();
 | 
				
			||||||
			search.select();
 | 
								search.select();
 | 
				
			||||||
@@ -55,17 +59,29 @@ class TfTabSearchElement extends LitElement {
 | 
				
			|||||||
		}
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						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() {
 | 
						render() {
 | 
				
			||||||
		if (this.query !== this.last_query) {
 | 
							if (this.query !== this.last_query) {
 | 
				
			||||||
 | 
								this.last_query = this.query;
 | 
				
			||||||
			this.search(this.query);
 | 
								this.search(this.query);
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
		let self = this;
 | 
							let self = this;
 | 
				
			||||||
		return html`
 | 
							return html`
 | 
				
			||||||
			<div style="display: flex; flex-direction: row">
 | 
								<div style="display: flex; flex-direction: row; gap: 4px">
 | 
				
			||||||
				<input type="text" id="search" value=${this.query} style="flex: 1" @keydown=${this.search_keydown}></input>
 | 
									<input type="text" class="w3-input w3-theme-d1" id="search" value=${this.query} style="flex: 1" @keydown=${this.search_keydown}></input>
 | 
				
			||||||
				<input type="button" value="Search" @click=${(event) => self.search(self.renderRoot.getElementById('search').value)}></input>
 | 
									<button class="w3-button w3-theme-d1" @click=${(event) => self.search(self.renderRoot.getElementById('search').value)}>Search</button>
 | 
				
			||||||
			</div>
 | 
								</div>
 | 
				
			||||||
			<tf-news id="news" whoami=${this.whoami} .messages=${this.messages} .users=${this.users}></tf-news>
 | 
								<tf-news id="news" whoami=${this.whoami} .messages=${this.messages} .users=${this.users} .expanded=${this.expanded} @tf-expand=${this.on_expand}></tf-news>
 | 
				
			||||||
		`;
 | 
							`;
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										28
									
								
								apps/ssb/tf-tag.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										28
									
								
								apps/ssb/tf-tag.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,28 @@
 | 
				
			|||||||
 | 
					import {LitElement, html, unsafeHTML} from './lit-all.min.js';
 | 
				
			||||||
 | 
					import {styles} from './tf-styles.js';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class TfTagElement extends LitElement {
 | 
				
			||||||
 | 
						static get properties() {
 | 
				
			||||||
 | 
							return {
 | 
				
			||||||
 | 
								tag: {type: String},
 | 
				
			||||||
 | 
								count: {type: Number},
 | 
				
			||||||
 | 
							};
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						static styles = styles;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						constructor() {
 | 
				
			||||||
 | 
							super();
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						render() {
 | 
				
			||||||
 | 
							let number = this.count ? html` (${this.count})` : undefined;
 | 
				
			||||||
 | 
							return html`<a
 | 
				
			||||||
 | 
								href="#q=${this.tag}"
 | 
				
			||||||
 | 
								style="display: inline-block; margin: 3px; border: 1px solid black; background-color: #444; padding: 4px; border-radius: 3px"
 | 
				
			||||||
 | 
								>${this.tag}${number}</a
 | 
				
			||||||
 | 
							>`;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					customElements.define('tf-tag', TfTagElement);
 | 
				
			||||||
@@ -7,7 +7,7 @@ class TfUserElement extends LitElement {
 | 
				
			|||||||
		return {
 | 
							return {
 | 
				
			||||||
			id: {type: String},
 | 
								id: {type: String},
 | 
				
			||||||
			users: {type: Object},
 | 
								users: {type: Object},
 | 
				
			||||||
		}
 | 
							};
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	static styles = styles;
 | 
						static styles = styles;
 | 
				
			||||||
@@ -19,25 +19,32 @@ class TfUserElement extends LitElement {
 | 
				
			|||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	render() {
 | 
						render() {
 | 
				
			||||||
 | 
							let image = html`<span
 | 
				
			||||||
 | 
								class="w3-theme-light w3-circle"
 | 
				
			||||||
 | 
								style="display: inline-block; width: 2em; height: 2em; text-align: center; line-height: 2em"
 | 
				
			||||||
 | 
								>?</span
 | 
				
			||||||
 | 
							>`;
 | 
				
			||||||
		let name = this.users?.[this.id]?.name;
 | 
							let name = this.users?.[this.id]?.name;
 | 
				
			||||||
		name = name !== undefined ?
 | 
							name =
 | 
				
			||||||
			html`<a target="_top" href=${'#' + this.id}>${name}</a>` :
 | 
								name !== undefined
 | 
				
			||||||
			html`<a target="_top" href=${'#' + this.id}>${this.id}</a>`;
 | 
									? html`<a target="_top" href=${'#' + this.id}>${name}</a>`
 | 
				
			||||||
 | 
									: html`<a target="_top" href=${'#' + this.id}>${this.id}</a>`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		if (this.users[this.id]) {
 | 
							if (this.users[this.id]) {
 | 
				
			||||||
			let image = this.users[this.id].image;
 | 
								let image_link = this.users[this.id].image;
 | 
				
			||||||
			image = typeof(image) == 'string' ? image : image?.link;
 | 
								image_link =
 | 
				
			||||||
			return html`
 | 
									typeof image_link == 'string' ? image_link : image_link?.link;
 | 
				
			||||||
				<div style="display: inline-block; font-weight: bold">
 | 
								if (image_link !== undefined) {
 | 
				
			||||||
						<img style="width: 2em; height: 2em; vertical-align: middle; border-radius: 50%" ?hidden=${image === undefined} src="${image ? '/' + image + '/view' : undefined}">
 | 
									image = html`<img
 | 
				
			||||||
						${name}
 | 
										class="w3-circle"
 | 
				
			||||||
				</div>`;
 | 
										style="width: 2em; height: 2em; vertical-align: middle"
 | 
				
			||||||
		} else {
 | 
										src="/${image_link}/view"
 | 
				
			||||||
			return html`
 | 
									/>`;
 | 
				
			||||||
				<div style="display: inline-block; font-weight: bold">
 | 
								}
 | 
				
			||||||
					${name}
 | 
					 | 
				
			||||||
				</div>`;
 | 
					 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
							return html` <div style="display: inline-block; font-weight: bold">
 | 
				
			||||||
 | 
								${image} ${name}
 | 
				
			||||||
 | 
							</div>`;
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user