Compare commits
1823 Commits
Author | SHA1 | Date | |
---|---|---|---|
09ae4e2096 | |||
0b46efe4ea | |||
f1dda43e66 | |||
ce483138d7 | |||
73cc39226d | |||
57257f63dd | |||
88b25790e8 | |||
e01defc4aa | |||
cb50c43e93 | |||
5908d15f91 | |||
f66cfaec12 | |||
259f92c53b | |||
a84f850e91 | |||
5a765e6f07 | |||
791889c659 | |||
5da63faf1f | |||
30d108fc35 | |||
a09fefab5e | |||
f74ca1c236 | |||
30e027092b | |||
fd4ac7c9b9 | |||
4482049b94 | |||
5839380437 | |||
2152470fdc | |||
93b2a81495 | |||
e139e952c0 | |||
cf1c57ccb8 | |||
f7a2138488 | |||
9614d03bef | |||
32a335c676 | |||
06e27fc1e0 | |||
1f40e8dcd9 | |||
77ff8cef1f | |||
ef844fbccb | |||
070dc5a4c0 | |||
177ef1cdcc | |||
4b1ebf02e1 | |||
863e50203e | |||
01b8c209de | |||
30e92f2bc1 | |||
02accabb4a | |||
fa00a41fe0 | |||
2e66666bdf | |||
4fe3c9a751 | |||
0a35e14590 | |||
e979c176e3 | |||
a0d9c3dc29 | |||
efcb68351c | |||
94e8bf2e1c | |||
82d1a294a6 | |||
de20274589 | |||
2f193e64c8 | |||
86751362cb | |||
4118323631 | |||
0d134f7f10 | |||
409724cfcd | |||
799a33be40 | |||
2fa9c66495 | |||
ad818a8e7e | |||
581f72b3f8 | |||
1dd7e4347c | |||
36cc9398c7 | |||
68817feeec | |||
97661e2ca2 | |||
72def5ae6d | |||
e638b155a1 | |||
32db18b0d6 | |||
b653a5250d | |||
30329f7cad | |||
29a1478c86 | |||
c882bf31ec | |||
17ccb8f083 | |||
0e7d2a8b0e | |||
3743543ef8 | |||
700dd7b45a | |||
c2eb73fd8a | |||
e1f4f7f95b | |||
37401409c6 | |||
b282631cd5 | |||
9618d3b3f3 | |||
c9f997d121 | |||
f1dee2a089 | |||
8273277c91 | |||
9758844da3 | |||
e41c7fbbc7 | |||
24db8a5a49 | |||
36e82b9873 | |||
8a32f2b8b1 | |||
277830bc3c | |||
a8fa969114 | |||
c3f3dced68 | |||
85fce59c0c | |||
8a6147d512 | |||
e799b256b2 | |||
b222dc0ca8 | |||
c52c6b04ca | |||
b95eed46bb | |||
7c36a543da | |||
90e000c18e | |||
1bb9d737d8 | |||
9a5db2ec51 | |||
dbed29a044 | |||
681859531c | |||
8e1ad6b16a | |||
5448f1ba2d | |||
e43da4e1a3 | |||
eaa9da49cc | |||
40873b529c | |||
8cc4c19d73 | |||
bb9c18faf1 | |||
fabdfb76b9 | |||
bce263a928 | |||
195920e476 | |||
a821d895c5 | |||
ab1b6ec27d | |||
6dc099809f | |||
03c8b75994 | |||
38887452ad | |||
7512edad59 | |||
944c895bcd | |||
e7d87ee8e2 | |||
cfdbd10635 | |||
d3a2d8733f | |||
a7e623d817 | |||
3f0c37cea4 | |||
2c96a6d22a | |||
57b4214a72 | |||
433b3d39d9 | |||
26441ed45c | |||
92cd38c2a0 | |||
3b5a06794f | |||
d104409272 | |||
e5f58c2898 | |||
f83863ef01 | |||
837f069cf5 | |||
9f057dc29a | |||
c4904f176c | |||
d3a5aba703 | |||
9e283e427c | |||
133ba31d66 | |||
241a65a92a | |||
0b54795bab | |||
6208193de5 | |||
c53321532f | |||
34f25e3e06 | |||
c46244366e | |||
6518af04fc | |||
bf137ff1f7 | |||
1877955b62 | |||
50d0875de2 | |||
bf151e6b7d | |||
82893402d0 | |||
8049102787 | |||
f42cc3d9fd | |||
5f9a5208db | |||
6df506d238 | |||
2bd3354256 | |||
b55aaa1d18 | |||
34e19505bd | |||
6e06ec0904 | |||
a5814074fe | |||
d7479df5a2 | |||
34508aa0ae | |||
ae096b2c9c | |||
95d036e34a | |||
4af5e8ec42 | |||
2a5f71bd5d | |||
97fb63dda1 | |||
87d42e3b3b | |||
0394129a4c | |||
3c499c834b | |||
17d6cc7d46 | |||
646bd7dc38 | |||
56e483782d | |||
e1b9066b26 | |||
7114ce2516 | |||
9240c6570a | |||
f80a44ccd7 | |||
e6f5eb244e | |||
ab62e83110 | |||
aeefb9e536 | |||
ee0efa536a | |||
2523130fdc | |||
c024777184 | |||
5951d7cd2d | |||
011670c70b | |||
6cebd6c769 | |||
546ae5cbf1 | |||
f543cc642e | |||
8ac3c5ea22 | |||
63918f0680 | |||
bfb3d8b8a2 | |||
e38ff99607 | |||
b0e3d922c8 | |||
a15bb8e994 | |||
6f487100cd | |||
0693a2315f | |||
f360e886ff | |||
6ea08cc5dc | |||
347c706d6f | |||
5f5e6616c7 | |||
657bcadc7e | |||
107666cc60 | |||
b37669184a | |||
163a01f224 | |||
3d58094199 | |||
463951a4f1 | |||
34804d5162 | |||
3895c33915 | |||
17f4eb1a56 | |||
0abdffdea6 | |||
d32999f178 | |||
f621feb843 | |||
8d277f029d | |||
1788a02338 | |||
ba0800d16c | |||
4008c7d8f6 | |||
610a2e2afc | |||
6f3715d1eb | |||
b78ecaa814 | |||
e6f5399d53 | |||
0e5806cadd | |||
68c9d4afa7 | |||
f0ea38fe49 | |||
b0332f923e | |||
8a76c25394 | |||
fd96126e3e | |||
ff3fbedc18 | |||
8791419f8e | |||
5447b247a0 | |||
aabbb10564 | |||
3ccd6c9a3e | |||
c290240de7 | |||
8e799b174b | |||
a9c3a93989 | |||
3ef8698f42 | |||
fa4e843c30 | |||
9a4d11f4d9 | |||
eed2b8d618 | |||
13f02c2aca | |||
d50f8fbc8b | |||
155238a516 | |||
427fcdbdca | |||
ca05d402a7 | |||
c5a80b68ca | |||
c1fb15b135 | |||
4b2c131836 | |||
9ca1e69b3c | |||
082d041d44 | |||
221f276c4b | |||
24cec21465 | |||
9f71ec6194 | |||
bb36afc390 | |||
b53bf0ff64 | |||
3ebc6f2436 | |||
2eef6778a6 | |||
81fabec810 | |||
dc6e7924b5 | |||
48dec5a2c8 | |||
9b500e1da9 | |||
a038820112 | |||
70a15973b6 | |||
09b6a00731 | |||
883c3cf0e9 | |||
a46bb8183c | |||
d5d5a7b012 | |||
a120efdc91 | |||
d48f4b06eb | |||
f078912736 | |||
63b0f0dedd | |||
84c22dbf5f | |||
b8cd1232be | |||
a518ab07f4 | |||
9e5a1ee975 | |||
95bf3f0316 | |||
d69dd513bc | |||
525cdf571a | |||
9cfe0a8804 | |||
50b54599ef | |||
ed6bef6d24 | |||
71268636df | |||
568729ecd6 | |||
9139725be6 | |||
969a8da6bf | |||
2338b26329 | |||
d4df206740 | |||
8a93cdd33c | |||
92b31de4a9 | |||
5452f3f623 | |||
256614dbaf | |||
049449b213 | |||
85b46336b1 | |||
590afa7b01 | |||
574292b798 | |||
21cf503a59 | |||
3630cdbfe0 | |||
0f3be229e6 | |||
8e5a024d3d | |||
410bb7c09d | |||
9de8b0f449 | |||
d47c3a1222 | |||
df99b3aa90 | |||
0090850e10 | |||
9efd64bd18 | |||
b16c37e48b | |||
3ee2c00726 | |||
d5a7e19f1a | |||
9b52415b35 | |||
dbe24494d9 | |||
3eab5a5f70 | |||
548febfb22 | |||
b40f72443a | |||
2c03496373 | |||
b6a937c954 | |||
63776d40bd | |||
cb3c7afade | |||
991022adfc | |||
2bc71a18a6 | |||
57ca864fbb | |||
a09edfb612 | |||
7997a739ab | |||
248b258413 | |||
0423ed7fb4 | |||
c29378c2f8 | |||
163fbd85e7 | |||
58bb86ebe1 | |||
c5140ee8e8 | |||
6270fd8118 | |||
3fff706848 | |||
c259defab5 | |||
e5fee5c306 | |||
9d35b4bdfb | |||
9497d7cf64 | |||
c7d3e602cb | |||
0076eb4ed4 | |||
6070bde413 | |||
c7a6d426f0 | |||
f66cf0f802 | |||
e4b6c81024 | |||
44d784cd04 | |||
0394201113 | |||
e270c16516 | |||
4c10538632 | |||
71329c5532 | |||
feb4bf9e87 | |||
5d5567e94c | |||
684e6fb9cb | |||
ee21fa6d03 | |||
7a2974e54f | |||
f4dfc1dd98 | |||
2eebfa9a7a | |||
10097ffeb8 | |||
cbe1f54a2a | |||
4d8f081a59 | |||
29e79c9484 | |||
ba35869b0a | |||
580688381e | |||
e63d69a440 | |||
be64fe04fb | |||
801ab20723 | |||
d974a5e044 | |||
1be94ae0be | |||
b883e6a485 | |||
a0210379ae | |||
e56dc207d1 | |||
523c9c9ad2 | |||
74bb2151c1 | |||
f79d7b35a4 | |||
3b36496dac | |||
4ebd6c24a9 | |||
05451d98b3 | |||
22a4bce3c8 | |||
76d499f00b | |||
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 | |||
0d9fac7363 | |||
2fb91fccc0 | |||
24e1ab12ab | |||
10ea885d8d | |||
ec65faa12d | |||
53692a1ea8 | |||
ebef51b4ea | |||
a94d6f9271 | |||
3d2c88c201 | |||
bdeee7fc0e | |||
33a037e0ea | |||
2dc2d9ebf6 | |||
9748f0ed8b | |||
d6be2f7d54 | |||
63615747a7 | |||
fbb657a85c | |||
bdac0c7879 | |||
54dde76a8a | |||
2bbe22bc7a | |||
ad8532f7ac | |||
602941104e | |||
d38b41687c | |||
08125cd1e8 | |||
2ce2097a3f | |||
a5da17e1b1 | |||
2b0962f087 | |||
37173cce4c | |||
37edbd9824 | |||
a32bb02223 | |||
2ab1b84432 | |||
52ae19220c | |||
10bfa65a4e | |||
2a3b1a1e33 | |||
f74f4f6da9 | |||
12a8b7a058 | |||
400f07660f | |||
d532795b7f | |||
6064ed6a3a | |||
2c1a43df2e | |||
bf72782c9f | |||
63dcab30c3 | |||
50e48af7c4 | |||
9127a18ff0 | |||
61ff466908 | |||
1c10768aa4 | |||
992b123853 | |||
f736756b20 | |||
28d73f5b37 | |||
262b0e5e52 | |||
1e3807bcb9 | |||
2ed3295f77 | |||
8c9d687d50 | |||
b8b694864e | |||
961109635b | |||
86bc46a11e | |||
a6a6fe75ec | |||
f55f863867 | |||
4ce988d00b | |||
1548a8a852 | |||
a9551b057b | |||
88c7d91858 | |||
53cb80ebf7 | |||
1f67343d75 | |||
4bea8bb6ba | |||
8e1461b3f1 | |||
90b513d070 | |||
8a2d3d4669 | |||
1741403206 | |||
980db880cc | |||
507a62539d | |||
6b5d73ed5c | |||
1f77df7a90 | |||
fa87462405 | |||
a5f9f927e6 | |||
b35d74ce36 | |||
ac60be14a5 | |||
beda047eb0 | |||
f6742bebf3 | |||
7f334ad783 | |||
ffda896308 | |||
b2fbe9dfac | |||
6d6c41bffa | |||
e04d137af5 | |||
ec52e62908 | |||
6104af0d70 | |||
0ca05e297d | |||
e0dcec074c | |||
a8cecb5c64 | |||
582ee0e4d7 | |||
0ba54c2b7b | |||
3c288f7f68 | |||
c692b1b1f8 | |||
7091b6e6a5 | |||
48cd08e095 | |||
ef7f9db9c4 | |||
0092f24fb9 | |||
f9db1a7acf | |||
da75ad9337 | |||
7318ddd70e | |||
ab75ec07f8 | |||
0a6b842179 | |||
5d5ff121f9 | |||
adefa76dfd | |||
2420869e7f | |||
f841ca4399 | |||
433db904cd | |||
c067623740 | |||
dab7050899 | |||
77df158178 | |||
0af1bcf110 | |||
e05302ac99 | |||
ce6cc82d64 | |||
85a2bc3f0f | |||
3285d93576 | |||
0f11f497ed | |||
45a5202456 | |||
ce0b4de5a1 | |||
134b2556ad | |||
67d34bf70e | |||
73863f9418 | |||
0cbc1a650b | |||
9248dfd97e | |||
b8f54f324f | |||
3269c7ca45 | |||
8a1b4cceec | |||
7cd925feca | |||
f6ae15c4dc | |||
6ed057089b | |||
a5ba014736 | |||
4d4cc92150 | |||
3b00b31e87 | |||
3c687dc780 | |||
987b2d539a | |||
80a1e94da4 | |||
69253432b8 | |||
53e4f4341c | |||
6ff33191bb | |||
513eb88a53 | |||
3506d9dec1 | |||
c09e043812 | |||
4c01f23ee8 | |||
ff06e91ac8 | |||
8ed359327c | |||
a66a70324d | |||
67fbbd4a8d | |||
235fc9b8f9 | |||
f257cccded | |||
5342ddb2bd | |||
7cba1b21ad | |||
120ed36552 | |||
a9f6593979 | |||
ca6d042ed6 | |||
ae4c2aef69 | |||
ed1c85288c | |||
71151a511d | |||
7f35f01b88 | |||
1d13c25ded | |||
09ddfffa6b | |||
d9aee6d05f | |||
94d7d2e3e0 | |||
f748fcf1f7 | |||
9c89c2f717 | |||
d88752d840 | |||
bb565aeb23 | |||
c1015a8bdd | |||
181b21080c | |||
577efb6b7a | |||
1a45113e0c | |||
c49da3db07 | |||
b406501263 | |||
c30b3bbb64 | |||
82f9859c57 | |||
4080266fa3 | |||
210149d6be | |||
c2eb439574 | |||
23a6a24288 | |||
932989ee9c | |||
1a91b56a1d | |||
8115881c08 | |||
7fe3bddeba | |||
376094452e | |||
cd8b32b3ca | |||
2251406bd1 | |||
d8fb956c14 | |||
b1ff215ad7 | |||
f9b4ab91c0 | |||
72952e0c39 | |||
acc14f7318 | |||
d48b8b0ae1 | |||
7ff09ed005 | |||
672fb8fcf4 | |||
fe6d492347 | |||
b65706ffc4 | |||
cb44d408cd | |||
880ab7fdde | |||
be6f24b3ee | |||
902287292d | |||
c4b4103802 | |||
c664f7808f | |||
6b267e472e | |||
dae66424dc | |||
170d5a9621 | |||
179da40a4b | |||
9b696503de | |||
efdecc6017 | |||
1a35a6a161 | |||
041e63ac70 | |||
046bf7e2a9 | |||
20ebdea9d1 | |||
1e84b74ced | |||
cdbc2d48f7 | |||
1140c5ddc7 | |||
0d23294d42 | |||
2dc7f58c80 | |||
de59a7f338 | |||
205f0df1b4 | |||
5ed9a77d38 | |||
ae545e7b2b | |||
e49b54207a | |||
c1df77bb96 | |||
98a7753a55 | |||
d3d4b1a13c | |||
241dfdb90e | |||
6ba41f03da | |||
9ef9dadbb8 | |||
06529fddfb | |||
f015c8727d | |||
3a5ae4c228 | |||
b12f8f9da8 | |||
1abc611e54 | |||
6a4559c580 | |||
54ebd0e643 | |||
60d1ea9d39 | |||
16dbc7617c | |||
a37ad69c8b | |||
3bbeec8ece | |||
de398786be | |||
b8fa59d3ec | |||
704ed737a9 | |||
04ae7a2540 | |||
954e0227d4 | |||
1ab79adb27 | |||
c7ee998b21 | |||
f53ce584e3 | |||
70866e03c8 | |||
656ab7beb6 | |||
1dec53821e | |||
c0a14a738e | |||
d9c5f74d62 | |||
e5dcff0200 | |||
25cc3d7c3a | |||
1cffc5ec24 | |||
5e72b111d9 | |||
3cdfc7af2b | |||
113a82b382 | |||
5b3ae3f006 | |||
c0ecdaae12 | |||
828f61c4e9 | |||
775f00c69c | |||
eadda41518 | |||
8279ec5e9e | |||
ab1f47ee9a | |||
d216d96144 | |||
88592886ca | |||
7077e69bf7 | |||
f983c3d987 | |||
26691051a5 | |||
fe33903e2e | |||
6ea6ae2322 | |||
bb0a840dc6 | |||
52f5bb408f | |||
ee1e1b11af | |||
56db6a8e4d | |||
3b676d967e | |||
97b7643049 | |||
c3fb80a1c8 | |||
7c29c1e18e | |||
4c0dc6ad04 | |||
6cfe0ca4fb | |||
46d3e8f567 | |||
3729346961 | |||
357d944a8d | |||
69991abbb4 | |||
0518c5dd21 | |||
e4c182a6fa | |||
8edc9aaa63 | |||
4525ee9cca | |||
3464f1d189 | |||
fc9c3982c2 | |||
d70dba021a | |||
41590921c3 | |||
4d629c45eb | |||
39f05b6bf5 | |||
58196c4ac0 | |||
eca3696740 | |||
fbfbd6a6b4 | |||
353f2ccc13 | |||
6628a5c420 | |||
1973030774 | |||
787e439524 | |||
fab2c17b43 | |||
1775fdd6b5 | |||
5cc7641788 | |||
6c2fd6d90f | |||
24530e1158 | |||
0bd1463a6b | |||
6728727e89 | |||
ac960a98bf | |||
f787eb077b | |||
b2ecc24e85 | |||
3c82a87968 | |||
f06753b56e | |||
41afc3bdd6 | |||
f764007fc6 | |||
ae5560f33a | |||
e6532979aa | |||
3078536245 | |||
1efc0fd73b | |||
aee99af953 | |||
a154b1c2f6 | |||
7f350a3d87 | |||
982b5817a2 | |||
52f744e106 | |||
7f9c01a9bb | |||
fb3ad0d95d | |||
fe5a6033ef | |||
ff2a0f0c3f | |||
66ea0dadd0 | |||
474ff9cd74 | |||
718383205b | |||
c9e01f220d | |||
f69e74ce53 | |||
b5c6cac048 | |||
515999e570 | |||
ab58f42f0c | |||
af3e96c7e8 | |||
782b5593d5 | |||
d892c9e734 | |||
d3e9041b15 | |||
3a40722c89 | |||
b42b5d11fa | |||
2d8a956c14 | |||
ed6550a4cd | |||
e1ca715c64 | |||
4e3bf99327 | |||
b5b6ed8ba5 | |||
4293e75082 | |||
927e2b7060 | |||
83bdbbb4dc | |||
1dc6084d2d | |||
ae894eaa9d | |||
a8ced8757c | |||
653e16b059 | |||
9c90b2bc1d | |||
cf61e68713 | |||
7b53c95832 | |||
c8e09d8637 | |||
cb9edaacd4 | |||
2992b7e955 | |||
5622db92a7 | |||
58f459fb3b | |||
3bc428a83e | |||
0556af3e07 | |||
2882af1c05 | |||
b06c657ef0 | |||
04ec425c9c | |||
842633f6d1 | |||
e5160b9d2c | |||
e8fb73fdf9 | |||
939e13c3c8 | |||
787e929747 | |||
b688a89b66 | |||
7848b5e560 | |||
87224d2bb6 | |||
2826efea56 | |||
0d1b231344 | |||
804359d12e | |||
11ad344e52 | |||
d802c0023b | |||
42fcfee042 | |||
a1d244567a | |||
00bdf1df4a | |||
9b2d4b393d | |||
5e0c20e432 | |||
352f33f5a1 | |||
efc5eb2aff | |||
7e9460f47c | |||
41cabad264 | |||
b488db9137 | |||
cb315c717b | |||
3381b588a1 | |||
7c2962afcf | |||
498a093cde | |||
07a87ff9de | |||
c138582638 | |||
011038a38a | |||
1bfa18b8d7 | |||
95f0b91a0e | |||
ffaaec5b37 | |||
ac0482d7f5 | |||
f4b46cc3a0 | |||
4bb095e81f | |||
5e92e2ffe1 | |||
a4a0745385 | |||
eb191254b0 | |||
e4e763b7a0 | |||
5ffc505ce2 | |||
1bdd67d659 | |||
483638a7e6 | |||
50bef73200 | |||
d4135f7133 | |||
557ae6ee5a | |||
07b4f2b08f | |||
9a75af8146 | |||
5b3c7dcecc | |||
6b20d69976 | |||
fbb61581c6 | |||
25d793e9e8 | |||
8f35004a01 | |||
e59eb66c1d | |||
412dce0a47 | |||
1aa4b0e590 | |||
ef9e42e030 | |||
059024452c | |||
39a1acaf38 | |||
8ecc07452e | |||
e85ee5766b | |||
91339dc8a7 | |||
7733cb2604 | |||
157209e9b5 | |||
cd51edcd8f | |||
a98a848bb7 | |||
c57b0a2f2f | |||
bf7d5c34f6 | |||
ea92fbdcea | |||
9f75346dd8 | |||
b5111efc29 | |||
0278aceb62 | |||
ec5d7c1a01 | |||
40216377f9 | |||
d062db2ba8 | |||
d3875cf738 | |||
fefb0f92bc | |||
0ddb86b5a8 | |||
d77c452120 | |||
e84ced6f79 | |||
e4d77679dc | |||
9fd4be0e4a | |||
7b32067b07 | |||
e1167b6854 | |||
25ee0a3561 | |||
4771810d6b | |||
ae10d3fa6f | |||
ac92b5f8de | |||
d0c89991be | |||
0a580b60b1 | |||
24116f498f | |||
5623cba7c3 | |||
bd81b2acf5 | |||
6c28ca738e | |||
b2a552b3e0 | |||
0f03701043 | |||
d470d6c398 | |||
98de9b037a | |||
df0bb102dc | |||
1734c88627 | |||
df94378b96 | |||
83fa488b8d | |||
1515525a1b | |||
0b5017b208 | |||
c864041fa0 | |||
e9e1a3e80d | |||
1ddaa7deb0 | |||
cf56078e25 | |||
a156cdea9f | |||
4637509b3d | |||
1ae9aaf752 | |||
11cd707382 | |||
1807264df5 | |||
e927ff915b | |||
b8068b9ed1 | |||
019ab99ecc | |||
c40a513876 | |||
dbfa9e5623 | |||
5e0304481b | |||
0bcc7d8c59 | |||
27c2f27708 | |||
e1f868730f | |||
7ba1e6980f | |||
35b7eb511a | |||
d51eb64c8e | |||
53aac8d23a | |||
9d105bdc1a | |||
873019f054 | |||
7f8155613c | |||
5f96eb18b2 | |||
32c7fcbbfa | |||
c28d4d9378 | |||
6af9c17efe | |||
ec9e9151dc | |||
86aa5e4d1e | |||
26150f98e1 | |||
bb81fc87b9 | |||
700d09c730 | |||
50d860183d | |||
1cf55d7d64 | |||
ae84f69025 | |||
49ffd1055e | |||
e2c25ab414 | |||
c02a3d3659 | |||
24cf18651a | |||
3eabe72299 | |||
e1448a1c3a | |||
df5dfa1539 | |||
23b15a8dc5 | |||
d550092bd3 | |||
4268963e70 | |||
3026443c1e | |||
4e359c3f5c | |||
4f1b31bce0 | |||
b980bb4946 | |||
fafc524c8c | |||
0cab3e7ed9 | |||
12010a84a3 | |||
f7974d2cef | |||
62e9dfea90 | |||
0bf216bb1a | |||
aba95d4fe8 | |||
5e205ac897 | |||
0f7472fa22 | |||
12ab2f4b85 | |||
f676cd937f | |||
263a59f6c5 | |||
2e1e4f90e7 | |||
6eed168b7d | |||
9f0315458f | |||
c590eb3a44 | |||
2e1b0089ae | |||
05b55c849a | |||
5fbe9c42bc | |||
3cddc524d1 | |||
efcada8e25 | |||
c616a16993 | |||
d4f7fdfc40 | |||
f760d48368 | |||
ba87f9acaa | |||
9faa4c9ca6 | |||
58b0b54785 | |||
e3b83c10db | |||
7f31798119 | |||
d9ffca81f8 | |||
1dd3b3c9aa | |||
8075bdfe99 | |||
b15cf901ad | |||
84a3d7348d | |||
00c1ec660e | |||
9e1bab03eb | |||
63c344112d | |||
18c90214a8 | |||
68cf3efcde | |||
308e24c9fa | |||
2fb7fceb0c | |||
fde7fb4270 | |||
03a2367532 | |||
08cd0ec878 | |||
0a01332d1f | |||
256c47c33c | |||
62ad08985c | |||
21ba7cb02c | |||
77ec1a0b2e | |||
07a0828626 | |||
08e32c0de4 | |||
f4f6bb8333 | |||
b1a6384ac1 | |||
786c83c57c | |||
843c53e15e | |||
470814f147 | |||
24a91219c1 | |||
d3e02470cd | |||
6d2b560c3d | |||
059392df8e | |||
3b4f0c1321 | |||
a09d159268 | |||
91ec68252d | |||
e85168ac53 | |||
35e0d8b68a | |||
cadcb236ee | |||
cfd5341a6b | |||
e922af4c55 | |||
45dfe34375 | |||
c78d3b0413 | |||
dd90fe4fbf | |||
be6a39bd15 | |||
da51e87774 | |||
5197eb91f7 | |||
87747c0b6b | |||
03cf347394 | |||
3487f335e5 | |||
8c0d380a4d | |||
b660abff7f | |||
5988fddf8d | |||
f268ca3adf | |||
cbc21cfbe6 | |||
cf195cdd44 | |||
85c5b4c4d6 | |||
7d8258c262 | |||
92c06b34a9 | |||
7012418b13 | |||
2b5a56abfe | |||
d8657866f5 | |||
a2851f8ade | |||
ff4c144be3 | |||
3650cd8350 |
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: true
|
||||
TabWidth: 4
|
||||
UseTab: Always
|
||||
...
|
4
.dockerignore
Normal file
4
.dockerignore
Normal file
@ -0,0 +1,4 @@
|
||||
.svn
|
||||
db.sqlite
|
||||
out/**/*.o
|
||||
out/**/*.d
|
2
.git-blame-ignore-revs
Normal file
2
.git-blame-ignore-revs
Normal file
@ -0,0 +1,2 @@
|
||||
# Add prettier to the project
|
||||
41024ddb7961b04a5688bbc997cb74de6fab4763
|
38
.gitea/workflows/build.yaml
Normal file
38
.gitea/workflows/build.yaml
Normal file
@ -0,0 +1,38 @@
|
||||
name: Build Tilde Friends
|
||||
run-name: ${{ gitea.actor }} running 🚀
|
||||
on: [push]
|
||||
|
||||
jobs:
|
||||
Build-All:
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
valid_volumes: ['/opt/keys']
|
||||
volumes:
|
||||
- /opt/keys:/opt/keys
|
||||
steps:
|
||||
- name: check out code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: true
|
||||
- run: ln -s /opt/keys .keys
|
||||
- name: Setup JDK
|
||||
uses: actions/setup-java@v3
|
||||
with:
|
||||
java-version: '17'
|
||||
distribution: 'temurin'
|
||||
- name: Setup Android SDK
|
||||
uses: android-actions/setup-android@v3
|
||||
with:
|
||||
packages: 'tools platform-tools build-tools;34.0.0 platforms;android-34 ndk;26.3.11579264'
|
||||
- run: sudo apt update && sudo apt install -y doxygen graphviz mingw-w64 libgpgme11
|
||||
- run: ANDROID_SDK=$HOME/.android/sdk make -j`nproc` all docs
|
||||
- run: docker build .
|
||||
- uses: actions/upload-artifact@v3
|
||||
with:
|
||||
path: out/TildeFriends-release.fdroid.apk
|
||||
- uses: actions/upload-artifact@v3
|
||||
with:
|
||||
path: out/winrelease/tildefriends.exe
|
||||
- uses: actions/upload-artifact@v3
|
||||
with:
|
||||
path: out/tildefriends-x86_64.AppImage
|
18
.gitignore
vendored
Normal file
18
.gitignore
vendored
Normal file
@ -0,0 +1,18 @@
|
||||
build/
|
||||
*.core
|
||||
db.*
|
||||
deps/ios_toolchain/
|
||||
deps/openssl/
|
||||
dist/
|
||||
.flatpak-builder
|
||||
.keys
|
||||
logs/
|
||||
**/node_modules
|
||||
out
|
||||
repo/
|
||||
result
|
||||
*.swo
|
||||
*.swp
|
||||
tmp/
|
||||
unsigned/
|
||||
.zsign_cache/
|
28
.gitmodules
vendored
Normal file
28
.gitmodules
vendored
Normal file
@ -0,0 +1,28 @@
|
||||
[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
|
||||
[submodule "deps/openssl_src"]
|
||||
path = deps/openssl_src
|
||||
url = https://github.com/openssl/openssl.git
|
||||
shallow = true
|
||||
[submodule "deps/c-ares"]
|
||||
path = deps/c-ares
|
||||
url = https://github.com/c-ares/c-ares.git
|
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`.
|
59
COPYING
59
COPYING
@ -1,59 +0,0 @@
|
||||
Tilde Friends - An operating system for the web.
|
||||
Copyright (C) 2014 Cory McWilliams <cory@unprompted.com>
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as
|
||||
published by the Free Software Foundation, either version 3 of the
|
||||
License, or (at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
Additional permission under GNU GPL version 3 section 7
|
||||
|
||||
If you modify this Program, or any covered work, by linking or combining it
|
||||
with libuv (or a modified version of that library), containing parts covered by
|
||||
the terms of the MIT License, the licensors of this Program grant you
|
||||
additional permission to convey the resulting work. {Corresponding Source for
|
||||
a non-source form of such a combination shall include the source code for the
|
||||
parts of libuv used as well as that of the covered work.}
|
||||
|
||||
If you modify this Program, or any covered work, by linking or combining it
|
||||
with QuickJS (or a modified version of that library), containing parts covered
|
||||
by the terms of the MIT License, the licensors of this Program grant you
|
||||
additional permission to convey the resulting work. {Corresponding Source for
|
||||
a non-source form of such a combination shall include the source code for the
|
||||
parts of QuickJS used as well as that of the covered work.}
|
||||
|
||||
If you modify this Program, or any covered work, by linking or combining it
|
||||
with libsodium (or a modified version of that library), containing parts
|
||||
covered by the terms of the ISC License, the licensors of this Program grant
|
||||
you additional permission to convey the resulting work. {Corresponding Source
|
||||
for a non-source form of such a combination shall include the source code for
|
||||
the parts of libsodium used as well as that of the covered work.}
|
||||
|
||||
If you modify this Program, or any covered work, by linking or combining it
|
||||
with xopt (or a modified version of that library), containing parts covered by
|
||||
the terms of the Apache License 2.0, the licensors of this Program grant you
|
||||
additional permission to convey the resulting work. {Corresponding Source for
|
||||
a non-source form of such a combination shall include the source code for the
|
||||
parts of xopt used as well as that of the covered work.}
|
||||
|
||||
If you modify this Program, or any covered work, by linking or combining it
|
||||
with crypt_blowfish (or a modified version of that library), containing parts
|
||||
covered by the terms of the MIT License, the licensors of this Program grant
|
||||
you additional permission to convey the resulting work. {Corresponding Source
|
||||
for a non-source form of such a combination shall include the source code for
|
||||
the parts of crypt_blowfish used as well as that of the covered work.}
|
||||
|
||||
If you modify this Program, or any covered work, by linking or combining it
|
||||
with base64c (or a modified version of that library), containing parts covered
|
||||
by the terms of the BSD 3-Clause License, the licensors of this Program grant
|
||||
you additional permission to convey the resulting work. {Corresponding Source
|
||||
for a non-source form of such a combination shall include the source code for
|
||||
the parts of base64c used as well as that of the covered work.}
|
23
Dockerfile
Normal file
23
Dockerfile
Normal file
@ -0,0 +1,23 @@
|
||||
FROM bitnami/minideb:bullseye AS build
|
||||
|
||||
RUN apt-get update && \
|
||||
apt-get install -y --no-install-recommends \
|
||||
gcc \
|
||||
libc6-dev \
|
||||
libssl-dev \
|
||||
make
|
||||
|
||||
COPY . /app
|
||||
RUN make -C /app -j $(nproc) release
|
||||
|
||||
FROM bitnami/minideb:bullseye
|
||||
RUN apt-get update && \
|
||||
apt-get install -y --no-install-recommends \
|
||||
libssl1.1
|
||||
|
||||
COPY --from=build /app/out/release/tildefriends /app/out/release/tildefriends
|
||||
COPY --from=build /app/apps /app/apps
|
||||
COPY --from=build /app/core /app/core
|
||||
WORKDIR /app
|
||||
EXPOSE 12345
|
||||
ENTRYPOINT ["/app/out/release/tildefriends"]
|
1182
GNUmakefile
Normal file
1182
GNUmakefile
Normal file
File diff suppressed because it is too large
Load Diff
680
LICENSE
680
LICENSE
@ -1,661 +1,19 @@
|
||||
GNU AFFERO GENERAL PUBLIC LICENSE
|
||||
Version 3, 19 November 2007
|
||||
|
||||
Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
|
||||
Preamble
|
||||
|
||||
The GNU Affero General Public License is a free, copyleft license for
|
||||
software and other kinds of works, specifically designed to ensure
|
||||
cooperation with the community in the case of network server software.
|
||||
|
||||
The licenses for most software and other practical works are designed
|
||||
to take away your freedom to share and change the works. By contrast,
|
||||
our General Public Licenses are intended to guarantee your freedom to
|
||||
share and change all versions of a program--to make sure it remains free
|
||||
software for all its users.
|
||||
|
||||
When we speak of free software, we are referring to freedom, not
|
||||
price. Our General Public Licenses are designed to make sure that you
|
||||
have the freedom to distribute copies of free software (and charge for
|
||||
them if you wish), that you receive source code or can get it if you
|
||||
want it, that you can change the software or use pieces of it in new
|
||||
free programs, and that you know you can do these things.
|
||||
|
||||
Developers that use our General Public Licenses protect your rights
|
||||
with two steps: (1) assert copyright on the software, and (2) offer
|
||||
you this License which gives you legal permission to copy, distribute
|
||||
and/or modify the software.
|
||||
|
||||
A secondary benefit of defending all users' freedom is that
|
||||
improvements made in alternate versions of the program, if they
|
||||
receive widespread use, become available for other developers to
|
||||
incorporate. Many developers of free software are heartened and
|
||||
encouraged by the resulting cooperation. However, in the case of
|
||||
software used on network servers, this result may fail to come about.
|
||||
The GNU General Public License permits making a modified version and
|
||||
letting the public access it on a server without ever releasing its
|
||||
source code to the public.
|
||||
|
||||
The GNU Affero General Public License is designed specifically to
|
||||
ensure that, in such cases, the modified source code becomes available
|
||||
to the community. It requires the operator of a network server to
|
||||
provide the source code of the modified version running there to the
|
||||
users of that server. Therefore, public use of a modified version, on
|
||||
a publicly accessible server, gives the public access to the source
|
||||
code of the modified version.
|
||||
|
||||
An older license, called the Affero General Public License and
|
||||
published by Affero, was designed to accomplish similar goals. This is
|
||||
a different license, not a version of the Affero GPL, but Affero has
|
||||
released a new version of the Affero GPL which permits relicensing under
|
||||
this license.
|
||||
|
||||
The precise terms and conditions for copying, distribution and
|
||||
modification follow.
|
||||
|
||||
TERMS AND CONDITIONS
|
||||
|
||||
0. Definitions.
|
||||
|
||||
"This License" refers to version 3 of the GNU Affero General Public License.
|
||||
|
||||
"Copyright" also means copyright-like laws that apply to other kinds of
|
||||
works, such as semiconductor masks.
|
||||
|
||||
"The Program" refers to any copyrightable work licensed under this
|
||||
License. Each licensee is addressed as "you". "Licensees" and
|
||||
"recipients" may be individuals or organizations.
|
||||
|
||||
To "modify" a work means to copy from or adapt all or part of the work
|
||||
in a fashion requiring copyright permission, other than the making of an
|
||||
exact copy. The resulting work is called a "modified version" of the
|
||||
earlier work or a work "based on" the earlier work.
|
||||
|
||||
A "covered work" means either the unmodified Program or a work based
|
||||
on the Program.
|
||||
|
||||
To "propagate" a work means to do anything with it that, without
|
||||
permission, would make you directly or secondarily liable for
|
||||
infringement under applicable copyright law, except executing it on a
|
||||
computer or modifying a private copy. Propagation includes copying,
|
||||
distribution (with or without modification), making available to the
|
||||
public, and in some countries other activities as well.
|
||||
|
||||
To "convey" a work means any kind of propagation that enables other
|
||||
parties to make or receive copies. Mere interaction with a user through
|
||||
a computer network, with no transfer of a copy, is not conveying.
|
||||
|
||||
An interactive user interface displays "Appropriate Legal Notices"
|
||||
to the extent that it includes a convenient and prominently visible
|
||||
feature that (1) displays an appropriate copyright notice, and (2)
|
||||
tells the user that there is no warranty for the work (except to the
|
||||
extent that warranties are provided), that licensees may convey the
|
||||
work under this License, and how to view a copy of this License. If
|
||||
the interface presents a list of user commands or options, such as a
|
||||
menu, a prominent item in the list meets this criterion.
|
||||
|
||||
1. Source Code.
|
||||
|
||||
The "source code" for a work means the preferred form of the work
|
||||
for making modifications to it. "Object code" means any non-source
|
||||
form of a work.
|
||||
|
||||
A "Standard Interface" means an interface that either is an official
|
||||
standard defined by a recognized standards body, or, in the case of
|
||||
interfaces specified for a particular programming language, one that
|
||||
is widely used among developers working in that language.
|
||||
|
||||
The "System Libraries" of an executable work include anything, other
|
||||
than the work as a whole, that (a) is included in the normal form of
|
||||
packaging a Major Component, but which is not part of that Major
|
||||
Component, and (b) serves only to enable use of the work with that
|
||||
Major Component, or to implement a Standard Interface for which an
|
||||
implementation is available to the public in source code form. A
|
||||
"Major Component", in this context, means a major essential component
|
||||
(kernel, window system, and so on) of the specific operating system
|
||||
(if any) on which the executable work runs, or a compiler used to
|
||||
produce the work, or an object code interpreter used to run it.
|
||||
|
||||
The "Corresponding Source" for a work in object code form means all
|
||||
the source code needed to generate, install, and (for an executable
|
||||
work) run the object code and to modify the work, including scripts to
|
||||
control those activities. However, it does not include the work's
|
||||
System Libraries, or general-purpose tools or generally available free
|
||||
programs which are used unmodified in performing those activities but
|
||||
which are not part of the work. For example, Corresponding Source
|
||||
includes interface definition files associated with source files for
|
||||
the work, and the source code for shared libraries and dynamically
|
||||
linked subprograms that the work is specifically designed to require,
|
||||
such as by intimate data communication or control flow between those
|
||||
subprograms and other parts of the work.
|
||||
|
||||
The Corresponding Source need not include anything that users
|
||||
can regenerate automatically from other parts of the Corresponding
|
||||
Source.
|
||||
|
||||
The Corresponding Source for a work in source code form is that
|
||||
same work.
|
||||
|
||||
2. Basic Permissions.
|
||||
|
||||
All rights granted under this License are granted for the term of
|
||||
copyright on the Program, and are irrevocable provided the stated
|
||||
conditions are met. This License explicitly affirms your unlimited
|
||||
permission to run the unmodified Program. The output from running a
|
||||
covered work is covered by this License only if the output, given its
|
||||
content, constitutes a covered work. This License acknowledges your
|
||||
rights of fair use or other equivalent, as provided by copyright law.
|
||||
|
||||
You may make, run and propagate covered works that you do not
|
||||
convey, without conditions so long as your license otherwise remains
|
||||
in force. You may convey covered works to others for the sole purpose
|
||||
of having them make modifications exclusively for you, or provide you
|
||||
with facilities for running those works, provided that you comply with
|
||||
the terms of this License in conveying all material for which you do
|
||||
not control copyright. Those thus making or running the covered works
|
||||
for you must do so exclusively on your behalf, under your direction
|
||||
and control, on terms that prohibit them from making any copies of
|
||||
your copyrighted material outside their relationship with you.
|
||||
|
||||
Conveying under any other circumstances is permitted solely under
|
||||
the conditions stated below. Sublicensing is not allowed; section 10
|
||||
makes it unnecessary.
|
||||
|
||||
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
|
||||
|
||||
No covered work shall be deemed part of an effective technological
|
||||
measure under any applicable law fulfilling obligations under article
|
||||
11 of the WIPO copyright treaty adopted on 20 December 1996, or
|
||||
similar laws prohibiting or restricting circumvention of such
|
||||
measures.
|
||||
|
||||
When you convey a covered work, you waive any legal power to forbid
|
||||
circumvention of technological measures to the extent such circumvention
|
||||
is effected by exercising rights under this License with respect to
|
||||
the covered work, and you disclaim any intention to limit operation or
|
||||
modification of the work as a means of enforcing, against the work's
|
||||
users, your or third parties' legal rights to forbid circumvention of
|
||||
technological measures.
|
||||
|
||||
4. Conveying Verbatim Copies.
|
||||
|
||||
You may convey verbatim copies of the Program's source code as you
|
||||
receive it, in any medium, provided that you conspicuously and
|
||||
appropriately publish on each copy an appropriate copyright notice;
|
||||
keep intact all notices stating that this License and any
|
||||
non-permissive terms added in accord with section 7 apply to the code;
|
||||
keep intact all notices of the absence of any warranty; and give all
|
||||
recipients a copy of this License along with the Program.
|
||||
|
||||
You may charge any price or no price for each copy that you convey,
|
||||
and you may offer support or warranty protection for a fee.
|
||||
|
||||
5. Conveying Modified Source Versions.
|
||||
|
||||
You may convey a work based on the Program, or the modifications to
|
||||
produce it from the Program, in the form of source code under the
|
||||
terms of section 4, provided that you also meet all of these conditions:
|
||||
|
||||
a) The work must carry prominent notices stating that you modified
|
||||
it, and giving a relevant date.
|
||||
|
||||
b) The work must carry prominent notices stating that it is
|
||||
released under this License and any conditions added under section
|
||||
7. This requirement modifies the requirement in section 4 to
|
||||
"keep intact all notices".
|
||||
|
||||
c) You must license the entire work, as a whole, under this
|
||||
License to anyone who comes into possession of a copy. This
|
||||
License will therefore apply, along with any applicable section 7
|
||||
additional terms, to the whole of the work, and all its parts,
|
||||
regardless of how they are packaged. This License gives no
|
||||
permission to license the work in any other way, but it does not
|
||||
invalidate such permission if you have separately received it.
|
||||
|
||||
d) If the work has interactive user interfaces, each must display
|
||||
Appropriate Legal Notices; however, if the Program has interactive
|
||||
interfaces that do not display Appropriate Legal Notices, your
|
||||
work need not make them do so.
|
||||
|
||||
A compilation of a covered work with other separate and independent
|
||||
works, which are not by their nature extensions of the covered work,
|
||||
and which are not combined with it such as to form a larger program,
|
||||
in or on a volume of a storage or distribution medium, is called an
|
||||
"aggregate" if the compilation and its resulting copyright are not
|
||||
used to limit the access or legal rights of the compilation's users
|
||||
beyond what the individual works permit. Inclusion of a covered work
|
||||
in an aggregate does not cause this License to apply to the other
|
||||
parts of the aggregate.
|
||||
|
||||
6. Conveying Non-Source Forms.
|
||||
|
||||
You may convey a covered work in object code form under the terms
|
||||
of sections 4 and 5, provided that you also convey the
|
||||
machine-readable Corresponding Source under the terms of this License,
|
||||
in one of these ways:
|
||||
|
||||
a) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by the
|
||||
Corresponding Source fixed on a durable physical medium
|
||||
customarily used for software interchange.
|
||||
|
||||
b) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by a
|
||||
written offer, valid for at least three years and valid for as
|
||||
long as you offer spare parts or customer support for that product
|
||||
model, to give anyone who possesses the object code either (1) a
|
||||
copy of the Corresponding Source for all the software in the
|
||||
product that is covered by this License, on a durable physical
|
||||
medium customarily used for software interchange, for a price no
|
||||
more than your reasonable cost of physically performing this
|
||||
conveying of source, or (2) access to copy the
|
||||
Corresponding Source from a network server at no charge.
|
||||
|
||||
c) Convey individual copies of the object code with a copy of the
|
||||
written offer to provide the Corresponding Source. This
|
||||
alternative is allowed only occasionally and noncommercially, and
|
||||
only if you received the object code with such an offer, in accord
|
||||
with subsection 6b.
|
||||
|
||||
d) Convey the object code by offering access from a designated
|
||||
place (gratis or for a charge), and offer equivalent access to the
|
||||
Corresponding Source in the same way through the same place at no
|
||||
further charge. You need not require recipients to copy the
|
||||
Corresponding Source along with the object code. If the place to
|
||||
copy the object code is a network server, the Corresponding Source
|
||||
may be on a different server (operated by you or a third party)
|
||||
that supports equivalent copying facilities, provided you maintain
|
||||
clear directions next to the object code saying where to find the
|
||||
Corresponding Source. Regardless of what server hosts the
|
||||
Corresponding Source, you remain obligated to ensure that it is
|
||||
available for as long as needed to satisfy these requirements.
|
||||
|
||||
e) Convey the object code using peer-to-peer transmission, provided
|
||||
you inform other peers where the object code and Corresponding
|
||||
Source of the work are being offered to the general public at no
|
||||
charge under subsection 6d.
|
||||
|
||||
A separable portion of the object code, whose source code is excluded
|
||||
from the Corresponding Source as a System Library, need not be
|
||||
included in conveying the object code work.
|
||||
|
||||
A "User Product" is either (1) a "consumer product", which means any
|
||||
tangible personal property which is normally used for personal, family,
|
||||
or household purposes, or (2) anything designed or sold for incorporation
|
||||
into a dwelling. In determining whether a product is a consumer product,
|
||||
doubtful cases shall be resolved in favor of coverage. For a particular
|
||||
product received by a particular user, "normally used" refers to a
|
||||
typical or common use of that class of product, regardless of the status
|
||||
of the particular user or of the way in which the particular user
|
||||
actually uses, or expects or is expected to use, the product. A product
|
||||
is a consumer product regardless of whether the product has substantial
|
||||
commercial, industrial or non-consumer uses, unless such uses represent
|
||||
the only significant mode of use of the product.
|
||||
|
||||
"Installation Information" for a User Product means any methods,
|
||||
procedures, authorization keys, or other information required to install
|
||||
and execute modified versions of a covered work in that User Product from
|
||||
a modified version of its Corresponding Source. The information must
|
||||
suffice to ensure that the continued functioning of the modified object
|
||||
code is in no case prevented or interfered with solely because
|
||||
modification has been made.
|
||||
|
||||
If you convey an object code work under this section in, or with, or
|
||||
specifically for use in, a User Product, and the conveying occurs as
|
||||
part of a transaction in which the right of possession and use of the
|
||||
User Product is transferred to the recipient in perpetuity or for a
|
||||
fixed term (regardless of how the transaction is characterized), the
|
||||
Corresponding Source conveyed under this section must be accompanied
|
||||
by the Installation Information. But this requirement does not apply
|
||||
if neither you nor any third party retains the ability to install
|
||||
modified object code on the User Product (for example, the work has
|
||||
been installed in ROM).
|
||||
|
||||
The requirement to provide Installation Information does not include a
|
||||
requirement to continue to provide support service, warranty, or updates
|
||||
for a work that has been modified or installed by the recipient, or for
|
||||
the User Product in which it has been modified or installed. Access to a
|
||||
network may be denied when the modification itself materially and
|
||||
adversely affects the operation of the network or violates the rules and
|
||||
protocols for communication across the network.
|
||||
|
||||
Corresponding Source conveyed, and Installation Information provided,
|
||||
in accord with this section must be in a format that is publicly
|
||||
documented (and with an implementation available to the public in
|
||||
source code form), and must require no special password or key for
|
||||
unpacking, reading or copying.
|
||||
|
||||
7. Additional Terms.
|
||||
|
||||
"Additional permissions" are terms that supplement the terms of this
|
||||
License by making exceptions from one or more of its conditions.
|
||||
Additional permissions that are applicable to the entire Program shall
|
||||
be treated as though they were included in this License, to the extent
|
||||
that they are valid under applicable law. If additional permissions
|
||||
apply only to part of the Program, that part may be used separately
|
||||
under those permissions, but the entire Program remains governed by
|
||||
this License without regard to the additional permissions.
|
||||
|
||||
When you convey a copy of a covered work, you may at your option
|
||||
remove any additional permissions from that copy, or from any part of
|
||||
it. (Additional permissions may be written to require their own
|
||||
removal in certain cases when you modify the work.) You may place
|
||||
additional permissions on material, added by you to a covered work,
|
||||
for which you have or can give appropriate copyright permission.
|
||||
|
||||
Notwithstanding any other provision of this License, for material you
|
||||
add to a covered work, you may (if authorized by the copyright holders of
|
||||
that material) supplement the terms of this License with terms:
|
||||
|
||||
a) Disclaiming warranty or limiting liability differently from the
|
||||
terms of sections 15 and 16 of this License; or
|
||||
|
||||
b) Requiring preservation of specified reasonable legal notices or
|
||||
author attributions in that material or in the Appropriate Legal
|
||||
Notices displayed by works containing it; or
|
||||
|
||||
c) Prohibiting misrepresentation of the origin of that material, or
|
||||
requiring that modified versions of such material be marked in
|
||||
reasonable ways as different from the original version; or
|
||||
|
||||
d) Limiting the use for publicity purposes of names of licensors or
|
||||
authors of the material; or
|
||||
|
||||
e) Declining to grant rights under trademark law for use of some
|
||||
trade names, trademarks, or service marks; or
|
||||
|
||||
f) Requiring indemnification of licensors and authors of that
|
||||
material by anyone who conveys the material (or modified versions of
|
||||
it) with contractual assumptions of liability to the recipient, for
|
||||
any liability that these contractual assumptions directly impose on
|
||||
those licensors and authors.
|
||||
|
||||
All other non-permissive additional terms are considered "further
|
||||
restrictions" within the meaning of section 10. If the Program as you
|
||||
received it, or any part of it, contains a notice stating that it is
|
||||
governed by this License along with a term that is a further
|
||||
restriction, you may remove that term. If a license document contains
|
||||
a further restriction but permits relicensing or conveying under this
|
||||
License, you may add to a covered work material governed by the terms
|
||||
of that license document, provided that the further restriction does
|
||||
not survive such relicensing or conveying.
|
||||
|
||||
If you add terms to a covered work in accord with this section, you
|
||||
must place, in the relevant source files, a statement of the
|
||||
additional terms that apply to those files, or a notice indicating
|
||||
where to find the applicable terms.
|
||||
|
||||
Additional terms, permissive or non-permissive, may be stated in the
|
||||
form of a separately written license, or stated as exceptions;
|
||||
the above requirements apply either way.
|
||||
|
||||
8. Termination.
|
||||
|
||||
You may not propagate or modify a covered work except as expressly
|
||||
provided under this License. Any attempt otherwise to propagate or
|
||||
modify it is void, and will automatically terminate your rights under
|
||||
this License (including any patent licenses granted under the third
|
||||
paragraph of section 11).
|
||||
|
||||
However, if you cease all violation of this License, then your
|
||||
license from a particular copyright holder is reinstated (a)
|
||||
provisionally, unless and until the copyright holder explicitly and
|
||||
finally terminates your license, and (b) permanently, if the copyright
|
||||
holder fails to notify you of the violation by some reasonable means
|
||||
prior to 60 days after the cessation.
|
||||
|
||||
Moreover, your license from a particular copyright holder is
|
||||
reinstated permanently if the copyright holder notifies you of the
|
||||
violation by some reasonable means, this is the first time you have
|
||||
received notice of violation of this License (for any work) from that
|
||||
copyright holder, and you cure the violation prior to 30 days after
|
||||
your receipt of the notice.
|
||||
|
||||
Termination of your rights under this section does not terminate the
|
||||
licenses of parties who have received copies or rights from you under
|
||||
this License. If your rights have been terminated and not permanently
|
||||
reinstated, you do not qualify to receive new licenses for the same
|
||||
material under section 10.
|
||||
|
||||
9. Acceptance Not Required for Having Copies.
|
||||
|
||||
You are not required to accept this License in order to receive or
|
||||
run a copy of the Program. Ancillary propagation of a covered work
|
||||
occurring solely as a consequence of using peer-to-peer transmission
|
||||
to receive a copy likewise does not require acceptance. However,
|
||||
nothing other than this License grants you permission to propagate or
|
||||
modify any covered work. These actions infringe copyright if you do
|
||||
not accept this License. Therefore, by modifying or propagating a
|
||||
covered work, you indicate your acceptance of this License to do so.
|
||||
|
||||
10. Automatic Licensing of Downstream Recipients.
|
||||
|
||||
Each time you convey a covered work, the recipient automatically
|
||||
receives a license from the original licensors, to run, modify and
|
||||
propagate that work, subject to this License. You are not responsible
|
||||
for enforcing compliance by third parties with this License.
|
||||
|
||||
An "entity transaction" is a transaction transferring control of an
|
||||
organization, or substantially all assets of one, or subdividing an
|
||||
organization, or merging organizations. If propagation of a covered
|
||||
work results from an entity transaction, each party to that
|
||||
transaction who receives a copy of the work also receives whatever
|
||||
licenses to the work the party's predecessor in interest had or could
|
||||
give under the previous paragraph, plus a right to possession of the
|
||||
Corresponding Source of the work from the predecessor in interest, if
|
||||
the predecessor has it or can get it with reasonable efforts.
|
||||
|
||||
You may not impose any further restrictions on the exercise of the
|
||||
rights granted or affirmed under this License. For example, you may
|
||||
not impose a license fee, royalty, or other charge for exercise of
|
||||
rights granted under this License, and you may not initiate litigation
|
||||
(including a cross-claim or counterclaim in a lawsuit) alleging that
|
||||
any patent claim is infringed by making, using, selling, offering for
|
||||
sale, or importing the Program or any portion of it.
|
||||
|
||||
11. Patents.
|
||||
|
||||
A "contributor" is a copyright holder who authorizes use under this
|
||||
License of the Program or a work on which the Program is based. The
|
||||
work thus licensed is called the contributor's "contributor version".
|
||||
|
||||
A contributor's "essential patent claims" are all patent claims
|
||||
owned or controlled by the contributor, whether already acquired or
|
||||
hereafter acquired, that would be infringed by some manner, permitted
|
||||
by this License, of making, using, or selling its contributor version,
|
||||
but do not include claims that would be infringed only as a
|
||||
consequence of further modification of the contributor version. For
|
||||
purposes of this definition, "control" includes the right to grant
|
||||
patent sublicenses in a manner consistent with the requirements of
|
||||
this License.
|
||||
|
||||
Each contributor grants you a non-exclusive, worldwide, royalty-free
|
||||
patent license under the contributor's essential patent claims, to
|
||||
make, use, sell, offer for sale, import and otherwise run, modify and
|
||||
propagate the contents of its contributor version.
|
||||
|
||||
In the following three paragraphs, a "patent license" is any express
|
||||
agreement or commitment, however denominated, not to enforce a patent
|
||||
(such as an express permission to practice a patent or covenant not to
|
||||
sue for patent infringement). To "grant" such a patent license to a
|
||||
party means to make such an agreement or commitment not to enforce a
|
||||
patent against the party.
|
||||
|
||||
If you convey a covered work, knowingly relying on a patent license,
|
||||
and the Corresponding Source of the work is not available for anyone
|
||||
to copy, free of charge and under the terms of this License, through a
|
||||
publicly available network server or other readily accessible means,
|
||||
then you must either (1) cause the Corresponding Source to be so
|
||||
available, or (2) arrange to deprive yourself of the benefit of the
|
||||
patent license for this particular work, or (3) arrange, in a manner
|
||||
consistent with the requirements of this License, to extend the patent
|
||||
license to downstream recipients. "Knowingly relying" means you have
|
||||
actual knowledge that, but for the patent license, your conveying the
|
||||
covered work in a country, or your recipient's use of the covered work
|
||||
in a country, would infringe one or more identifiable patents in that
|
||||
country that you have reason to believe are valid.
|
||||
|
||||
If, pursuant to or in connection with a single transaction or
|
||||
arrangement, you convey, or propagate by procuring conveyance of, a
|
||||
covered work, and grant a patent license to some of the parties
|
||||
receiving the covered work authorizing them to use, propagate, modify
|
||||
or convey a specific copy of the covered work, then the patent license
|
||||
you grant is automatically extended to all recipients of the covered
|
||||
work and works based on it.
|
||||
|
||||
A patent license is "discriminatory" if it does not include within
|
||||
the scope of its coverage, prohibits the exercise of, or is
|
||||
conditioned on the non-exercise of one or more of the rights that are
|
||||
specifically granted under this License. You may not convey a covered
|
||||
work if you are a party to an arrangement with a third party that is
|
||||
in the business of distributing software, under which you make payment
|
||||
to the third party based on the extent of your activity of conveying
|
||||
the work, and under which the third party grants, to any of the
|
||||
parties who would receive the covered work from you, a discriminatory
|
||||
patent license (a) in connection with copies of the covered work
|
||||
conveyed by you (or copies made from those copies), or (b) primarily
|
||||
for and in connection with specific products or compilations that
|
||||
contain the covered work, unless you entered into that arrangement,
|
||||
or that patent license was granted, prior to 28 March 2007.
|
||||
|
||||
Nothing in this License shall be construed as excluding or limiting
|
||||
any implied license or other defenses to infringement that may
|
||||
otherwise be available to you under applicable patent law.
|
||||
|
||||
12. No Surrender of Others' Freedom.
|
||||
|
||||
If conditions are imposed on you (whether by court order, agreement or
|
||||
otherwise) that contradict the conditions of this License, they do not
|
||||
excuse you from the conditions of this License. If you cannot convey a
|
||||
covered work so as to satisfy simultaneously your obligations under this
|
||||
License and any other pertinent obligations, then as a consequence you may
|
||||
not convey it at all. For example, if you agree to terms that obligate you
|
||||
to collect a royalty for further conveying from those to whom you convey
|
||||
the Program, the only way you could satisfy both those terms and this
|
||||
License would be to refrain entirely from conveying the Program.
|
||||
|
||||
13. Remote Network Interaction; Use with the GNU General Public License.
|
||||
|
||||
Notwithstanding any other provision of this License, if you modify the
|
||||
Program, your modified version must prominently offer all users
|
||||
interacting with it remotely through a computer network (if your version
|
||||
supports such interaction) an opportunity to receive the Corresponding
|
||||
Source of your version by providing access to the Corresponding Source
|
||||
from a network server at no charge, through some standard or customary
|
||||
means of facilitating copying of software. This Corresponding Source
|
||||
shall include the Corresponding Source for any work covered by version 3
|
||||
of the GNU General Public License that is incorporated pursuant to the
|
||||
following paragraph.
|
||||
|
||||
Notwithstanding any other provision of this License, you have
|
||||
permission to link or combine any covered work with a work licensed
|
||||
under version 3 of the GNU General Public License into a single
|
||||
combined work, and to convey the resulting work. The terms of this
|
||||
License will continue to apply to the part which is the covered work,
|
||||
but the work with which it is combined will remain governed by version
|
||||
3 of the GNU General Public License.
|
||||
|
||||
14. Revised Versions of this License.
|
||||
|
||||
The Free Software Foundation may publish revised and/or new versions of
|
||||
the GNU Affero General Public License from time to time. Such new versions
|
||||
will be similar in spirit to the present version, but may differ in detail to
|
||||
address new problems or concerns.
|
||||
|
||||
Each version is given a distinguishing version number. If the
|
||||
Program specifies that a certain numbered version of the GNU Affero General
|
||||
Public License "or any later version" applies to it, you have the
|
||||
option of following the terms and conditions either of that numbered
|
||||
version or of any later version published by the Free Software
|
||||
Foundation. If the Program does not specify a version number of the
|
||||
GNU Affero General Public License, you may choose any version ever published
|
||||
by the Free Software Foundation.
|
||||
|
||||
If the Program specifies that a proxy can decide which future
|
||||
versions of the GNU Affero General Public License can be used, that proxy's
|
||||
public statement of acceptance of a version permanently authorizes you
|
||||
to choose that version for the Program.
|
||||
|
||||
Later license versions may give you additional or different
|
||||
permissions. However, no additional obligations are imposed on any
|
||||
author or copyright holder as a result of your choosing to follow a
|
||||
later version.
|
||||
|
||||
15. Disclaimer of Warranty.
|
||||
|
||||
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
|
||||
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
|
||||
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
|
||||
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
|
||||
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
|
||||
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
|
||||
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
||||
|
||||
16. Limitation of Liability.
|
||||
|
||||
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
|
||||
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
|
||||
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
|
||||
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
|
||||
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
|
||||
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
|
||||
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
|
||||
SUCH DAMAGES.
|
||||
|
||||
17. Interpretation of Sections 15 and 16.
|
||||
|
||||
If the disclaimer of warranty and limitation of liability provided
|
||||
above cannot be given local legal effect according to their terms,
|
||||
reviewing courts shall apply local law that most closely approximates
|
||||
an absolute waiver of all civil liability in connection with the
|
||||
Program, unless a warranty or assumption of liability accompanies a
|
||||
copy of the Program in return for a fee.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
How to Apply These Terms to Your New Programs
|
||||
|
||||
If you develop a new program, and you want it to be of the greatest
|
||||
possible use to the public, the best way to achieve this is to make it
|
||||
free software which everyone can redistribute and change under these terms.
|
||||
|
||||
To do so, attach the following notices to the program. It is safest
|
||||
to attach them to the start of each source file to most effectively
|
||||
state the exclusion of warranty; and each file should have at least
|
||||
the "copyright" line and a pointer to where the full notice is found.
|
||||
|
||||
<one line to give the program's name and a brief idea of what it does.>
|
||||
Copyright (C) <year> <name of author>
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
Also add information on how to contact you by electronic and paper mail.
|
||||
|
||||
If your software can interact with users remotely through a computer
|
||||
network, you should also make sure that it provides a way for users to
|
||||
get its source. For example, if your program is a web application, its
|
||||
interface could display a "Source" link that leads users to an archive
|
||||
of the code. There are many ways you could offer source, and different
|
||||
solutions will be better for different programs; see section 13 for the
|
||||
specific requirements.
|
||||
|
||||
You should also get your employer (if you work as a programmer) or school,
|
||||
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
||||
For more information on this, and how to apply and follow the GNU AGPL, see
|
||||
<http://www.gnu.org/licenses/>.
|
||||
Copyright 2014 Cory McWilliams
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
this software and associated documentation files (the "Software"), to deal in
|
||||
the Software without restriction, including without limitation the rights to
|
||||
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
||||
of the Software, and to permit persons to whom the Software is furnished to do
|
||||
so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
170
Makefile
170
Makefile
@ -1,170 +0,0 @@
|
||||
PROJECT = tildefriends
|
||||
BUILD_DIR ?= out
|
||||
|
||||
COMMON_CFLAGS = \
|
||||
-Wall \
|
||||
-Werror \
|
||||
-Wextra \
|
||||
-Wno-unused-parameter \
|
||||
-Wno-cast-function-type \
|
||||
-MMD \
|
||||
-ffunction-sections \
|
||||
-fdata-sections
|
||||
COMMON_LDFLAGS += -Wl,-gc-sections
|
||||
|
||||
ifneq ($(UNUSED),)
|
||||
COMMON_LDFLAGS += -Wl,-print-gc-sections
|
||||
endif
|
||||
|
||||
ifneq ($(DEBUG),)
|
||||
COMMON_CFLAGS += -g -fsanitize=address -fsanitize=undefined
|
||||
COMMON_LDFLAGS += -fsanitize=address -fsanitize=undefined
|
||||
BUILD_DIR := $(BUILD_DIR)/debug
|
||||
else
|
||||
COMMON_CFLAGS += -DNDEBUG -O3
|
||||
BUILD_DIR := $(BUILD_DIR)/release
|
||||
endif
|
||||
|
||||
APP_BIN = $(BUILD_DIR)/$(PROJECT)
|
||||
APP_SOURCES = $(wildcard src/*.c)
|
||||
APP_OBJS = $(patsubst %.c,$(BUILD_DIR)/%.o,$(APP_SOURCES))
|
||||
$(APP_OBJS): CFLAGS += \
|
||||
-Ideps/base64c/include \
|
||||
-Ideps/crypt_blowfish \
|
||||
-Ideps/quickjs \
|
||||
-Ideps/sqlite \
|
||||
-Ideps/libuv/include \
|
||||
-Ideps/xopt
|
||||
|
||||
BASE64C_SOURCES = deps/base64c/src/base64c.c
|
||||
BASE64C_OBJS = $(patsubst %.c,$(BUILD_DIR)/%.o,$(BASE64C_SOURCES))
|
||||
$(BASE64C_OBJS): CFLAGS += \
|
||||
-Wno-sign-compare
|
||||
|
||||
BLOWFISH_SOURCES = \
|
||||
deps/crypt_blowfish/crypt_blowfish.c \
|
||||
deps/crypt_blowfish/crypt_gensalt.c \
|
||||
deps/crypt_blowfish/wrapper.c
|
||||
BLOWFISH_OBJS = $(patsubst %.c,$(BUILD_DIR)/%.o,$(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/threadpool.c \
|
||||
deps/libuv/src/timer.c \
|
||||
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/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 \
|
||||
deps/libuv/src/uv-common.c \
|
||||
deps/libuv/src/uv-data-getter-setters.c \
|
||||
deps/libuv/src/version.c
|
||||
UV_OBJS = $(patsubst %.c,$(BUILD_DIR)/%.o,$(UV_SOURCES))
|
||||
$(UV_OBJS): CFLAGS += \
|
||||
-Ideps/libuv/include \
|
||||
-Ideps/libuv/src \
|
||||
-Wno-unused-but-set-variable \
|
||||
-Wno-incompatible-pointer-types \
|
||||
-Wno-sign-compare \
|
||||
-D_GNU_SOURCE \
|
||||
|
||||
SQLITE_SOURCES = deps/sqlite/sqlite3.c
|
||||
SQLITE_OBJS = $(patsubst %.c,$(BUILD_DIR)/%.o,$(SQLITE_SOURCES))
|
||||
$(SQLITE_OBJS): CFLAGS += \
|
||||
-DSQLITE_DBCONFIG_DEFAULT_DEFENSIVE \
|
||||
-DSQLITE_ENABLE_JSON1 \
|
||||
-DSQLITE_MAX_LENGTH=5242880 \
|
||||
-DSQLITE_MAX_SQL_LENGTH=100000 \
|
||||
-DSQLITE_MAX_COLUMN=100 \
|
||||
-DSQLITE_MAX_EXPR_DEPTH=20 \
|
||||
-DSQLITE_MAX_COMPOUND_SELECT=3 \
|
||||
-DSQLITE_MAX_VDBE_OP=25000 \
|
||||
-DSQLITE_MAX_FUNCTION_ARG=8 \
|
||||
-DSQLITE_MAX_ATTACHED=0 \
|
||||
-DSQLITE_MAX_LIKE_PATTERN_LENGTH=50 \
|
||||
-DSQLITE_MAX_VARIABLE_NUMBER=100 \
|
||||
-DSQLITE_MAX_TRIGGER_DEPTH=10 \
|
||||
-DSQLITE_SECURE_DELETE \
|
||||
-Wno-implicit-fallthrough
|
||||
|
||||
XOPT_SOURCES = deps/xopt/xopt.c
|
||||
XOPT_OBJS = $(patsubst %.c,$(BUILD_DIR)/%.o,$(XOPT_SOURCES))
|
||||
|
||||
QUICKJS_SOURCES = \
|
||||
deps/quickjs/cutils.c \
|
||||
deps/quickjs/libbf.c \
|
||||
deps/quickjs/libregexp.c \
|
||||
deps/quickjs/libunicode.c \
|
||||
deps/quickjs/quickjs-libc.c \
|
||||
deps/quickjs/quickjs.c
|
||||
QUICKJS_OBJS = $(patsubst %.c,$(BUILD_DIR)/%.o,$(QUICKJS_SOURCES))
|
||||
$(QUICKJS_OBJS): CFLAGS += \
|
||||
-DCONFIG_VERSION=\"$(shell cat deps/quickjs/VERSION)\" \
|
||||
-DDUMP_LEAKS \
|
||||
-D_GNU_SOURCE \
|
||||
-Wno-sign-compare \
|
||||
-Wno-implicit-fallthrough \
|
||||
-Wno-unused-variable \
|
||||
-Wno-unused-but-set-variable
|
||||
|
||||
APP_LDFLAGS = \
|
||||
$(COMMON_LDFLAGS) \
|
||||
$(LDFLAGS) \
|
||||
-pthread \
|
||||
-ldl \
|
||||
-lm \
|
||||
-lssl \
|
||||
-lcrypto \
|
||||
-lsodium
|
||||
|
||||
DEFAULT_TARGET = $(APP_BIN)
|
||||
all: $(DEFAULT_TARGET)
|
||||
.PHONY: all
|
||||
|
||||
ALL_APP_OBJS = \
|
||||
$(APP_OBJS) \
|
||||
$(BASE64C_OBJS) \
|
||||
$(BLOWFISH_OBJS) \
|
||||
$(UV_OBJS) \
|
||||
$(SQLITE_OBJS) \
|
||||
$(QUICKJS_OBJS) \
|
||||
$(XOPT_OBJS)
|
||||
|
||||
DEPS = $(ALL_APP_OBJS:.o=.d)
|
||||
-include $(DEPS)
|
||||
|
||||
$(APP_BIN): $(ALL_APP_OBJS)
|
||||
$(CC) -o $@ $^ $(APP_LDFLAGS)
|
||||
|
||||
$(BUILD_DIR)/%.o: %.c
|
||||
@mkdir -p $(dir $@)
|
||||
@echo [c] $@
|
||||
@$(CC) $(COMMON_CFLAGS) $(CFLAGS) -c $< -o $@
|
||||
|
||||
clean:
|
||||
rm -rf $(BUILD_DIR)
|
||||
.PHONY: clean
|
63
README.md
63
README.md
@ -1,26 +1,65 @@
|
||||
# Tilde Friends
|
||||
Tilde Friends is a program that aims to securely host and share pure JavaScript web applications.
|
||||
|
||||
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
|
||||
Scuttlebutt, as well as a platform for writing and running web applications.
|
||||
|
||||
## Goals
|
||||
|
||||
1. Make it easy and fun to run all sorts of web applications.
|
||||
2. Provide a security model that is easy to understand and protects your data.
|
||||
3. Make creating and sharing web applications accessible to anyone with a browser.
|
||||
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.
|
||||
|
||||
## Building
|
||||
1. Requires libsodium and openssl. Other dependencies are kept up to date in the tree.
|
||||
2. To build, run `make` or `make DEBUG=1`. An executable will be generated in a subdirectory of `out/`.
|
||||
|
||||
Builds on Linux (x86_64 and aarch64), MacOS, OpenBSD, and Haiku. Builds for
|
||||
all of those host platforms plus mingw64, iOS, and android.
|
||||
|
||||
Tilde Friends uses git submodules, so either:
|
||||
|
||||
```
|
||||
git clone --recurse-submodules https://dev.tildefriends.net/cory/tildefriends.git
|
||||
```
|
||||
|
||||
or:
|
||||
|
||||
```
|
||||
git clone https://dev.tildefriends.net/cory/tildefriends.git
|
||||
cd tildefriends
|
||||
git submodule update --init --recursive
|
||||
```
|
||||
|
||||
The `.tar.xz` source releases are all-inclusive.
|
||||
|
||||
1. On Linux only, system OpenSSL libraries (`libssl-dev`, in debian-speak) are
|
||||
assumed to be available.
|
||||
2. To build, run `make debug` or `make release`. An executable will be
|
||||
generated in a subdirectory of `out/`.
|
||||
3. It's possible to build for Android, iOS, and Windows on Linux, if you have
|
||||
the right dependencies in the right places. `make windebug winrelease
|
||||
iosdebug-ipa iosrelease-ipa release-apk`.
|
||||
4. To build in docker, `docker build .`.
|
||||
5. `make format` will normalize formatting to the coding standard.
|
||||
|
||||
## Running
|
||||
This is only just starting to show some signs of beginning to work as intended. Set expectations low.
|
||||
|
||||
Running the built `tildefriends` executable will start a web server at <http://localhost:12345/>. `tildefriends -h` lists further options.
|
||||
By default, running the built `tildefriends` executable will start a web server
|
||||
at <http://localhost:12345/>. `tildefriends -h` lists further options.
|
||||
|
||||
The first user to create an account and log in will be granted administrative privileges. Everything can be managed entirely from the web interface.
|
||||
|
||||
Some starter apps can be installed by running `tildefriends import -u cory`. Hint: `~cory/docs/` and `~cory/index/`.
|
||||
The first user to create an account and log in will be granted administrative
|
||||
privileges. Further administration can be done at
|
||||
<http://localhost:12345/~core/admin/>.
|
||||
|
||||
## Documentation
|
||||
There are the very beginnings of developer documentation in `apps/cory/docs/` that can be read in-place or in-browser by running `tildefriends import -u cory` and then visiting <http://localhost:12345/~cory/docs/>.
|
||||
|
||||
Docs are a work in progress:
|
||||
<https://www.tildefriends.net/~cory/wiki/#test-wiki/tf-app-quick-reference>.
|
||||
|
||||
## License
|
||||
All code unless otherwise noted in [COPYING](https://www.unprompted.com/projects/browser/projects/tildefriends/trunk/COPYING) is provided under the [Affero GPL 3.0](https://www.unprompted.com/projects/browser/projects/tildefriends/trunk/LICENSE) license.
|
||||
|
||||
All code unless otherwise noted in is provided under the
|
||||
[MIT](https://opensource.org/licenses/MIT) license.
|
||||
|
5
apps/admin.json
Normal file
5
apps/admin.json
Normal file
@ -0,0 +1,5 @@
|
||||
{
|
||||
"type": "tildefriends-app",
|
||||
"emoji": "🎛",
|
||||
"previous": "&R49FywYF8CXPhoSEydLbSCgvCddeyTiBwGuDU/gqY+M=.sha256"
|
||||
}
|
30
apps/admin/app.js
Normal file
30
apps/admin/app.js
Normal file
@ -0,0 +1,30 @@
|
||||
import * as tfrpc from '/tfrpc.js';
|
||||
|
||||
tfrpc.register(function delete_user(user) {
|
||||
return core.deleteUser(user);
|
||||
});
|
||||
|
||||
tfrpc.register(function global_settings_set(key, value) {
|
||||
return core.globalSettingsSet(key, value);
|
||||
});
|
||||
|
||||
async function main() {
|
||||
try {
|
||||
let data = {
|
||||
users: {},
|
||||
granted: await core.allPermissionsGranted(),
|
||||
settings: await core.globalSettingsDescriptions(),
|
||||
};
|
||||
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>'
|
||||
);
|
||||
}
|
||||
}
|
||||
main();
|
41
apps/admin/index.html
Normal file
41
apps/admin/index.html
Normal file
@ -0,0 +1,41 @@
|
||||
<!doctype html>
|
||||
<html style="width: 100%">
|
||||
<head>
|
||||
<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>
|
||||
<body class="w3-theme-l4">
|
||||
<header class="w3-row w3-padding w3-header w3-theme-l1">
|
||||
<h1>Tilde Friends Administration</h1>
|
||||
</header>
|
||||
</body>
|
||||
<script type="module" src="script.js"></script>
|
||||
</html>
|
13
apps/admin/lit.min.js
vendored
Normal file
13
apps/admin/lit.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
115
apps/admin/script.js
Normal file
115
apps/admin/script.js
Normal file
@ -0,0 +1,115 @@
|
||||
import {html, render} from './lit.min.js';
|
||||
import * as tfrpc from '/static/tfrpc.js';
|
||||
|
||||
function delete_user(user) {
|
||||
if (confirm(`Are you sure you want to delete the user "${user}"?`)) {
|
||||
tfrpc.rpc
|
||||
.delete_user(user)
|
||||
.then(function () {
|
||||
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) {
|
||||
tfrpc.rpc
|
||||
.global_settings_set(key, value)
|
||||
.then(function () {
|
||||
alert(`Set "${key}" to "${value}".`);
|
||||
})
|
||||
.catch(function (error) {
|
||||
alert(`Failed to set "${key}": ${JSON.stringify(error, null, 2)}.`);
|
||||
});
|
||||
}
|
||||
|
||||
function title_case(name) {
|
||||
return name
|
||||
.split('_')
|
||||
.map((x) => x.charAt(0).toUpperCase() + x.substring(1))
|
||||
.join(' ');
|
||||
}
|
||||
|
||||
window.addEventListener('load', function () {
|
||||
const permission_template = (permission) => html` <code>${permission}</code>`;
|
||||
function input_template(key, description) {
|
||||
if (description.type === 'boolean') {
|
||||
return html`
|
||||
<li class="w3-row">
|
||||
<label class="w3-quarter" for=${'gs_' + key} style="font-weight: bold">${title_case(key)}</label>
|
||||
<div class="w3-quarter w3-padding">${description.description}</div>
|
||||
<div class="w3-quarter w3-padding w3-center"><input class="w3-check" type="checkbox" ?checked=${description.value} id=${'gs_' + key}></input></div>
|
||||
<button class="w3-quarter w3-button w3-theme-action" @click=${(e) => global_settings_set(key, e.srcElement.previousElementSibling.firstChild.checked)}>Set</button>
|
||||
</li>
|
||||
`;
|
||||
} else if (description.type === 'textarea') {
|
||||
return html`
|
||||
<li class="w3-row">
|
||||
<label class="w3-quarter" for=${'gs_' + key} style="font-weight: bold"
|
||||
>${title_case(key)}</label
|
||||
>
|
||||
<div class="w3-rest w3-padding">${description.description}</div>
|
||||
<textarea
|
||||
class="w3-input"
|
||||
style="vertical-align: top; resize: vertical"
|
||||
id=${'gs_' + key}
|
||||
>
|
||||
${description.value}</textarea
|
||||
>
|
||||
<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 {
|
||||
return html`
|
||||
<li class="w3-row">
|
||||
<label class="w3-quarter" for=${'gs_' + key} style="font-weight: bold">${title_case(key)}</label>
|
||||
<div class="w3-quarter w3-padding">${description.description}</div>
|
||||
<input class="w3-input w3-quarter" type="text" value="${description.value}" id=${'gs_' + key}></input>
|
||||
<button class="w3-button w3-quarter w3-theme-action" @click=${(e) => global_settings_set(key, e.srcElement.previousElementSibling.value)}>Set</button>
|
||||
</li>
|
||||
`;
|
||||
}
|
||||
}
|
||||
const user_template = (user, permissions) => html`
|
||||
<li class="w3-card w3-margin">
|
||||
<button
|
||||
class="w3-button w3-theme-action"
|
||||
@click=${(e) => delete_user(user)}
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
${user}: ${permissions.map((x) => permission_template(x))}
|
||||
</li>
|
||||
`;
|
||||
const users_template = (users) =>
|
||||
html` <header class="w3-container w3-theme-l2"><h2>Users</h2></header>
|
||||
<ul class="w3-ul">
|
||||
${Object.entries(users).map((u) => user_template(u[0], u[1]))}
|
||||
</ul>`;
|
||||
const page_template = (data) =>
|
||||
html`<div style="padding: 0; margin: 0; width: 100%; max-width: 100%">
|
||||
<header class="w3-container w3-theme-l2"><h2>Global Settings</h2></header>
|
||||
<div class="w3-container">
|
||||
<ul class="w3-ul">
|
||||
${Object.keys(data.settings)
|
||||
.sort()
|
||||
.map((x) => html`${input_template(x, data.settings[x])}`)}
|
||||
</ul>
|
||||
</div>
|
||||
${users_template(data.users)}
|
||||
</div> `;
|
||||
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}
|
5
apps/api.json
Normal file
5
apps/api.json
Normal file
@ -0,0 +1,5 @@
|
||||
{
|
||||
"type": "tildefriends-app",
|
||||
"emoji": "📜",
|
||||
"previous": "&miGORZ8BwjHg2YO0t4bms6SI28XWPYqnqOZ8u9zsbZc=.sha256"
|
||||
}
|
77
apps/api/app.js
Normal file
77
apps/api/app.js
Normal file
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.
|
||||
`;
|
5
apps/apps.json
Normal file
5
apps/apps.json
Normal file
@ -0,0 +1,5 @@
|
||||
{
|
||||
"type": "tildefriends-app",
|
||||
"emoji": "💻",
|
||||
"previous": "&icsPplXHgmpkbNWyo/WTvRdT/A8BXxW4lJixOtP4ueQ=.sha256"
|
||||
}
|
182
apps/apps/app.js
Normal file
182
apps/apps/app.js
Normal file
@ -0,0 +1,182 @@
|
||||
/**
|
||||
* 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) {
|
||||
let result = {};
|
||||
|
||||
// For each app
|
||||
for (let [key, value] of Object.entries(apps)) {
|
||||
// Get it's blob and parse it
|
||||
let blob = await ssb.blobGet(value);
|
||||
blob = blob ? utf8Decode(blob) : '{}';
|
||||
|
||||
// Add it to the result object
|
||||
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;
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const apps = await fetch_info(await core.apps());
|
||||
const core_apps = await fetch_info(await core.apps('core'));
|
||||
const shared_apps = await fetch_shared_apps();
|
||||
|
||||
const stylesheet = `
|
||||
body {
|
||||
color: whitesmoke;
|
||||
font-family: sans-serif;
|
||||
margin: 16px;
|
||||
}
|
||||
.container {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, 64px);
|
||||
gap: 1em;
|
||||
justify-content: space-around;
|
||||
background-color: #ffffff10;
|
||||
border: 2px solid #073642;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.app {
|
||||
height: 96px;
|
||||
width: 64px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.app > a {
|
||||
text-decoration: none;
|
||||
max-width: 64px;
|
||||
text-overflow: ellipsis ellipsis;
|
||||
overflow: hidden;
|
||||
color: whitesmoke;
|
||||
}
|
||||
`;
|
||||
|
||||
const body = `
|
||||
<h1 style="text-shadow: #808080 0 0 10px;">Welcome to Tilde Friends.</h1>
|
||||
|
||||
<h2>your apps</h2>
|
||||
<div id="apps" class="container"></div>
|
||||
|
||||
<h2>shared apps</h2>
|
||||
<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);
|
||||
}
|
||||
}
|
||||
|
||||
populate_apps('apps', '${core.user.credentials?.session?.name}', ${JSON.stringify(apps)});
|
||||
populate_apps('core_apps', 'core', ${JSON.stringify(core_apps)});
|
||||
populate_apps('shared_apps', undefined, ${JSON.stringify(shared_apps)});
|
||||
`;
|
||||
|
||||
// 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();
|
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 +0,0 @@
|
||||
{"type":"tildefriends-app","files":{"app.js":"&WCq6ssQedT5denXPXlz2BswPD6hmt++EmWIMIDUMurA=.sha256","index.md":"&Lr7IXs8osbmWz6SDsGTQCiybbxkbWSK2MrUcXMzgqTs=.sha256","todo.md":"&XrOJ3D5YMTN+j+0hJgLLy7Y61B6Z14ebv+60ee+N37I=.sha256","structure.md":"&xRhQ4Mpom1Idskum07osbBQYcYWroH0sELQBkQHrOMg=.sha256","purpose.md":"&c0/YqFhXC0X3DqiEo55NqzI5wq0VTw6cVZTf/gAWS3w=.sha256","guide.md":"&SgnGL0+rjetY2o9A2+lVRbNvHIkqKwMnZr9gXWneIlc=.sha256"}}
|
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,11 +0,0 @@
|
||||
# Tilde Friends Documentation
|
||||
|
||||
Tilde Friends is a participating member of a greater social
|
||||
network, [Secure Scuttlebutt](https://scuttlebutt.nz/),
|
||||
augmenting it with a way to safely and securely write, share,
|
||||
and run code.
|
||||
|
||||
- [Purpose](#purpose)
|
||||
- [Structure](#structure)
|
||||
- [Guide](#guide)
|
||||
- [TODO](#todo)
|
4
apps/cory/docs/markdeep.min.js
vendored
4
apps/cory/docs/markdeep.min.js
vendored
File diff suppressed because one or more lines are too long
@ -1,24 +0,0 @@
|
||||
# Tilde Friends Purpose
|
||||
[Back to index](#index)
|
||||
|
||||
## Beliefs
|
||||
1. The web is the universal virtual machine.
|
||||
- It is here, ready to be used from your desktop, laptop, smart phone,
|
||||
tablet, game console, and smart TV.
|
||||
- It is not ideal, but it is the best we have right now,
|
||||
and all signs point to it continuing to improve, at least
|
||||
in terms of features, security, and device support.
|
||||
2. Distributed is superior to centralized.
|
||||
- Distributed services don't need ads.
|
||||
- Distributed services can't be acquired by evil corporations.
|
||||
- Distributed services respect the user's privacy.
|
||||
- Distributed services respect the user.
|
||||
3. Offline-first is superior to online-only.
|
||||
- The internet goes down sometimes. Applications should continue
|
||||
to work.
|
||||
3. Making and sharing code should be easy.
|
||||
- Cloning your repository, installing dev tools, running a
|
||||
docker image, or fighting with dependencies is *not* easy.
|
||||
- If you see a thing in a web browser, you should be able to click
|
||||
`edit`, make a change, save, and see the result.
|
||||
[Wikipedia](https://www.wikipedia.org/) is easy.
|
@ -1,67 +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 server.
|
||||
|
||||
## 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 calling functions asynchronously. Calling a remote
|
||||
function (ie. a function in another process) returns a `Promise`. In
|
||||
addition, any functions passed in either direction are serialized in
|
||||
such a way that they can be called remotely.
|
||||
|
||||
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,48 +0,0 @@
|
||||
# Tilde Friends TODO
|
||||
[Back to index](#index)
|
||||
|
||||
## MVP
|
||||
- release
|
||||
- blog
|
||||
- update COPYING
|
||||
- update README
|
||||
- auto-populate data on initial launch
|
||||
- audit + document API exposed to apps
|
||||
- ssb core
|
||||
- good refresh
|
||||
- disconnect all current connections and reset reconnect timers?
|
||||
- reload the page
|
||||
- live updates
|
||||
- createHistoryStream for every account followed from local accounts
|
||||
- apps
|
||||
- app messages
|
||||
- installable apps
|
||||
- web interface
|
||||
- live updates
|
||||
- strip out unnecessary things?
|
||||
- more raw views until it's more functional?
|
||||
|
||||
## Done
|
||||
- likely classes of script errors
|
||||
- tf core
|
||||
- good error feedback
|
||||
- markdeep demo
|
||||
- send blobs
|
||||
|
||||
## Later
|
||||
- DB migration
|
||||
- stop using CDNs
|
||||
- collect loads of stats
|
||||
- faster save - parallel / don't save unmodified
|
||||
- test likely denials of service
|
||||
- package standalone executable
|
||||
- ideas
|
||||
- visualizations / analysis of gps data
|
||||
- good web interface for managing connections
|
||||
- identity
|
||||
- multiple identities
|
||||
- tie identities to TF login accounts
|
||||
- tf account timeout why
|
||||
- make some demo apps
|
||||
- rock paper scissors, somehow
|
||||
- don't resave files that didn't change
|
@ -1 +0,0 @@
|
||||
{"type":"tildefriends-app","files":{"app.js":"&6uFJG2C0kZar1Aj+7p2/KzYEBXgmK/uJSt7aIJqenN4=.sha256","index.html":"&TFtniuUIVO7XeWCgwmqPAmuBzpGX6slxJQcPMEr+860=.sha256","vue-material.js":"&K5cdLqXYCENPak/TCINHQhyJhpS4G9DlZHGwoh/LF2g=.sha256"}}
|
@ -1,381 +0,0 @@
|
||||
"use strict";
|
||||
|
||||
const k_posts_max = 20;
|
||||
const k_votes_max = 100;
|
||||
|
||||
async function following(db, id) {
|
||||
var o = await db.get(id + ":following");
|
||||
const k_version = 4;
|
||||
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.sqlStream(
|
||||
"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],
|
||||
async function(row) {
|
||||
if (row.following) {
|
||||
f.users.add(row.contact);
|
||||
} else {
|
||||
f.users.delete(row.contact);
|
||||
}
|
||||
f.sequence = row.sequence;
|
||||
});
|
||||
f.users = Array.from(f.users);
|
||||
var j = JSON.stringify(f);
|
||||
if (o != j) {
|
||||
await db.set(id + ":following", j);
|
||||
}
|
||||
return f.users;
|
||||
}
|
||||
|
||||
async function followingDeep(db, seed_ids, depth) {
|
||||
if (depth <= 0) {
|
||||
return seed_ids;
|
||||
}
|
||||
var f = await Promise.all(seed_ids.map(x => following(db, x)));
|
||||
var ids = [].concat(...f);
|
||||
var x = await followingDeep(db, [...new Set(ids)].sort(), depth - 1);
|
||||
x = [].concat(...x, ...seed_ids);
|
||||
return x;
|
||||
}
|
||||
|
||||
async function followers(db, id) {
|
||||
var o = await db.get(id + ":followers");
|
||||
const k_version = 2;
|
||||
var f = o ? JSON.parse(o) : o;
|
||||
if (!f || f.version != k_version) {
|
||||
f = {users: [], rowid: 0, version: k_version};
|
||||
}
|
||||
f.users = new Set(f.users);
|
||||
await ssb.sqlStream(
|
||||
"SELECT "+
|
||||
" rowid, "+
|
||||
" author AS contact, "+
|
||||
" json_extract(content, '$.following') AS following "+
|
||||
"FROM messages "+
|
||||
"WHERE "+
|
||||
" rowid > $1 AND "+
|
||||
" json_extract(content, '$.type') = 'contact' AND "+
|
||||
" json_extract(content, '$.contact') = $2 "+
|
||||
"UNION SELECT MAX(rowid) as rowid, NULL, NULL FROM messages "+
|
||||
"ORDER BY rowid",
|
||||
[f.rowid, id],
|
||||
async function(row) {
|
||||
if (row.following) {
|
||||
f.users.add(row.contact);
|
||||
} else {
|
||||
f.users.delete(row.contact);
|
||||
}
|
||||
f.rowid = row.rowid;
|
||||
});
|
||||
f.users = Array.from(f.users);
|
||||
var j = JSON.stringify(f);
|
||||
if (o != j) {
|
||||
await db.set(id + ":followers", j);
|
||||
}
|
||||
return f.users;
|
||||
}
|
||||
|
||||
async function sendUser(db, id) {
|
||||
return Promise.all([
|
||||
following(db, id).then(async function(following) {
|
||||
return app.postMessage({following: {id: id, users: following}});
|
||||
}),
|
||||
followers(db, id).then(async function(followers) {
|
||||
return app.postMessage({followers: {id: id, users: followers}});
|
||||
}),
|
||||
]);
|
||||
}
|
||||
|
||||
async function pubsByUser(db, id) {
|
||||
var o = await db.get(id + ":pubs");
|
||||
const k_version = 2;
|
||||
var f = o ? JSON.parse(o) : o;
|
||||
if (!f || f.version != k_version) {
|
||||
f = {pubs: [], sequence: 0, version: k_version};
|
||||
}
|
||||
f.pubs = Object.fromEntries(f.pubs.map(x => [JSON.stringify(x), x]));
|
||||
await ssb.sqlStream(
|
||||
"SELECT "+
|
||||
" sequence, "+
|
||||
" json_extract(content, '$.address.host') AS host, "+
|
||||
" json_extract(content, '$.address.port') AS port, "+
|
||||
" json_extract(content, '$.address.key') AS key "+
|
||||
"FROM messages "+
|
||||
"WHERE "+
|
||||
" sequence > ?1 AND "+
|
||||
" author = ?2 AND "+
|
||||
" json_extract(content, '$.type') = 'pub' "+
|
||||
"UNION SELECT MAX(sequence) as sequence, NULL, NULL, NULL FROM messages WHERE author = ?2 "+
|
||||
"ORDER BY sequence",
|
||||
[f.sequence, id],
|
||||
async function(row) {
|
||||
f.sequence = row.sequence;
|
||||
if (row.host) {
|
||||
row = {host: row.host, port: row.port, key: row.key};
|
||||
f.pubs[JSON.stringify(row)] = row;
|
||||
}
|
||||
});
|
||||
f.pubs = Object.values(f.pubs);
|
||||
var j = JSON.stringify(f);
|
||||
if (o != j) {
|
||||
await db.set(id + ":pubs", j);
|
||||
}
|
||||
return f.pubs;
|
||||
}
|
||||
|
||||
async function visiblePubs(db, id) {
|
||||
var ids = [id].concat(await following(db, id));
|
||||
var pubs = {};
|
||||
for (var follow of ids) {
|
||||
var followPubs = await pubsByUser(db, follow);
|
||||
for (var pub of followPubs) {
|
||||
pubs[JSON.stringify(pub)] = pub;
|
||||
}
|
||||
}
|
||||
return Object.values(pubs);
|
||||
}
|
||||
|
||||
async function getAbout(db, id) {
|
||||
var o = await db.get(id + ":about");
|
||||
const k_version = 4;
|
||||
var f = o ? JSON.parse(o) : o;
|
||||
if (!f || f.version != k_version) {
|
||||
f = {about: {}, sequence: 0, version: k_version};
|
||||
}
|
||||
await ssb.sqlStream(
|
||||
"SELECT "+
|
||||
" sequence, "+
|
||||
" content "+
|
||||
"FROM messages "+
|
||||
"WHERE "+
|
||||
" sequence > ?1 AND "+
|
||||
" author = ?2 AND "+
|
||||
" json_extract(content, '$.type') = 'about' AND "+
|
||||
" json_extract(content, '$.about') = author "+
|
||||
"UNION SELECT MAX(sequence) as sequence, NULL FROM messages WHERE author = ?2 "+
|
||||
"ORDER BY sequence",
|
||||
[f.sequence, id],
|
||||
async function(row) {
|
||||
f.sequence = row.sequence;
|
||||
if (row.content) {
|
||||
var about = {};
|
||||
try {
|
||||
about = JSON.parse(row.content);
|
||||
} catch {
|
||||
}
|
||||
delete about.about;
|
||||
delete about.type;
|
||||
f.about = Object.assign(f.about, about);
|
||||
}
|
||||
});
|
||||
var j = JSON.stringify(f);
|
||||
if (o != j) {
|
||||
await db.set(id + ":about", j);
|
||||
}
|
||||
return f.about;
|
||||
}
|
||||
|
||||
function fnv32a(value)
|
||||
{
|
||||
var result = 0x811c9dc5;
|
||||
for (var i = 0; i < value.length; i++) {
|
||||
result ^= value.charCodeAt(i);
|
||||
result += (result << 1) + (result << 4) + (result << 7) + (result << 8) + (result << 24);
|
||||
}
|
||||
return result >>> 0;
|
||||
}
|
||||
|
||||
async function getRecentPostIds(db, id, ids, limit) {
|
||||
const k_version = 6;
|
||||
var o = await db.get(id + ':recent_posts');
|
||||
var recent = [];
|
||||
var f = o ? JSON.parse(o) : o;
|
||||
var ids_hash = fnv32a(JSON.stringify(ids));
|
||||
if (!f || f.version != k_version || f.ids_hash != ids_hash) {
|
||||
f = {recent: [], rowid: 0, version: k_version, ids_hash: ids_hash};
|
||||
}
|
||||
await ssb.sqlStream(
|
||||
"SELECT "+
|
||||
" rowid, "+
|
||||
" id "+
|
||||
"FROM messages "+
|
||||
"WHERE "+
|
||||
" rowid > ? AND "+
|
||||
" author IN (" + ids.map(x => '?').join(", ") + ") AND "+
|
||||
" json_extract(content, '$.type') = 'post' "+
|
||||
"UNION SELECT MAX(rowid) as rowid, NULL FROM messages "+
|
||||
"ORDER BY rowid DESC LIMIT ?",
|
||||
[].concat([f.rowid], ids, [limit + 1]),
|
||||
function(row) {
|
||||
if (row.id) {
|
||||
recent.push(row.id);
|
||||
}
|
||||
if (row.rowid) {
|
||||
f.rowid = row.rowid;
|
||||
}
|
||||
});
|
||||
f.recent = [].concat(recent, f.recent).slice(0, limit);
|
||||
var j = JSON.stringify(f);
|
||||
if (o != j) {
|
||||
await db.set(id + ":recent_posts", j);
|
||||
}
|
||||
return f.recent;
|
||||
}
|
||||
|
||||
async function getVotes(db, id) {
|
||||
var o = await db.get(id + ":votes");
|
||||
const k_version = 2;
|
||||
var votes = [];
|
||||
var f = o ? JSON.parse(o) : o;
|
||||
if (!f || f.version != k_version) {
|
||||
f = {votes: [], rowid: 0, version: k_version};
|
||||
}
|
||||
await ssb.sqlStream(
|
||||
"SELECT "+
|
||||
" rowid, "+
|
||||
" author, "+
|
||||
" id, "+
|
||||
" sequence, "+
|
||||
" timestamp, "+
|
||||
" content "+
|
||||
"FROM messages "+
|
||||
"WHERE "+
|
||||
" rowid > ? AND "+
|
||||
" author = ? AND "+
|
||||
" json_extract(content, '$.type') = 'vote' "+
|
||||
"UNION SELECT MAX(rowid) as rowid, NULL, NULL AS id, NULL, NULL, NULL FROM messages "+
|
||||
"ORDER BY rowid DESC LIMIT ?",
|
||||
[f.rowid, id, k_votes_max],
|
||||
async function(row) {
|
||||
if (row.id) {
|
||||
votes.push(row);
|
||||
} else {
|
||||
f.rowid = row.rowid;
|
||||
}
|
||||
});
|
||||
f.votes = [].concat(votes.reverse(), f.votes).slice(0, k_votes_max);
|
||||
var j = JSON.stringify(f);
|
||||
if (o != j) {
|
||||
await db.set(id + ":votes", j);
|
||||
}
|
||||
return f.votes;
|
||||
}
|
||||
|
||||
async function getPosts(db, ids) {
|
||||
var posts = [];
|
||||
if (ids.length) {
|
||||
await ssb.sqlStream(
|
||||
"SELECT rowid, * FROM messages WHERE id IN (" + ids.map(x => "?").join(", ") + ")",
|
||||
ids,
|
||||
async function(row) {
|
||||
try {
|
||||
posts.push(row);
|
||||
} catch {
|
||||
}
|
||||
});
|
||||
}
|
||||
return posts;
|
||||
}
|
||||
|
||||
async function ready() {
|
||||
return refresh();
|
||||
}
|
||||
|
||||
core.register('onBroadcastsChanged', async function() {
|
||||
await app.postMessage({broadcasts: await ssb.getBroadcasts()});
|
||||
});
|
||||
|
||||
core.register('onConnectionsChanged', async function() {
|
||||
var connections = await ssb.connections();
|
||||
await app.postMessage({connections: connections});
|
||||
});
|
||||
|
||||
async function refresh() {
|
||||
await app.postMessage({clear: true});
|
||||
var whoami = await ssb.whoami();
|
||||
var db = await database("ssb");
|
||||
await Promise.all([
|
||||
app.postMessage({whoami: whoami}),
|
||||
app.postMessage({pubs: await visiblePubs(db, whoami)}),
|
||||
app.postMessage({broadcasts: await ssb.getBroadcasts()}),
|
||||
app.postMessage({connections: await ssb.connections()}),
|
||||
followingDeep(db, [whoami], 2).then(function(f) {
|
||||
getRecentPostIds(db, whoami, [].concat([whoami], f), k_posts_max).then(async function(ids) {
|
||||
return getPosts(db, ids);
|
||||
}).then(async function(posts) {
|
||||
var roots = posts.map(function(x) {
|
||||
try {
|
||||
return JSON.parse(x.content).root;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
});
|
||||
roots = roots.filter(function(root) {
|
||||
return root && posts.every(post => post.id != root);
|
||||
});
|
||||
return [].concat(posts, await getPosts(db, roots));
|
||||
}).then(async function(posts) {
|
||||
posts.forEach(async function(post) {
|
||||
await app.postMessage({message: post});
|
||||
});
|
||||
});
|
||||
f.forEach(async function(id) {
|
||||
await Promise.all([
|
||||
getVotes(db, id).then(async function(votes) {
|
||||
return Promise.all(votes.map(vote => app.postMessage({vote: vote})));
|
||||
}),
|
||||
getAbout(db, id).then(async function(user) {
|
||||
return app.postMessage({user: {user: id, about: user}});
|
||||
}),
|
||||
]);
|
||||
});
|
||||
}),
|
||||
sendUser(db, whoami),
|
||||
]);
|
||||
}
|
||||
|
||||
core.register('message', async function(m) {
|
||||
if (m.message == 'ready') {
|
||||
await ready();
|
||||
} else if (m.message) {
|
||||
if (m.message.connect) {
|
||||
await ssb.connect(m.message.connect);
|
||||
} else if (m.message.post) {
|
||||
await ssb.post(m.message.post);
|
||||
} else if (m.message.appendMessage) {
|
||||
await ssb.appendMessage(m.message.appendMessage);
|
||||
} else if (m.message.user) {
|
||||
await sendUser(await database("ssb"), m.message.user);
|
||||
} else if (m.message.refresh) {
|
||||
await refresh();
|
||||
}
|
||||
} else {
|
||||
print(JSON.stringify(m));
|
||||
}
|
||||
});
|
||||
|
||||
async function main() {
|
||||
if (core.user &&
|
||||
core.user.credentials &&
|
||||
core.user.credentials.permissions &&
|
||||
core.user.credentials.permissions.administration) {
|
||||
await app.setDocument(utf8Decode(await getFile("index.html")));
|
||||
} else {
|
||||
await app.setDocument('<div style="color: #f00">Only the administrator can use this app at this time. Login at the top right.</div>');
|
||||
}
|
||||
}
|
||||
|
||||
main();
|
@ -1,281 +0,0 @@
|
||||
<html>
|
||||
<head>
|
||||
<meta content="width=device-width,initial-scale=1,minimal-ui" name="viewport">
|
||||
<link rel="stylesheet" href="//fonts.googleapis.com/css?family=Roboto:400,500,700,400italic|Material+Icons">
|
||||
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
|
||||
<link rel="stylesheet" href="https://unpkg.com/vue-material/dist/vue-material.min.css">
|
||||
<link rel="stylesheet" href="https://unpkg.com/vue-material/dist/theme/default-dark.css">
|
||||
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
|
||||
<script src="vue-material.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/commonmark/0.29.1/commonmark.min.js"></script>
|
||||
<script>
|
||||
var g_data = {
|
||||
whoami: null,
|
||||
connections: [],
|
||||
messages: [],
|
||||
users: {},
|
||||
broadcasts: [],
|
||||
showUsers: false,
|
||||
show_connect_dialog: false,
|
||||
show_user_dialog: null,
|
||||
connect: null,
|
||||
pubs: [],
|
||||
votes: [],
|
||||
};
|
||||
var g_data_initial = JSON.parse(JSON.stringify(g_data));
|
||||
window.addEventListener('message', function(event) {
|
||||
var key = Object.keys(event.data)[0];
|
||||
if (key + 's' in g_data && Array.isArray(g_data[key + 's'])) {
|
||||
g_data[key + 's'].push(event.data[key]);
|
||||
} else if (key == 'user') {
|
||||
Vue.set(g_data.users, event.data.user.user, Object.assign({}, g_data.users[event.data.user.user] || {}, event.data.user.about));
|
||||
} else if (key == 'followers') {
|
||||
if (!g_data.users[event.data.followers.id]) {
|
||||
Vue.set(g_data.users, event.data.followers.id, {});
|
||||
}
|
||||
Vue.set(g_data.users[event.data.followers.id], 'followers', event.data.followers.users);
|
||||
} else if (key == 'following') {
|
||||
if (!g_data.users[event.data.following.id]) {
|
||||
Vue.set(g_data.users, event.data.following.id, {});
|
||||
}
|
||||
Vue.set(g_data.users[event.data.following.id], 'following', event.data.following.users);
|
||||
} else if (key == 'broadcasts') {
|
||||
g_data.broadcasts = event.data.broadcasts;
|
||||
} else if (key == 'pubs') {
|
||||
g_data.pubs = event.data.pubs;
|
||||
} else if (key == 'clear') {
|
||||
Object.keys(g_data_initial).forEach(function(key) {
|
||||
Vue.set(g_data, key, JSON.parse(JSON.stringify(g_data_initial[key])));
|
||||
});
|
||||
} else {
|
||||
g_data[key] = event.data[key];
|
||||
}
|
||||
});
|
||||
window.addEventListener('load', function() {
|
||||
Vue.use(VueMaterial.default);
|
||||
Vue.component('tf-user', {
|
||||
data: function() { return {users: g_data.users, show_user_dialog: false, show_follow_dialog: false} },
|
||||
props: ['id'],
|
||||
mounted: function() {
|
||||
window.parent.postMessage({user: this.id}, '*');
|
||||
},
|
||||
computed: {
|
||||
following: {
|
||||
get: function() {
|
||||
return g_data.users[g_data.whoami] &&
|
||||
g_data.users[g_data.whoami].following &&
|
||||
g_data.users[g_data.whoami].following.indexOf(this.id) != -1;
|
||||
},
|
||||
set: function(newValue) {
|
||||
if (g_data.users[g_data.whoami] &&
|
||||
g_data.users[g_data.whoami].following) {
|
||||
if (newValue && g_data.users[g_data.whoami].following.indexOf(this.id) == -1) {
|
||||
window.parent.postMessage({appendMessage: {type: "contact", following: true, contact: this.id}}, '*');
|
||||
} else if (!newValue) {
|
||||
window.parent.postMessage({appendMessage: {type: "contact", following: false, contact: this.id}}, '*');
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
template: `<span @click="show_user_dialog = true">
|
||||
{{users[id] && users[id].name ? users[id].name : id}}
|
||||
<md-tooltip v-if="users[id] && users[id].name">{{id}}</md-tooltip>
|
||||
<md-dialog :md-active.sync="show_user_dialog">
|
||||
<md-dialog-title>{{users[id] && users[id].name ? users[id].name : id}}</md-dialog-title>
|
||||
<md-dialog-content v-if="users[id]">
|
||||
<div v-if="users[id].image"><img :src="'/' + users[id].image + '/view'"></div>
|
||||
<div v-if="users[id].name">{{id}}</div>
|
||||
<div>{{users[id].description}}</div>
|
||||
<div><md-switch v-model="following">Following</md-switch></div>
|
||||
<md-list>
|
||||
<md-subheader>Followers</md-subheader>
|
||||
<md-list-item v-for="follower in (users[id] || []).followers" v-bind:key="'follower-' + follower">
|
||||
<tf-user :id="follower"></tf-user>
|
||||
</md-list-item>
|
||||
<md-subheader>Following</md-subheader>
|
||||
<md-list-item v-for="user in (users[id] || []).following" v-bind:key="'following-' + user">
|
||||
<tf-user :id="user"></tf-user>
|
||||
</md-list-item>
|
||||
</md-list>
|
||||
</md-dialog-content>
|
||||
<md-dialog-actions>
|
||||
<md-button @click="show_user_dialog = false">Close</md-button>
|
||||
</md-dialog-actions>
|
||||
</md-dialog>
|
||||
</span>`,
|
||||
});
|
||||
Vue.component('tf-message', {
|
||||
props: ['message', 'messages'],
|
||||
data: function() { return { showRaw: false } },
|
||||
computed: {
|
||||
content_json: function() {
|
||||
try {
|
||||
return JSON.parse(this.message.content);
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
},
|
||||
sub_messages: function() {
|
||||
var id = this.message.id;
|
||||
return this.messages.filter(function (x) {
|
||||
try {
|
||||
return JSON.parse(x.content).root == id;
|
||||
} catch {}
|
||||
});
|
||||
},
|
||||
votes: function() {
|
||||
return [];
|
||||
var id = this.message.id;
|
||||
return this.votes.filter(function (x) {
|
||||
try {
|
||||
var j = JSON.parse(x.content);
|
||||
return j.type == 'vote' && j.vote.link == id;
|
||||
} catch {}
|
||||
}).reduce(function (accum, value) {
|
||||
var expression = JSON.parse(value.content).vote.expression;
|
||||
if (!accum[expression]) {
|
||||
accum[expression] = [];
|
||||
}
|
||||
accum[expression].push(value);
|
||||
return accum;
|
||||
}, {});
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
markdown: function(md) {
|
||||
var reader = new commonmark.Parser({safe: true});
|
||||
var writer = new commonmark.HtmlRenderer();
|
||||
return writer.render(reader.parse(md));
|
||||
},
|
||||
json: function(message) {
|
||||
try {
|
||||
return JSON.parse(message.content);
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
},
|
||||
},
|
||||
template: `<md-app class="md-elevation-8" style="margin: 1em" v-if="!content_json || ['pub', 'vote'].indexOf(content_json.type) == -1">
|
||||
<md-app-toolbar>
|
||||
<h3>
|
||||
<tf-user :id="message.author"></tf-user>
|
||||
</h3>
|
||||
<div style="font-size: x-small">{{new Date(message.timestamp)}}</div>
|
||||
<div class="md-toolbar-section-end">
|
||||
<md-menu>
|
||||
<md-button md-menu-trigger class="md-icon-button"><md-icon>more_vert</md-icon></md-button>
|
||||
<md-menu-content>
|
||||
<md-menu-item v-if="!showRaw" v-on:click="showRaw = true">View Raw</md-menu-item>
|
||||
<md-menu-item v-else v-on:click="showRaw = false">View Message</md-menu-item>
|
||||
</md-menu-content>
|
||||
</md-menu>
|
||||
</div>
|
||||
</md-app-toolbar>
|
||||
<md-app-content>
|
||||
<div v-if="showRaw">{{message.content}}</div>
|
||||
<div v-else>
|
||||
<div v-if="content_json && content_json.type == 'post'">
|
||||
<div v-html="this.markdown(content_json.text)"></div>
|
||||
<img v-for="mention in content_json.mentions" v-if="mention.link && typeof(mention.link) == 'string' && mention.link.startsWith('&')" :src="'/' + mention.link + '/view'"></img>
|
||||
</div>
|
||||
<div v-else-if="content_json && content_json.type == 'contact'"><tf-user :id="message.author"></tf-user> {{content_json.following ? '==>' : '=/=>'}} <tf-user :id="content_json.contact"></tf-user></div>
|
||||
<div v-else>{{message.content}}</div>
|
||||
</div>
|
||||
<tf-message v-for="sub_message in sub_messages" v-bind:message="sub_message" v-bind:messages="messages" v-bind:key="sub_message.id"></tf-message>
|
||||
<md-chip v-for="vote in Object.keys(votes)" v-bind:key="vote">
|
||||
{{vote + (votes[vote].length > 1 ? ' (' + votes[vote].length + ')' : '')}}
|
||||
</md-chip>
|
||||
</md-app-content>
|
||||
</md-app>`,
|
||||
});
|
||||
function markdown(d) { return d; }
|
||||
Vue.config.performance = true;
|
||||
var vue = new Vue({
|
||||
el: '#app',
|
||||
data: g_data,
|
||||
methods: {
|
||||
post_message: function() {
|
||||
window.parent.postMessage({post: document.getElementById('post_text').value}, '*');
|
||||
},
|
||||
ssb_connect: function(connection) {
|
||||
window.parent.postMessage({connect: connection}, '*');
|
||||
},
|
||||
content_json: function(message) {
|
||||
try {
|
||||
return JSON.parse(message.content);
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
},
|
||||
refresh: function() {
|
||||
window.parent.postMessage({refresh: true}, '*');
|
||||
},
|
||||
}
|
||||
});
|
||||
});
|
||||
window.parent.postMessage('ready', '*');
|
||||
</script>
|
||||
</head>
|
||||
<body style="color: #fff">
|
||||
<div id="app">
|
||||
<md-dialog :md-active.sync="show_connect_dialog">
|
||||
<md-dialog-title>Connect</md-dialog-title>
|
||||
<md-dialog-content>
|
||||
<md-field>
|
||||
<label>net:127.0.0.1:8008~shs:id</label>
|
||||
<md-input v-model="connect"></md-input>
|
||||
</md-field>
|
||||
</md-dialog-content>
|
||||
<md-dialog-actions>
|
||||
<md-button class="md-primary" @click="ssb_connect(connect); connect = null; show_connect_dialog = false">Connect</md-button>
|
||||
<md-button @click="connect = null; show_connect_dialog = false">Cancel</md-button>
|
||||
</md-dialog-actions>
|
||||
</md-dialog>
|
||||
<md-app style="position: absolute; height: 100%; width: 100%">
|
||||
<md-app-toolbar class="md-primary">
|
||||
<md-button class="md-icon-button" @click="showUsers = !showUsers">
|
||||
<md-icon>menu</md-icon>
|
||||
</md-button>
|
||||
<span class="md-title">Tilde Friends Secure Scuttlebutt Test</span>
|
||||
</md-app-toolbar>
|
||||
<md-app-drawer :md-active.sync="showUsers" md-persistent="full">
|
||||
<md-list>
|
||||
<md-subheader>Followers</md-subheader>
|
||||
<md-list-item v-for="follower in (users[whoami] || []).followers" v-bind:key="'follower-' + follower"><tf-user :id="follower"></tf-user></md-list-item>
|
||||
<md-subheader>Following</md-subheader>
|
||||
<md-list-item v-for="user in (users[whoami] || []).following" v-bind:key="'following-' + user"><tf-user :id="user"></tf-user></md-list-item>
|
||||
<md-subheader>Network</md-subheader>
|
||||
<md-list-item v-for="broadcast in broadcasts" v-bind:key="JSON.stringify(broadcast)" @click="ssb_connect(broadcast)">{{broadcast.address}}:{{broadcast.port}} <tf-user :id="broadcast.pubkey"></tf-user></md-list-item>
|
||||
<md-subheader>Pubs</md-subheader>
|
||||
<md-list-item v-for="pub in pubs" v-bind:key="JSON.stringify(pub)" @click="ssb_connect({address: pub.host, port: pub.port, pubkey: pub.key})">{{pub.host}}:{{pub.port}} <tf-user :id="pub.key"></tf-user></md-list-item>
|
||||
<md-subheader>Connections</md-subheader>
|
||||
<md-list-item v-for="connection in connections" v-bind:key="'connection-' + JSON.stringify(connection)"><tf-user :id="connection"></tf-user></md-list-item>
|
||||
<md-list-item @click="show_connect_dialog = true">Connect</md-list-item>
|
||||
</md-list>
|
||||
</md-app-drawer>
|
||||
<md-app-content>
|
||||
<md-button @click="refresh()" class="md-icon-button md-dense md-raised md-primary">
|
||||
<md-icon>cached</md-icon>
|
||||
</md-button>
|
||||
Welcome, <tf-user :id="whoami"></tf-user>.
|
||||
<md-card class="md-elevation-8">
|
||||
<md-card-header>
|
||||
<div class="md-title">What's up?</div>
|
||||
</md-card-header>
|
||||
<md-card-content>
|
||||
<md-field>
|
||||
<label>Post a message</label>
|
||||
<md-textarea id="post_text"></md-textarea>
|
||||
</md-field>
|
||||
</md-card-content>
|
||||
<md-card-actions>
|
||||
<md-button class="md-raised md-primary" v-on:click="post_message()">Submit Post</md-button>
|
||||
</md-card-actions>
|
||||
</md-card>
|
||||
<tf-message v-for="message in messages" v-if="!content_json(message).root" v-bind:message="message" v-bind:messages="messages" v-bind:key="message.id"></tf-message>
|
||||
</md-app-content>
|
||||
</md-app>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
File diff suppressed because one or more lines are too long
5
apps/db.json
Normal file
5
apps/db.json
Normal file
@ -0,0 +1,5 @@
|
||||
{
|
||||
"type": "tildefriends-app",
|
||||
"emoji": "💽",
|
||||
"previous": "&uQzkIe/Aj8yNhLKe3hEq+5fEJsGwIUx8NVBjJKwoV2U=.sha256"
|
||||
}
|
83
apps/db/app.js
Normal file
83
apps/db/app.js
Normal file
@ -0,0 +1,83 @@
|
||||
async function database_list() {
|
||||
var dbs = await databases();
|
||||
var doc = `<!DOCTYPE html>
|
||||
<html>
|
||||
<body style="background: #888">
|
||||
<h1>Databases</h1>
|
||||
<ul id="dbs"></ul>
|
||||
</body>
|
||||
<script>
|
||||
function populate_dbs(id, dbs) {
|
||||
var list = document.getElementById(id);
|
||||
for (let db of dbs) {
|
||||
var li = list.appendChild(document.createElement('li'));
|
||||
var a = document.createElement('a');
|
||||
a.innerText = db;
|
||||
a.href = './#' + db;
|
||||
a.target = '_top';
|
||||
li.appendChild(a);
|
||||
}
|
||||
}
|
||||
populate_dbs('dbs', ${JSON.stringify(dbs)});
|
||||
</script>
|
||||
</html>`;
|
||||
app.setDocument(doc);
|
||||
}
|
||||
|
||||
async function key_list(db) {
|
||||
let keys = await db.getAll();
|
||||
let object = {};
|
||||
for (let key of keys) {
|
||||
object[key] = await db.get(key);
|
||||
}
|
||||
let doc = `<!DOCTYPE html>
|
||||
<html>
|
||||
<body style="background: #888">
|
||||
<a href="#" target="_top">back</a>
|
||||
<h1>Keys</h1>
|
||||
<ul id="keys"></ul>
|
||||
</body>
|
||||
<script>
|
||||
function populate_dbs(id, keys) {
|
||||
var list = document.getElementById(id);
|
||||
for (let [key, value] of Object.entries(keys)) {
|
||||
var li = list.appendChild(document.createElement('li'));
|
||||
li.innerText = key + ' = ' + value;
|
||||
}
|
||||
}
|
||||
populate_dbs('keys', ${JSON.stringify(object)});
|
||||
</script>
|
||||
</html>`;
|
||||
app.setDocument(doc);
|
||||
}
|
||||
|
||||
function load() {
|
||||
if (core.user?.credentials?.session) {
|
||||
database_list();
|
||||
} else {
|
||||
app.setDocument(`<!DOCTYPE html>
|
||||
<html>
|
||||
<body style="background: #888">
|
||||
<h1>Must be signed in to examine databases.</h1>
|
||||
</body>
|
||||
</html>`);
|
||||
}
|
||||
}
|
||||
|
||||
core.register('message', async function (message) {
|
||||
if (message.event == 'hashChange') {
|
||||
let hash = message.hash.substring(1);
|
||||
if (hash.startsWith(':shared:')) {
|
||||
let parts = hash.split(':');
|
||||
let packageName = parts[3];
|
||||
let key = parts.slice(4).join(':');
|
||||
key_list(await my_shared_database(packageName, key));
|
||||
} else if (hash.length) {
|
||||
key_list(await database(hash.split(':').slice(1).join(':')));
|
||||
} else {
|
||||
load();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
load();
|
4
apps/follow.json
Normal file
4
apps/follow.json
Normal file
@ -0,0 +1,4 @@
|
||||
{
|
||||
"type": "tildefriends-app",
|
||||
"emoji": "➡️"
|
||||
}
|
317
apps/follow/app.js
Normal file
317
apps/follow/app.js
Normal file
@ -0,0 +1,317 @@
|
||||
let g_about_cache = {};
|
||||
|
||||
async function query(sql, args) {
|
||||
let result = [];
|
||||
await ssb.sqlAsync(sql, args, function (row) {
|
||||
result.push(row);
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
async function 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 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 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];
|
||||
}
|
||||
return [...new Set(Object.values(result).flat())];
|
||||
}
|
||||
|
||||
async function following_deep(ids, depth, blocking) {
|
||||
let db = await database('cache');
|
||||
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) {
|
||||
if (g_about_cache[id]) {
|
||||
return g_about_cache[id];
|
||||
}
|
||||
let o = await db.get(id + ':about');
|
||||
const k_version = 4;
|
||||
let f = o ? JSON.parse(o) : o;
|
||||
if (!f || f.version != k_version) {
|
||||
f = {about: {}, sequence: 0, version: k_version};
|
||||
}
|
||||
await ssb.sqlAsync(
|
||||
'SELECT ' +
|
||||
' sequence, ' +
|
||||
' content ' +
|
||||
'FROM messages ' +
|
||||
'WHERE ' +
|
||||
' author = ?1 AND ' +
|
||||
' sequence > ?2 AND ' +
|
||||
" json_extract(content, '$.type') = 'about' AND " +
|
||||
" json_extract(content, '$.about') = ?1 " +
|
||||
'UNION SELECT MAX(sequence) as sequence, NULL FROM messages WHERE author = ?1 ' +
|
||||
'ORDER BY sequence',
|
||||
[id, f.sequence],
|
||||
function (row) {
|
||||
f.sequence = row.sequence;
|
||||
if (row.content) {
|
||||
let about = {};
|
||||
try {
|
||||
about = JSON.parse(row.content);
|
||||
} catch {}
|
||||
delete about.about;
|
||||
delete about.type;
|
||||
f.about = Object.assign(f.about, about);
|
||||
}
|
||||
}
|
||||
);
|
||||
let j = JSON.stringify(f);
|
||||
if (o != j) {
|
||||
await db.set(id + ':about', j);
|
||||
}
|
||||
g_about_cache[id] = f.about;
|
||||
return f.about;
|
||||
}
|
||||
|
||||
async function getSize(db, id) {
|
||||
let size = 0;
|
||||
await ssb.sqlAsync(
|
||||
'SELECT (LENGTH(author) * COUNT(*) + SUM(LENGTH(content)) + SUM(LENGTH(id))) AS size FROM messages WHERE author = ?1',
|
||||
[id],
|
||||
function (row) {
|
||||
size += row.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) {
|
||||
let value = bytes;
|
||||
let unit = 'B';
|
||||
const k_units = ['kB', 'MB', 'GB', 'TB'];
|
||||
for (let u of k_units) {
|
||||
if (value >= 1024) {
|
||||
value /= 1024;
|
||||
unit = u;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return Math.round(value * 10) / 10 + ' ' + unit;
|
||||
}
|
||||
|
||||
function escape(value) {
|
||||
return value
|
||||
.replaceAll('&', '&')
|
||||
.replaceAll('<', '<')
|
||||
.replaceAll('>', '>');
|
||||
}
|
||||
|
||||
async function main() {
|
||||
await app.setDocument('<pre style="color: #fff">building...</pre>');
|
||||
let db = await database('ssb');
|
||||
let whoami = await ssb.getIdentities();
|
||||
let tree = '';
|
||||
await app.setDocument(
|
||||
`<pre style="color: #fff">Enumerating followed users...</pre>`
|
||||
);
|
||||
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(
|
||||
'<!DOCTYPE html>\n<html>\n<body style="color: #fff"><h1>Following</h1>\n<ul>' +
|
||||
tree +
|
||||
'</ul>\n</body>\n</html>'
|
||||
);
|
||||
}
|
||||
|
||||
main();
|
5
apps/identity.json
Normal file
5
apps/identity.json
Normal file
@ -0,0 +1,5 @@
|
||||
{
|
||||
"type": "tildefriends-app",
|
||||
"emoji": "🪪",
|
||||
"previous": "&5kw/2PgcySwOYCmAkjHTR2xTkIx3i7UjQmtQ8MfgWw8=.sha256"
|
||||
}
|
153
apps/identity/app.js
Normal file
153
apps/identity/app.js
Normal file
@ -0,0 +1,153 @@
|
||||
import * as tfrpc from '/tfrpc.js';
|
||||
|
||||
const is_admin = core.user?.credentials?.permissions?.administration;
|
||||
|
||||
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();
|
||||
});
|
||||
tfrpc.register(async function make_server(id) {
|
||||
return await ssb.swapWithServerIdentity(id);
|
||||
});
|
||||
|
||||
async function main() {
|
||||
let ids = await ssb.getIdentities();
|
||||
let server_id = await ssb.getServerIdentity();
|
||||
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);
|
||||
}
|
||||
}
|
||||
handler.make_server = async function make_server(event) {
|
||||
let id = event.srcElement.dataset.id;
|
||||
try {
|
||||
if (confirm('Are you sure you want to make "' + id + '" the server identity?\\n\\nFor it to take effect, you will need to both sign in again and restart Tilde Friends.')) {
|
||||
await tfrpc.rpc.make_server(id);
|
||||
}
|
||||
} catch (e) {
|
||||
alert('Error making server ID: ' + e);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<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>
|
||||
${is_admin && id != server_id ? `<button onclick="handler.make_server(event)" data-id="${id}" class="w3-button w3-theme">Make Server Identity</button>` : ''}
|
||||
${id}${id == server_id ? ' <div class="w3-tag w3-theme-l4 w3-round">🖥 local server</div>' : ''}
|
||||
</li>`
|
||||
)
|
||||
.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();
|
91
apps/issues/commonmark-linkify.js
Normal file
91
apps/issues/commonmark-linkify.js
Normal file
@ -0,0 +1,91 @@
|
||||
function textNode(text) {
|
||||
const node = new commonmark.Node("text", undefined);
|
||||
node.literal = text;
|
||||
return node;
|
||||
}
|
||||
|
||||
function linkNode(text, url) {
|
||||
const urlNode = new commonmark.Node("link", undefined);
|
||||
urlNode.destination = url;
|
||||
urlNode.appendChild(textNode(text));
|
||||
|
||||
return urlNode;
|
||||
}
|
||||
|
||||
function splitMatches(text, regexp) {
|
||||
// Regexp must be sticky.
|
||||
regexp = new RegExp(regexp, "gm");
|
||||
|
||||
let i = 0;
|
||||
const result = [];
|
||||
|
||||
let match = regexp.exec(text);
|
||||
while (match) {
|
||||
const matchText = match[0];
|
||||
|
||||
if (match.index > i) {
|
||||
result.push([text.substring(i, match.index), false]);
|
||||
}
|
||||
|
||||
result.push([matchText, true]);
|
||||
i = match.index + matchText.length;
|
||||
|
||||
match = regexp.exec(text);
|
||||
}
|
||||
|
||||
if (i < text.length) {
|
||||
result.push([text.substring(i, text.length), false]);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
const urlRegexp = new RegExp("https?://[^ ]+[^ .,]");
|
||||
|
||||
function splitURLs(textNodes) {
|
||||
const text = textNodes.map(n => n.literal).join("");
|
||||
const parts = splitMatches(text, urlRegexp);
|
||||
|
||||
return parts.map(part => {
|
||||
if (part[1]) {
|
||||
return linkNode(part[0], part[0]);
|
||||
} else {
|
||||
return textNode(part[0]);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export function transform(parsed) {
|
||||
const walker = parsed.walker();
|
||||
let event;
|
||||
|
||||
let nodes = [];
|
||||
while ((event = walker.next())) {
|
||||
const node = event.node;
|
||||
if (event.entering && node.type === "text") {
|
||||
nodes.push(node);
|
||||
} else {
|
||||
if (nodes.length > 0) {
|
||||
splitURLs(nodes)
|
||||
.reverse()
|
||||
.forEach(newNode => {
|
||||
nodes[0].insertAfter(newNode);
|
||||
});
|
||||
|
||||
nodes.forEach(n => n.unlink());
|
||||
nodes = [];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (nodes.length > 0) {
|
||||
splitURLs(nodes)
|
||||
.reverse()
|
||||
.forEach(newNode => {
|
||||
nodes[0].insertAfter(newNode);
|
||||
});
|
||||
nodes.forEach(n => n.unlink());
|
||||
}
|
||||
|
||||
return parsed;
|
||||
}
|
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);
|
5
apps/ssb.json
Normal file
5
apps/ssb.json
Normal file
@ -0,0 +1,5 @@
|
||||
{
|
||||
"type": "tildefriends-app",
|
||||
"emoji": "🐌",
|
||||
"previous": "&0ZjcRFTyAmJMm3qhqKy5Ffrue23G1CAUfc1i2WYZrzQ=.sha256"
|
||||
}
|
126
apps/ssb/app.js
Normal file
126
apps/ssb/app.js
Normal file
@ -0,0 +1,126 @@
|
||||
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 getServerIdentity() {
|
||||
return ssb.getServerIdentity();
|
||||
});
|
||||
tfrpc.register(async function setServerFollowingMe(id, following) {
|
||||
return ssb.setServerFollowingMe(id, following);
|
||||
});
|
||||
tfrpc.register(async function getIdentities() {
|
||||
return ssb.getIdentities();
|
||||
});
|
||||
tfrpc.register(async function getAllIdentities() {
|
||||
return ssb.getAllIdentities();
|
||||
});
|
||||
tfrpc.register(async function following(ids, depth) {
|
||||
return ssb.following(ids, depth);
|
||||
});
|
||||
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);
|
||||
});
|
||||
core.register('onMessage', async function (id) {
|
||||
await tfrpc.rpc.notifyNewMessage(id);
|
||||
});
|
||||
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(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();
|
||||
});
|
||||
tfrpc.register(async function sync() {
|
||||
return await ssb.sync();
|
||||
});
|
||||
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());
|
||||
});
|
||||
core.register('setActiveIdentity', async function (id) {
|
||||
await tfrpc.rpc.set('identity', id);
|
||||
});
|
||||
|
||||
async function main() {
|
||||
if (typeof database !== 'undefined') {
|
||||
g_database = await database('ssb');
|
||||
}
|
||||
await app.setDocument(utf8Decode(await getFile('index.html')));
|
||||
}
|
||||
main();
|
94
apps/ssb/commonmark-hashtag.js
Normal file
94
apps/ssb/commonmark-hashtag.js
Normal file
@ -0,0 +1,94 @@
|
||||
function textNode(text) {
|
||||
const node = new commonmark.Node('text', undefined);
|
||||
node.literal = text;
|
||||
return node;
|
||||
}
|
||||
|
||||
function linkNode(text, link) {
|
||||
const linkNode = new commonmark.Node('link', undefined);
|
||||
if (link.startsWith('#')) {
|
||||
linkNode.destination = `#q=${encodeURIComponent(link)}`;
|
||||
} else {
|
||||
linkNode.destination = link;
|
||||
}
|
||||
linkNode.appendChild(textNode(text));
|
||||
return linkNode;
|
||||
}
|
||||
|
||||
function splitMatches(text, regexp) {
|
||||
// Regexp must be sticky.
|
||||
regexp = new RegExp(regexp, 'gm');
|
||||
|
||||
let i = 0;
|
||||
const result = [];
|
||||
|
||||
let match = regexp.exec(text);
|
||||
while (match) {
|
||||
const matchText = match[0];
|
||||
|
||||
if (match.index > i) {
|
||||
result.push([text.substring(i, match.index), false]);
|
||||
}
|
||||
|
||||
result.push([matchText, true]);
|
||||
i = match.index + matchText.length;
|
||||
|
||||
match = regexp.exec(text);
|
||||
}
|
||||
|
||||
if (i < text.length) {
|
||||
result.push([text.substring(i, text.length), false]);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
const regex = new RegExp('(?:https?://[^ ]+[^ .,])|(?:(?<!\\w)#[\\w-]+)|(?:@[A-Za-z0-9+/]+=.ed25519)|(?:[%&][A-Za-z0-9+/]+=.sha256)');
|
||||
|
||||
function split(textNodes) {
|
||||
const text = textNodes.map((n) => n.literal).join('');
|
||||
const parts = splitMatches(text, regex);
|
||||
|
||||
return parts.map((part) => {
|
||||
if (part[1]) {
|
||||
return linkNode(part[0], part[0]);
|
||||
} else {
|
||||
return textNode(part[0]);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export function transform(parsed) {
|
||||
const walker = parsed.walker();
|
||||
let event;
|
||||
|
||||
let nodes = [];
|
||||
while ((event = walker.next())) {
|
||||
const node = event.node;
|
||||
if (event.entering && node.type === 'text') {
|
||||
nodes.push(node);
|
||||
} else {
|
||||
if (nodes.length > 0) {
|
||||
split(nodes)
|
||||
.reverse()
|
||||
.forEach((newNode) => {
|
||||
nodes[0].insertAfter(newNode);
|
||||
});
|
||||
|
||||
nodes.forEach((n) => n.unlink());
|
||||
nodes = [];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (nodes.length > 0) {
|
||||
split(nodes)
|
||||
.reverse()
|
||||
.forEach((newNode) => {
|
||||
nodes[0].insertAfter(newNode);
|
||||
});
|
||||
nodes.forEach((n) => n.unlink());
|
||||
}
|
||||
|
||||
return parsed;
|
||||
}
|
1
apps/ssb/commonmark.min.js
vendored
Normal file
1
apps/ssb/commonmark.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
164
apps/ssb/emojis.js
Normal file
164
apps/ssb/emojis.js
Normal file
@ -0,0 +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;
|
||||
|
||||
function get_emojis() {
|
||||
if (g_emojis) {
|
||||
return Promise.resolve(g_emojis);
|
||||
}
|
||||
return fetch('emojis.json').then(function (result) {
|
||||
g_emojis = result.json();
|
||||
return g_emojis;
|
||||
});
|
||||
}
|
||||
|
||||
async function get_recent(author) {
|
||||
let recent = await tfrpc.rpc.query(
|
||||
`
|
||||
SELECT DISTINCT content ->> '$.vote.expression' AS value
|
||||
FROM messages
|
||||
WHERE author = ? AND
|
||||
content ->> '$.type' = 'vote'
|
||||
ORDER BY timestamp DESC LIMIT 10
|
||||
`,
|
||||
[author]
|
||||
);
|
||||
return recent.map((x) => x.value);
|
||||
}
|
||||
|
||||
export async function picker(callback, anchor, author) {
|
||||
let json = await get_emojis();
|
||||
let recent = await get_recent(author);
|
||||
|
||||
let div = document.createElement('div');
|
||||
div.id = 'emoji_picker';
|
||||
div.style.color = '#000';
|
||||
div.style.background = '#fff';
|
||||
div.style.border = '1px solid #000';
|
||||
div.style.display = 'block';
|
||||
div.style.overflow = 'scroll';
|
||||
div.style.fontWeight = 'bold';
|
||||
div.style.fontSize = 'xx-large';
|
||||
let input = document.createElement('input');
|
||||
input.type = 'text';
|
||||
input.style.display = 'block';
|
||||
input.style.boxSizing = 'border-box';
|
||||
input.style.width = '100%';
|
||||
input.style.margin = '0';
|
||||
input.style.position = 'relative';
|
||||
div.appendChild(input);
|
||||
let list = document.createElement('div');
|
||||
div.appendChild(list);
|
||||
div.addEventListener('mousedown', function (event) {
|
||||
event.stopPropagation();
|
||||
});
|
||||
|
||||
function key_down(event) {
|
||||
if (event.key == 'Escape') {
|
||||
cleanup();
|
||||
}
|
||||
}
|
||||
|
||||
function chosen(event) {
|
||||
console.log(event.srcElement.innerText);
|
||||
callback(event.srcElement.innerText);
|
||||
cleanup();
|
||||
}
|
||||
|
||||
function refresh() {
|
||||
while (list.firstChild) {
|
||||
list.removeChild(list.firstChild);
|
||||
}
|
||||
let search = input.value.toLowerCase();
|
||||
let any_at_all = false;
|
||||
if (recent) {
|
||||
let emoji_to_name = {};
|
||||
for (let row of Object.values(json)) {
|
||||
for (let entry of Object.entries(row)) {
|
||||
emoji_to_name[entry[1]] = entry[0];
|
||||
}
|
||||
}
|
||||
let header = document.createElement('div');
|
||||
header.appendChild(document.createTextNode('Recent'));
|
||||
list.appendChild(header);
|
||||
let any = false;
|
||||
for (let entry of recent) {
|
||||
if (
|
||||
search &&
|
||||
search.length &&
|
||||
(emoji_to_name[entry] || '').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 = emoji_to_name[entry] || entry;
|
||||
emoji.appendChild(document.createTextNode(entry));
|
||||
list.appendChild(emoji);
|
||||
any = true;
|
||||
}
|
||||
if (!any) {
|
||||
list.removeChild(header);
|
||||
}
|
||||
}
|
||||
for (let row of Object.entries(json)) {
|
||||
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);
|
||||
}
|
1
apps/ssb/emojis.json
Normal file
1
apps/ssb/emojis.json
Normal file
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
24
apps/ssb/index.html
Normal file
24
apps/ssb/index.html
Normal file
@ -0,0 +1,24 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Tilde Friends</title>
|
||||
<base target="_top" />
|
||||
<link rel="stylesheet" href="tribute.css" />
|
||||
<style>
|
||||
.tribute-container {
|
||||
color: #000;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body style="margin: 0; padding: 0">
|
||||
<tf-app></tf-app>
|
||||
<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-hashtag.js" type="module"></script>
|
||||
<script src="script.js" type="module"></script>
|
||||
</body>
|
||||
</html>
|
120
apps/ssb/lit-all.min.js
vendored
Normal file
120
apps/ssb/lit-all.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
apps/ssb/lit-all.min.js.map
Normal file
1
apps/ssb/lit-all.min.js.map
Normal file
File diff suppressed because one or more lines are too long
17
apps/ssb/script.js
Normal file
17
apps/ssb/script.js
Normal file
@ -0,0 +1,17 @@
|
||||
import {LitElement, html} from './lit-all.min.js';
|
||||
import * as tfrpc from '/static/tfrpc.js';
|
||||
|
||||
import * as tf_app from './tf-app.js';
|
||||
import * as tf_message from './tf-message.js';
|
||||
import * as tf_user from './tf-user.js';
|
||||
import * as tf_compose from './tf-compose.js';
|
||||
import * as tf_news from './tf-news.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_feed from './tf-tab-news-feed.js';
|
||||
import * as tf_tab_search from './tf-tab-search.js';
|
||||
import * as tf_tab_connections from './tf-tab-connections.js';
|
||||
import * as tf_tab_query from './tf-tab-query.js';
|
||||
import * as tf_tag from './tf-tag.js';
|
389
apps/ssb/tf-app.js
Normal file
389
apps/ssb/tf-app.js
Normal file
@ -0,0 +1,389 @@
|
||||
import {LitElement, html, css, guard, until} from './lit-all.min.js';
|
||||
import * as tfrpc from '/static/tfrpc.js';
|
||||
import {styles} from './tf-styles.js';
|
||||
|
||||
class TfElement extends LitElement {
|
||||
static get properties() {
|
||||
return {
|
||||
whoami: {type: String},
|
||||
hash: {type: String},
|
||||
unread: {type: Array},
|
||||
tab: {type: String},
|
||||
broadcasts: {type: Array},
|
||||
connections: {type: Array},
|
||||
loading: {type: Boolean},
|
||||
loaded: {type: Boolean},
|
||||
following: {type: Array},
|
||||
users: {type: Object},
|
||||
ids: {type: Array},
|
||||
tags: {type: Array},
|
||||
};
|
||||
}
|
||||
|
||||
static styles = styles;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
let self = this;
|
||||
this.hash = '#';
|
||||
this.unread = [];
|
||||
this.tab = 'news';
|
||||
this.broadcasts = [];
|
||||
this.connections = [];
|
||||
this.following = [];
|
||||
this.users = {};
|
||||
this.loaded = false;
|
||||
this.tags = [];
|
||||
tfrpc.rpc.getBroadcasts().then((b) => {
|
||||
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) {
|
||||
self.set_hash(hash);
|
||||
});
|
||||
tfrpc.register(async function notifyNewMessage(id) {
|
||||
await self.fetch_new_message(id);
|
||||
});
|
||||
tfrpc.register(function set(name, value) {
|
||||
if (name === 'broadcasts') {
|
||||
self.broadcasts = value;
|
||||
} else if (name === 'connections') {
|
||||
self.connections = value;
|
||||
} else if (name === 'identity') {
|
||||
self.whoami = value;
|
||||
}
|
||||
});
|
||||
this.initial_load();
|
||||
}
|
||||
|
||||
async initial_load() {
|
||||
let whoami = await tfrpc.rpc.getActiveIdentity();
|
||||
let ids = (await tfrpc.rpc.getIdentities()) || [];
|
||||
this.whoami = whoami ?? (ids.length ? ids[0] : undefined);
|
||||
this.ids = ids;
|
||||
}
|
||||
|
||||
set_hash(hash) {
|
||||
this.hash = decodeURIComponent(hash || '#');
|
||||
if (this.hash.startsWith('#q=')) {
|
||||
this.tab = 'search';
|
||||
} else if (this.hash === '#connections') {
|
||||
this.tab = 'connections';
|
||||
} else if (this.hash === '#mentions') {
|
||||
this.tab = 'mentions';
|
||||
} else if (this.hash.startsWith('#sql=')) {
|
||||
this.tab = 'query';
|
||||
} else {
|
||||
this.tab = 'news';
|
||||
}
|
||||
}
|
||||
|
||||
async fetch_about(ids, users) {
|
||||
const k_cache_version = 1;
|
||||
let cache = await tfrpc.rpc.databaseGet('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 = (
|
||||
await tfrpc.rpc.query(
|
||||
`
|
||||
SELECT MAX(rowid) AS max_row_id FROM messages
|
||||
`,
|
||||
[]
|
||||
)
|
||||
)[0].max_row_id;
|
||||
for (let id of Object.keys(cache.about)) {
|
||||
if (ids.indexOf(id) == -1) {
|
||||
delete cache.about[id];
|
||||
}
|
||||
}
|
||||
|
||||
let abouts = await tfrpc.rpc.query(
|
||||
`
|
||||
SELECT
|
||||
messages.id, messages.previous, messages.author, messages.sequence, messages.timestamp, messages.hash, json(messages.content) AS content, messages.signature
|
||||
FROM
|
||||
messages,
|
||||
json_each(?1) AS following
|
||||
WHERE
|
||||
messages.author = following.value AND
|
||||
messages.rowid > ?3 AND
|
||||
messages.rowid <= ?4 AND
|
||||
json_extract(messages.content, '$.type') = 'about'
|
||||
UNION
|
||||
SELECT
|
||||
messages.id, messages.previous, messages.author, messages.sequence, messages.timestamp, messages.hash, json(messages.content) AS content, messages.signature
|
||||
FROM
|
||||
messages,
|
||||
json_each(?2) AS following
|
||||
WHERE
|
||||
messages.author = following.value AND
|
||||
messages.rowid <= ?4 AND
|
||||
json_extract(messages.content, '$.type') = 'about'
|
||||
ORDER BY messages.author, messages.sequence
|
||||
`,
|
||||
[
|
||||
JSON.stringify(ids.filter((id) => cache.about[id])),
|
||||
JSON.stringify(ids.filter((id) => !cache.about[id])),
|
||||
cache.last_row_id,
|
||||
max_row_id,
|
||||
]
|
||||
);
|
||||
for (let about of abouts) {
|
||||
let content = JSON.parse(about.content);
|
||||
if (content.about === about.author) {
|
||||
delete content.type;
|
||||
delete content.about;
|
||||
cache.about[about.author] = Object.assign(
|
||||
cache.about[about.author] || {},
|
||||
content
|
||||
);
|
||||
}
|
||||
}
|
||||
cache.last_row_id = max_row_id;
|
||||
await tfrpc.rpc.databaseSet('about', JSON.stringify(cache));
|
||||
users = users || {};
|
||||
for (let id of Object.keys(cache.about)) {
|
||||
users[id] = Object.assign(users[id] || {}, cache.about[id]);
|
||||
}
|
||||
return Object.assign({}, users);
|
||||
}
|
||||
|
||||
async fetch_new_message(id) {
|
||||
let messages = await tfrpc.rpc.query(
|
||||
`
|
||||
SELECT messages.id, previous, author, sequence, timestamp, hash, json(content) AS content, signature
|
||||
FROM messages
|
||||
JOIN json_each(?) AS following ON messages.author = following.value
|
||||
WHERE messages.id = ?
|
||||
`,
|
||||
[JSON.stringify(this.following), id]
|
||||
);
|
||||
if (messages && messages.length) {
|
||||
this.unread = [...this.unread, ...messages];
|
||||
this.unread = this.unread.slice(this.unread.length - 1024);
|
||||
}
|
||||
}
|
||||
|
||||
async _handle_whoami_changed(event) {
|
||||
let old_id = this.whoami;
|
||||
let new_id = event.srcElement.selected;
|
||||
console.log('received', new_id);
|
||||
if (this.whoami !== new_id) {
|
||||
console.log(event);
|
||||
this.whoami = new_id;
|
||||
console.log(`whoami ${old_id} => ${new_id}`);
|
||||
await tfrpc.rpc.localStorageSet('whoami', new_id);
|
||||
}
|
||||
}
|
||||
|
||||
async create_identity() {
|
||||
if (confirm('Are you sure you want to create a new identity?')) {
|
||||
await tfrpc.rpc.createIdentity();
|
||||
this.ids = (await tfrpc.rpc.getIdentities()) || [];
|
||||
if (this.ids && !this.whoami) {
|
||||
this.whoami = this.ids[0];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async load_recent_tags() {
|
||||
let start = new Date();
|
||||
this.tags = await tfrpc.rpc.query(
|
||||
`
|
||||
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() {
|
||||
let whoami = this.whoami;
|
||||
let tags = this.load_recent_tags();
|
||||
let following = await tfrpc.rpc.following([whoami], 2);
|
||||
let users = {};
|
||||
let by_count = [];
|
||||
for (let [id, v] of Object.entries(following)) {
|
||||
users[id] = {
|
||||
following: v.of,
|
||||
blocking: v.ob,
|
||||
followed: v.if,
|
||||
blocked: v.ib,
|
||||
};
|
||||
by_count.push({count: v.of, id: id});
|
||||
}
|
||||
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;
|
||||
await tags;
|
||||
console.log(`load finished ${whoami} => ${this.whoami}`);
|
||||
this.whoami = whoami;
|
||||
this.loaded = whoami;
|
||||
}
|
||||
|
||||
render_tab() {
|
||||
let following = this.following;
|
||||
let users = this.users;
|
||||
if (this.tab === 'news') {
|
||||
return html`
|
||||
<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') {
|
||||
return html`
|
||||
<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') {
|
||||
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>
|
||||
`;
|
||||
} else if (this.tab === 'query') {
|
||||
return html`
|
||||
<tf-tab-query
|
||||
.following=${this.following}
|
||||
whoami=${this.whoami}
|
||||
.users=${this.users}
|
||||
query=${this.hash?.startsWith('#sql=')
|
||||
? decodeURIComponent(this.hash.substring(5))
|
||||
: null}
|
||||
></tf-tab-query>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
async set_tab(tab) {
|
||||
this.tab = tab;
|
||||
if (tab === 'news') {
|
||||
await tfrpc.rpc.setHash('#');
|
||||
} else if (tab === 'connections') {
|
||||
await tfrpc.rpc.setHash('#connections');
|
||||
} else if (tab === 'mentions') {
|
||||
await tfrpc.rpc.setHash('#mentions');
|
||||
} else if (tab === 'query') {
|
||||
await tfrpc.rpc.setHash('#sql=');
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
let self = this;
|
||||
|
||||
if (!this.loading && this.whoami && this.loaded !== this.whoami) {
|
||||
this.loading = true;
|
||||
this.load().finally(function () {
|
||||
self.loading = false;
|
||||
});
|
||||
}
|
||||
|
||||
const k_tabs = {
|
||||
'📰': 'news',
|
||||
'📡': 'connections',
|
||||
'@': 'mentions',
|
||||
'🔍': 'search',
|
||||
'👩💻': 'query',
|
||||
};
|
||||
|
||||
let tabs = html`
|
||||
<div class="w3-bar w3-theme-l1">
|
||||
${Object.entries(k_tabs).map(
|
||||
([k, v]) => html`
|
||||
<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>
|
||||
`;
|
||||
let contents = !this.loaded
|
||||
? this.loading
|
||||
? html`<div
|
||||
class="w3-panel w3-theme-l5 w3-card-4 w3-padding-large w3-round-xlarge"
|
||||
>
|
||||
Loading...
|
||||
</div>
|
||||
${this.render_tab()}`
|
||||
: html`<div>Select or create an identity.</div>`
|
||||
: this.render_tab();
|
||||
return html`
|
||||
<div
|
||||
style="width: 100vw; min-height: 100vh; height: 100%"
|
||||
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>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define('tf-app', TfElement);
|
577
apps/ssb/tf-compose.js
Normal file
577
apps/ssb/tf-compose.js
Normal file
@ -0,0 +1,577 @@
|
||||
import {LitElement, html, unsafeHTML, live} from './lit-all.min.js';
|
||||
import * as tfutils from './tf-utils.js';
|
||||
import * as tfrpc from '/static/tfrpc.js';
|
||||
import {styles} from './tf-styles.js';
|
||||
import Tribute from './tribute.esm.js';
|
||||
|
||||
class TfComposeElement extends LitElement {
|
||||
static get properties() {
|
||||
return {
|
||||
whoami: {type: String},
|
||||
users: {type: Object},
|
||||
root: {type: String},
|
||||
branch: {type: String},
|
||||
apps: {type: Object},
|
||||
drafts: {type: Object},
|
||||
author: {type: String},
|
||||
};
|
||||
}
|
||||
|
||||
static styles = styles;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.users = {};
|
||||
this.root = undefined;
|
||||
this.branch = undefined;
|
||||
this.apps = undefined;
|
||||
this.drafts = {};
|
||||
this.author = undefined;
|
||||
}
|
||||
|
||||
process_text(text) {
|
||||
if (!text) {
|
||||
return '';
|
||||
}
|
||||
/* Update mentions. */
|
||||
let draft = this.get_draft();
|
||||
let updated = false;
|
||||
for (let match of text.matchAll(/\[([^\[]+)]\(([@&%][^\)]+)/g)) {
|
||||
let name = match[1];
|
||||
let link = match[2];
|
||||
let balance = 0;
|
||||
let bracket_end = match.index + match[1].length + '[]'.length - 1;
|
||||
for (let i = bracket_end; i >= 0; i--) {
|
||||
if (text.charAt(i) == ']') {
|
||||
balance++;
|
||||
} else if (text.charAt(i) == '[') {
|
||||
balance--;
|
||||
}
|
||||
if (balance <= 0) {
|
||||
name = text.substring(i + 1, bracket_end);
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!draft.mentions) {
|
||||
draft.mentions = {};
|
||||
}
|
||||
if (!draft.mentions[link]) {
|
||||
draft.mentions[link] = {
|
||||
link: link,
|
||||
};
|
||||
}
|
||||
draft.mentions[link].name = name.startsWith('@')
|
||||
? name.substring(1)
|
||||
: name;
|
||||
updated = true;
|
||||
}
|
||||
if (updated) {
|
||||
setTimeout(() => this.notify(draft), 0);
|
||||
}
|
||||
return tfutils.markdown(text);
|
||||
}
|
||||
|
||||
input(event) {
|
||||
let edit = this.renderRoot.getElementById('edit');
|
||||
let preview = this.renderRoot.getElementById('preview');
|
||||
preview.innerHTML = this.process_text(edit.innerText);
|
||||
let content_warning = this.renderRoot.getElementById('content_warning');
|
||||
let draft = this.get_draft();
|
||||
draft.text = edit.innerText;
|
||||
draft.content_warning = content_warning?.value;
|
||||
setTimeout(() => this.notify(draft), 0);
|
||||
}
|
||||
|
||||
notify(draft) {
|
||||
this.dispatchEvent(
|
||||
new CustomEvent('tf-draft', {
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
detail: {
|
||||
id: this.branch,
|
||||
draft: draft,
|
||||
},
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
convert_to_format(buffer, type, mime_type) {
|
||||
return new Promise(function (resolve, reject) {
|
||||
let img = new Image();
|
||||
img.onload = function () {
|
||||
let canvas = document.createElement('canvas');
|
||||
let width_scale = Math.min(img.width, 1024) / img.width;
|
||||
let height_scale = Math.min(img.height, 1024) / img.height;
|
||||
let scale = Math.min(width_scale, height_scale);
|
||||
canvas.width = img.width * scale;
|
||||
canvas.height = img.height * scale;
|
||||
let context = canvas.getContext('2d');
|
||||
context.drawImage(img, 0, 0, canvas.width, canvas.height);
|
||||
let data_url = canvas.toDataURL(mime_type);
|
||||
let result = atob(data_url.split(',')[1])
|
||||
.split('')
|
||||
.map((x) => x.charCodeAt(0));
|
||||
resolve(result);
|
||||
};
|
||||
img.onerror = function (event) {
|
||||
reject(new Error('Failed to load image.'));
|
||||
};
|
||||
let raw = Array.from(new Uint8Array(buffer))
|
||||
.map((b) => String.fromCharCode(b))
|
||||
.join('');
|
||||
let original = `data:${type};base64,${btoa(raw)}`;
|
||||
img.src = original;
|
||||
});
|
||||
}
|
||||
|
||||
async add_file(file) {
|
||||
try {
|
||||
let draft = this.get_draft();
|
||||
let self = this;
|
||||
let buffer = await file.arrayBuffer();
|
||||
let type = file.type;
|
||||
if (type.startsWith('image/')) {
|
||||
let best_buffer;
|
||||
let best_type;
|
||||
for (let format of ['image/png', 'image/jpeg', 'image/webp']) {
|
||||
let test_buffer = await self.convert_to_format(
|
||||
buffer,
|
||||
file.type,
|
||||
format
|
||||
);
|
||||
if (!best_buffer || test_buffer.length < best_buffer.length) {
|
||||
best_buffer = test_buffer;
|
||||
best_type = format;
|
||||
}
|
||||
}
|
||||
buffer = best_buffer;
|
||||
type = best_type;
|
||||
} else {
|
||||
buffer = Array.from(new Uint8Array(buffer));
|
||||
}
|
||||
let id = await tfrpc.rpc.store_blob(buffer);
|
||||
let name = type.split('/')[0] + ':' + file.name;
|
||||
if (!draft.mentions) {
|
||||
draft.mentions = {};
|
||||
}
|
||||
draft.mentions[id] = {
|
||||
link: id,
|
||||
name: name,
|
||||
type: type,
|
||||
size: buffer.length ?? buffer.byteLength,
|
||||
};
|
||||
let edit = self.renderRoot.getElementById('edit');
|
||||
edit.innerText += `\n![${name}](${id})`;
|
||||
self.input();
|
||||
} catch (e) {
|
||||
alert(e?.message);
|
||||
}
|
||||
}
|
||||
|
||||
paste(event) {
|
||||
let self = this;
|
||||
for (let item of event.clipboardData.items) {
|
||||
if (item.type?.startsWith('image/')) {
|
||||
let file = item.getAsFile();
|
||||
if (!file) {
|
||||
continue;
|
||||
}
|
||||
self.add_file(file);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
document.execCommand(
|
||||
'insertText',
|
||||
false,
|
||||
event.clipboardData.getData('text/plain')
|
||||
);
|
||||
}
|
||||
|
||||
async submit() {
|
||||
let self = this;
|
||||
let draft = this.get_draft();
|
||||
let edit = this.renderRoot.getElementById('edit');
|
||||
let message = {
|
||||
type: 'post',
|
||||
text: edit.innerText,
|
||||
};
|
||||
if (this.root || this.branch) {
|
||||
message.root = this.root;
|
||||
message.branch = this.branch;
|
||||
}
|
||||
if (Object.values(draft.mentions || {}).length) {
|
||||
message.mentions = Object.values(draft.mentions);
|
||||
}
|
||||
if (draft.content_warning !== undefined) {
|
||||
message.contentWarning = draft.content_warning;
|
||||
}
|
||||
console.log('Would post:', message);
|
||||
if (draft.encrypt_to) {
|
||||
let to = new Set(draft.encrypt_to);
|
||||
to.add(this.whoami);
|
||||
to = [...to];
|
||||
message.recps = to;
|
||||
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);
|
||||
self.notify(undefined);
|
||||
} catch (error) {
|
||||
alert(error.message);
|
||||
}
|
||||
}
|
||||
|
||||
discard() {
|
||||
this.notify(undefined);
|
||||
}
|
||||
|
||||
attach() {
|
||||
let self = this;
|
||||
let input = document.createElement('input');
|
||||
input.type = 'file';
|
||||
input.onchange = function (event) {
|
||||
let file = event.target.files[0];
|
||||
self.add_file(file);
|
||||
};
|
||||
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('"', '""') + '"', `%![%${text}%](%)%`]
|
||||
);
|
||||
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() {
|
||||
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({
|
||||
iframe: this.shadowRoot,
|
||||
collection: [
|
||||
{
|
||||
values: values,
|
||||
selectTemplate: function (item) {
|
||||
return item
|
||||
? `[@${item.original.key}](${item.original.value})`
|
||||
: undefined;
|
||||
},
|
||||
},
|
||||
{
|
||||
trigger: '&',
|
||||
values: this.autocomplete,
|
||||
selectTemplate: function (item) {
|
||||
return item
|
||||
? `![${item.original.key}](${item.original.value})`
|
||||
: undefined;
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
tribute.attach(this.renderRoot.getElementById('edit'));
|
||||
}
|
||||
|
||||
updated() {
|
||||
super.updated();
|
||||
let edit = this.renderRoot.getElementById('edit');
|
||||
if (this.last_updated_text !== edit.innerText) {
|
||||
let preview = this.renderRoot.getElementById('preview');
|
||||
preview.innerHTML = this.process_text(edit.innerText);
|
||||
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) {
|
||||
let draft = this.get_draft();
|
||||
delete draft.mentions[id];
|
||||
setTimeout(() => this.notify(), 0);
|
||||
}
|
||||
|
||||
render_mention(mention) {
|
||||
let self = this;
|
||||
return html` <div style="display: flex; flex-direction: row">
|
||||
<div style="align-self: center; margin: 0.5em">
|
||||
<button
|
||||
class="w3-button w3-theme-d1"
|
||||
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() {
|
||||
let self = this;
|
||||
|
||||
async function attach_selected_app() {
|
||||
let name = self.renderRoot.getElementById('select').value;
|
||||
let id = self.apps[name];
|
||||
let mentions = {};
|
||||
mentions[id] = {
|
||||
name: name,
|
||||
link: id,
|
||||
type: 'application/tildefriends',
|
||||
};
|
||||
if (name && id) {
|
||||
let app = JSON.parse(await tfrpc.rpc.get_blob(id));
|
||||
for (let entry of Object.entries(app.files)) {
|
||||
mentions[entry[1]] = {
|
||||
name: entry[0],
|
||||
link: entry[1],
|
||||
};
|
||||
}
|
||||
}
|
||||
let draft = self.get_draft();
|
||||
draft.mentions = Object.assign(draft.mentions || {}, mentions);
|
||||
self.requestUpdate();
|
||||
self.notify(draft);
|
||||
self.apps = null;
|
||||
}
|
||||
|
||||
if (this.apps) {
|
||||
return html`
|
||||
<div class="w3-card-4 w3-margin w3-padding">
|
||||
<select id="select" class="w3-select w3-theme-d1">
|
||||
${Object.keys(self.apps).map(
|
||||
(app) => html`<option value=${app}>${app}</option>`
|
||||
)}
|
||||
</select>
|
||||
<button class="w3-button w3-theme-d1" @click=${attach_selected_app}>
|
||||
Attach
|
||||
</button>
|
||||
<button
|
||||
class="w3-button w3-theme-d1"
|
||||
@click=${() => (this.apps = null)}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
render_attach_app_button() {
|
||||
let self = this;
|
||||
async function attach_app() {
|
||||
self.apps = await tfrpc.rpc.apps();
|
||||
}
|
||||
if (!this.apps) {
|
||||
return html`<button class="w3-button w3-theme-d1" @click=${attach_app}>
|
||||
Attach App
|
||||
</button>`;
|
||||
} else {
|
||||
return html`<button
|
||||
class="w3-button w3-theme-d1"
|
||||
@click=${() => (this.apps = null)}
|
||||
>
|
||||
Discard App
|
||||
</button>`;
|
||||
}
|
||||
}
|
||||
|
||||
set_content_warning(value) {
|
||||
let draft = this.get_draft();
|
||||
draft.content_warning = value;
|
||||
this.notify(draft);
|
||||
this.requestUpdate();
|
||||
}
|
||||
|
||||
render_content_warning() {
|
||||
let self = this;
|
||||
let draft = this.get_draft();
|
||||
if (draft.content_warning !== undefined) {
|
||||
return html`
|
||||
<div class="w3-container w3-padding">
|
||||
<p>
|
||||
<input type="checkbox" class="w3-check w3-theme-d1" id="cw" @change=${() => self.set_content_warning(undefined)} checked="checked"></input>
|
||||
<label for="cw">CW</label>
|
||||
</p>
|
||||
<input type="text" class="w3-input w3-border w3-theme-d1" id="content_warning" placeholder="Enter a content warning here." @input=${self.input} value=${draft.content_warning}></input>
|
||||
</div>
|
||||
`;
|
||||
} else {
|
||||
return html`
|
||||
<input type="checkbox" class="w3-check w3-theme-d1" id="cw" @change=${() => self.set_content_warning('')}></input>
|
||||
<label for="cw">CW</label>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
get_draft() {
|
||||
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() {
|
||||
let self = this;
|
||||
let draft = self.get_draft();
|
||||
let content_warning =
|
||||
draft.content_warning !== undefined
|
||||
? html`<div class="w3-panel w3-round-xlarge w3-theme-d2">
|
||||
<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`
|
||||
<div
|
||||
class="w3-card-4 w3-theme-d4 w3-padding-small"
|
||||
style="box-sizing: border-box"
|
||||
>
|
||||
${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="plaintext-only"
|
||||
.innerText=${live(draft.text ?? '')}
|
||||
></span>
|
||||
</div>
|
||||
<div class="w3-half w3-padding">
|
||||
${content_warning}
|
||||
<div id="preview"></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>
|
||||
`;
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define('tf-compose', TfComposeElement);
|
805
apps/ssb/tf-message.js
Normal file
805
apps/ssb/tf-message.js
Normal file
@ -0,0 +1,805 @@
|
||||
import {LitElement, html, render, unsafeHTML} from './lit-all.min.js';
|
||||
import * as tfrpc from '/static/tfrpc.js';
|
||||
import * as tfutils from './tf-utils.js';
|
||||
import * as emojis from './emojis.js';
|
||||
import {styles} from './tf-styles.js';
|
||||
|
||||
class TfMessageElement extends LitElement {
|
||||
static get properties() {
|
||||
return {
|
||||
whoami: {type: String},
|
||||
message: {type: Object},
|
||||
users: {type: Object},
|
||||
drafts: {type: Object},
|
||||
format: {type: String},
|
||||
blog_data: {type: String},
|
||||
expanded: {type: Object},
|
||||
};
|
||||
}
|
||||
|
||||
static styles = styles;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
let self = this;
|
||||
this.whoami = null;
|
||||
this.message = {};
|
||||
this.users = {};
|
||||
this.drafts = {};
|
||||
this.format = 'message';
|
||||
this.expanded = {};
|
||||
}
|
||||
|
||||
show_reply() {
|
||||
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);
|
||||
}
|
||||
|
||||
discard_reply() {
|
||||
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() {
|
||||
function normalize_expression(expression) {
|
||||
if (expression === 'Like' || !expression) {
|
||||
return '👍';
|
||||
} else if (expression === 'Unlike') {
|
||||
return '👎';
|
||||
} else if (expression === 'heart') {
|
||||
return '❤️';
|
||||
} else {
|
||||
return expression;
|
||||
}
|
||||
}
|
||||
if (this.message?.votes?.length) {
|
||||
return html` <div class="w3-container">
|
||||
<div
|
||||
class="w3-button w3-bar w3-padding-small"
|
||||
@click=${this.show_reactions}
|
||||
>
|
||||
${(this.message.votes || []).map(
|
||||
(vote) => html`
|
||||
<span
|
||||
class="w3-bar-item w3-padding-small"
|
||||
title="${this.users[vote.author]?.name ??
|
||||
vote.author} ${new Date(vote.timestamp)}"
|
||||
>
|
||||
${normalize_expression(vote.content.vote.expression)}
|
||||
</span>
|
||||
`
|
||||
)}
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
render_raw() {
|
||||
let raw = {
|
||||
id: this.message?.id,
|
||||
previous: this.message?.previous,
|
||||
author: this.message?.author,
|
||||
sequence: this.message?.sequence,
|
||||
timestamp: this.message?.timestamp,
|
||||
hash: this.message?.hash,
|
||||
content: this.message?.content,
|
||||
signature: this.message?.signature,
|
||||
};
|
||||
return html`<div style="white-space: pre-wrap">
|
||||
${JSON.stringify(raw, null, 2)}
|
||||
</div>`;
|
||||
}
|
||||
|
||||
vote(emoji) {
|
||||
let reaction = emoji;
|
||||
let message = this.message.id;
|
||||
if (
|
||||
confirm(
|
||||
'Are you sure you want to react with ' +
|
||||
reaction +
|
||||
' to ' +
|
||||
message +
|
||||
'?'
|
||||
)
|
||||
) {
|
||||
tfrpc.rpc
|
||||
.appendMessage(this.whoami, {
|
||||
type: 'vote',
|
||||
vote: {
|
||||
link: message,
|
||||
value: 1,
|
||||
expression: reaction,
|
||||
},
|
||||
})
|
||||
.catch(function (error) {
|
||||
alert(error?.message);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
react(event) {
|
||||
emojis.picker((x) => this.vote(x), null, this.whoami);
|
||||
}
|
||||
|
||||
show_image(link) {
|
||||
let div = document.createElement('div');
|
||||
div.style.left = 0;
|
||||
div.style.top = 0;
|
||||
div.style.width = '100%';
|
||||
div.style.height = '100%';
|
||||
div.style.position = 'fixed';
|
||||
div.style.background = '#000';
|
||||
div.style.zIndex = 100;
|
||||
div.style.display = 'grid';
|
||||
let img = document.createElement('img');
|
||||
img.src = link;
|
||||
img.style.maxWidth = '100%';
|
||||
img.style.maxHeight = '100%';
|
||||
img.style.display = 'block';
|
||||
img.style.margin = 'auto';
|
||||
img.style.objectFit = 'contain';
|
||||
img.style.width = '100%';
|
||||
div.appendChild(img);
|
||||
function image_close(event) {
|
||||
document.body.removeChild(div);
|
||||
window.removeEventListener('keydown', image_close);
|
||||
}
|
||||
div.onclick = image_close;
|
||||
window.addEventListener('keydown', image_close);
|
||||
document.body.appendChild(div);
|
||||
}
|
||||
|
||||
body_click(event) {
|
||||
if (event.srcElement.tagName == 'IMG') {
|
||||
this.show_image(event.srcElement.src);
|
||||
} 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) {
|
||||
if (!mention?.link || typeof mention.link != 'string') {
|
||||
return html` <pre>${JSON.stringify(mention)}</pre>`;
|
||||
} else if (
|
||||
mention?.link?.startsWith('&') &&
|
||||
mention?.type?.startsWith('image/')
|
||||
) {
|
||||
return html`
|
||||
<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('&') &&
|
||||
mention.name?.startsWith('audio:')
|
||||
) {
|
||||
return html`
|
||||
<audio controls style="height: 32px">
|
||||
<source src=${'/' + mention.link + '/view'}></source>
|
||||
</audio>
|
||||
`;
|
||||
} else if (
|
||||
mention.link?.startsWith('&') &&
|
||||
mention.name?.startsWith('video:')
|
||||
) {
|
||||
return html`
|
||||
<video controls style="max-height: 240px; max-width: 128px">
|
||||
<source src=${'/' + mention.link + '/view'}></source>
|
||||
</video>
|
||||
`;
|
||||
} else if (
|
||||
mention.link?.startsWith('&') &&
|
||||
mention?.type === 'application/tildefriends'
|
||||
) {
|
||||
return html` <a href=${`/${mention.link}/`}>😎 ${mention.name}</a>`;
|
||||
} else if (mention.link?.startsWith('%') || mention.link?.startsWith('@')) {
|
||||
return html` <a href=${'#' + encodeURIComponent(mention.link)}
|
||||
>${mention.name}</a
|
||||
>`;
|
||||
} else if (mention.link?.startsWith('#')) {
|
||||
return html` <a href=${'#q=' + encodeURIComponent(mention.link)}
|
||||
>${mention.link}</a
|
||||
>`;
|
||||
} else if (
|
||||
Object.keys(mention).length == 2 &&
|
||||
mention.link &&
|
||||
mention.name
|
||||
) {
|
||||
return html` <a href=${`/${mention.link}/view`}>${mention.name}</a>`;
|
||||
} else {
|
||||
return html` <pre style="white-space: pre-wrap">
|
||||
${JSON.stringify(mention, null, 2)}</pre
|
||||
>`;
|
||||
}
|
||||
}
|
||||
|
||||
render_mentions() {
|
||||
let mentions = this.message?.content?.mentions || [];
|
||||
mentions = mentions.filter(
|
||||
(x) => this.message?.content?.text?.indexOf(x.link) === -1
|
||||
);
|
||||
if (mentions.length) {
|
||||
let self = this;
|
||||
return html`
|
||||
<fieldset style="padding: 0.5em; border: 1px solid black">
|
||||
<legend>Mentions</legend>
|
||||
${mentions.map((x) => self.render_mention(x))}
|
||||
</fieldset>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
total_child_messages(message) {
|
||||
if (!message.child_messages) {
|
||||
return 0;
|
||||
}
|
||||
let total = message.child_messages.length;
|
||||
for (let m of message.child_messages) {
|
||||
total += this.total_child_messages(m);
|
||||
}
|
||||
return total;
|
||||
}
|
||||
|
||||
set_expanded(expanded, tag) {
|
||||
this.dispatchEvent(
|
||||
new CustomEvent('tf-expand', {
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
detail: {id: (this.message.id || '') + (tag || ''), expanded: expanded},
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
toggle_expanded(tag) {
|
||||
this.set_expanded(
|
||||
!this.expanded[(this.message.id || '') + (tag || '')],
|
||||
tag
|
||||
);
|
||||
}
|
||||
|
||||
render_children() {
|
||||
let self = this;
|
||||
if (this.message.child_messages?.length) {
|
||||
if (!this.expanded[this.message.id]) {
|
||||
return html`<button
|
||||
class="w3-button w3-theme-d1"
|
||||
@click=${() => self.set_expanded(true)}
|
||||
>
|
||||
+ ${this.total_child_messages(this.message) + ' More'}
|
||||
</button>`;
|
||||
} else {
|
||||
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() {
|
||||
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 raw_button;
|
||||
switch (this.format) {
|
||||
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) {
|
||||
let body;
|
||||
return html`
|
||||
<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>
|
||||
<span style="padding-right: 8px"
|
||||
><a tfarget="_top" href=${'#' + self.message.id}>%</a> ${new Date(
|
||||
self.message.timestamp
|
||||
).toLocaleString()}</span
|
||||
>
|
||||
${raw_button} ${self.format == 'raw' ? self.render_raw() : inner}
|
||||
${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>
|
||||
`;
|
||||
}
|
||||
if (this.message?.type === 'contact_group') {
|
||||
return html` <div
|
||||
class="w3-card-4 w3-theme-d4 w3-border-theme"
|
||||
style="margin-top: 8px; padding: 16px; overflow-wrap: anywhere"
|
||||
>
|
||||
${this.message.messages.map(
|
||||
(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) {
|
||||
return html` <div
|
||||
class="w3-card-4 w3-theme-d4 w3-border-theme"
|
||||
style="margin-top: 8px; padding: 16px; overflow-wrap: anywhere"
|
||||
>
|
||||
<a target="_top" href=${'#' + this.message.id}>${this.message.id}</a>
|
||||
(placeholder)
|
||||
<div>${this.render_votes()}</div>
|
||||
${(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>
|
||||
`
|
||||
)}
|
||||
</div>`;
|
||||
} else if (typeof (content?.type === 'string')) {
|
||||
if (content.type == 'about') {
|
||||
let name;
|
||||
let image;
|
||||
let description;
|
||||
if (content.name !== undefined) {
|
||||
name = html`<div><b>Name:</b> ${content.name}</div>`;
|
||||
}
|
||||
if (content.image !== undefined) {
|
||||
image = html`
|
||||
<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) {
|
||||
description = html`
|
||||
<div style="flex: 1 0 50%; overflow-wrap: anywhere">
|
||||
<div>${unsafeHTML(tfutils.markdown(content.description))}</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
let update =
|
||||
content.about == this.message.author
|
||||
? html`<div style="font-weight: bold">Updated profile.</div>`
|
||||
: html`<div style="font-weight: bold">
|
||||
Updated profile for
|
||||
<tf-user id=${content.about} .users=${this.users}></tf-user>.
|
||||
</div>`;
|
||||
return small_frame(html` ${update} ${name} ${image} ${description} `);
|
||||
} else if (content.type == 'contact') {
|
||||
return html`
|
||||
<div>
|
||||
<tf-user id=${this.message.author} .users=${this.users}></tf-user>
|
||||
is
|
||||
${content.blocking === true
|
||||
? 'blocking'
|
||||
: content.blocking === false
|
||||
? 'no longer blocking'
|
||||
: content.following === true
|
||||
? 'following'
|
||||
: content.following === false
|
||||
? 'no longer following'
|
||||
: '?'}
|
||||
<tf-user
|
||||
id=${this.message.content.contact}
|
||||
.users=${this.users}
|
||||
></tf-user>
|
||||
</div>
|
||||
`;
|
||||
} else if (content.type == 'post') {
|
||||
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>
|
||||
`;
|
||||
let self = this;
|
||||
let body;
|
||||
switch (this.format) {
|
||||
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`
|
||||
<div
|
||||
class="w3-panel w3-round-xlarge w3-theme-l4"
|
||||
style="cursor: pointer"
|
||||
@click=${(x) => this.toggle_expanded(':cw')}
|
||||
>
|
||||
<p>${content.contentWarning}</p>
|
||||
</div>
|
||||
`;
|
||||
let content_html = html`
|
||||
${this.render_channels()}
|
||||
<div @click=${this.body_click}>${body}</div>
|
||||
${this.render_mentions()}
|
||||
`;
|
||||
let payload = content.contentWarning
|
||||
? self.expanded[(this.message.id || '') + ':cw']
|
||||
? 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`
|
||||
<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>
|
||||
${payload} ${this.render_votes()}
|
||||
<p>
|
||||
${reply}
|
||||
<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>
|
||||
${content.text} ${this.render_votes()}
|
||||
<p>
|
||||
<button class="w3-button w3-theme-d1" @click=${this.react}>
|
||||
React
|
||||
</button>
|
||||
</p>
|
||||
${this.render_children()}
|
||||
</div>
|
||||
`;
|
||||
} else if (content.type === 'blog') {
|
||||
let self = this;
|
||||
tfrpc.rpc.get_blob(content.blog).then(function (data) {
|
||||
self.blog_data = data;
|
||||
});
|
||||
let payload = this.expanded[(this.message.id || '') + ':blog']
|
||||
? html`<div>
|
||||
${this.blog_data
|
||||
? unsafeHTML(tfutils.markdown(this.blog_data))
|
||||
: 'Loading...'}
|
||||
</div>`
|
||||
: undefined;
|
||||
let body;
|
||||
switch (this.format) {
|
||||
case 'raw':
|
||||
body = this.render_raw();
|
||||
break;
|
||||
case 'md':
|
||||
body = content.summary;
|
||||
break;
|
||||
case 'message':
|
||||
body = html`
|
||||
<div
|
||||
style="border: 1px solid #fff; border-radius: 1em; padding: 8px; margin: 4px; cursor: pointer"
|
||||
@click=${(x) => self.toggle_expanded(':blog')}>
|
||||
<h2>${content.title}</h2>
|
||||
<div style="display: flex; flex-direction: row">
|
||||
<img src=/${content.thumbnail}/view></img>
|
||||
<span>${content.summary}</span>
|
||||
</div>
|
||||
</div>
|
||||
${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`
|
||||
<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 w3-theme-d4 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>
|
||||
<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>${body}</div>
|
||||
${this.render_mentions()}
|
||||
<div>
|
||||
${reply}
|
||||
<button class="w3-button w3-theme-d1" @click=${this.react}>
|
||||
React
|
||||
</button>
|
||||
</div>
|
||||
${this.render_votes()} ${this.render_children()}
|
||||
</div>
|
||||
`;
|
||||
} else if (content.type === 'pub') {
|
||||
return small_frame(
|
||||
html` <style>
|
||||
span {
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
</style>
|
||||
<span>
|
||||
<div>
|
||||
🍻
|
||||
<tf-user
|
||||
.users=${this.users}
|
||||
id=${content.address.key}
|
||||
></tf-user>
|
||||
</div>
|
||||
<pre>${content.address.host}:${content.address.port}</pre>
|
||||
</span>`
|
||||
);
|
||||
} else if (content.type === 'channel') {
|
||||
return small_frame(html`
|
||||
<div>
|
||||
${content.subscribed ? 'subscribed to' : 'unsubscribed from'}
|
||||
<a href=${'#q=' + encodeURIComponent('#' + content.channel)}
|
||||
>#${content.channel}</a
|
||||
>
|
||||
</div>
|
||||
`);
|
||||
} else if (typeof this.message.content == 'string') {
|
||||
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 {
|
||||
return small_frame(html`<div><b>type</b>: ${content.type}</div>`);
|
||||
}
|
||||
} else {
|
||||
return small_frame(this.render_raw());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define('tf-message', TfMessageElement);
|
203
apps/ssb/tf-news.js
Normal file
203
apps/ssb/tf-news.js
Normal file
@ -0,0 +1,203 @@
|
||||
import {LitElement, html, unsafeHTML, until} from './lit-all.min.js';
|
||||
import * as tfrpc from '/static/tfrpc.js';
|
||||
import {styles} from './tf-styles.js';
|
||||
|
||||
class TfNewsElement extends LitElement {
|
||||
static get properties() {
|
||||
return {
|
||||
whoami: {type: String},
|
||||
users: {type: Object},
|
||||
messages: {type: Array},
|
||||
following: {type: Array},
|
||||
drafts: {type: Object},
|
||||
expanded: {type: Object},
|
||||
};
|
||||
}
|
||||
|
||||
static styles = styles;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
let self = this;
|
||||
this.whoami = null;
|
||||
this.users = {};
|
||||
this.messages = [];
|
||||
this.following = [];
|
||||
this.drafts = {};
|
||||
this.expanded = {};
|
||||
}
|
||||
|
||||
process_messages(messages) {
|
||||
let self = this;
|
||||
let messages_by_id = {};
|
||||
|
||||
console.log('processing', messages.length, 'messages');
|
||||
|
||||
function ensure_message(id) {
|
||||
let found = messages_by_id[id];
|
||||
if (found) {
|
||||
return found;
|
||||
} else {
|
||||
let added = {
|
||||
id: id,
|
||||
placeholder: true,
|
||||
content: '"placeholder"',
|
||||
parent_message: undefined,
|
||||
child_messages: [],
|
||||
votes: [],
|
||||
};
|
||||
messages_by_id[id] = added;
|
||||
return added;
|
||||
}
|
||||
}
|
||||
|
||||
function link_message(message) {
|
||||
if (message.content.type === 'vote') {
|
||||
let parent = ensure_message(message.content.vote.link);
|
||||
if (!parent.votes) {
|
||||
parent.votes = [];
|
||||
}
|
||||
parent.votes.push(message);
|
||||
message.parent_message = message.content.vote.link;
|
||||
} else if (message.content.type == 'post') {
|
||||
if (message.content.root) {
|
||||
if (typeof message.content.root === 'string') {
|
||||
let m = ensure_message(message.content.root);
|
||||
if (!m.child_messages) {
|
||||
m.child_messages = [];
|
||||
}
|
||||
m.child_messages.push(message);
|
||||
message.parent_message = message.content.root;
|
||||
} else {
|
||||
let m = ensure_message(message.content.root[0]);
|
||||
if (!m.child_messages) {
|
||||
m.child_messages = [];
|
||||
}
|
||||
m.child_messages.push(message);
|
||||
message.parent_message = message.content.root[0];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (let message of messages) {
|
||||
message.votes = [];
|
||||
message.parent_message = undefined;
|
||||
message.child_messages = undefined;
|
||||
}
|
||||
|
||||
for (let message of messages) {
|
||||
try {
|
||||
message.content = JSON.parse(message.content);
|
||||
} catch {}
|
||||
if (!messages_by_id[message.id]) {
|
||||
messages_by_id[message.id] = message;
|
||||
link_message(message);
|
||||
} else if (messages_by_id[message.id].placeholder) {
|
||||
let placeholder = messages_by_id[message.id];
|
||||
messages_by_id[message.id] = message;
|
||||
message.parent_message = placeholder.parent_message;
|
||||
message.child_messages = placeholder.child_messages;
|
||||
message.votes = placeholder.votes;
|
||||
if (
|
||||
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.push(message);
|
||||
}
|
||||
link_message(message);
|
||||
}
|
||||
}
|
||||
|
||||
return messages_by_id;
|
||||
}
|
||||
|
||||
update_latest_subtree_timestamp(messages) {
|
||||
let latest = 0;
|
||||
for (let message of messages || []) {
|
||||
if (message.latest_subtree_timestamp === undefined) {
|
||||
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);
|
||||
}
|
||||
return latest;
|
||||
}
|
||||
|
||||
finalize_messages(messages_by_id) {
|
||||
function recursive_sort(messages, top) {
|
||||
if (messages) {
|
||||
if (top) {
|
||||
messages.sort(
|
||||
(a, b) => b.latest_subtree_timestamp - a.latest_subtree_timestamp
|
||||
);
|
||||
} else {
|
||||
messages.sort((a, b) => a.timestamp - b.timestamp);
|
||||
}
|
||||
for (let message of messages) {
|
||||
recursive_sort(message.child_messages, false);
|
||||
}
|
||||
return messages.map((x) => Object.assign({}, x));
|
||||
} else {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
let roots = Object.values(messages_by_id).filter((x) => !x.parent_message);
|
||||
this.update_latest_subtree_timestamp(roots);
|
||||
return recursive_sort(roots, true);
|
||||
}
|
||||
|
||||
group_following(messages) {
|
||||
let result = [];
|
||||
let group = [];
|
||||
for (let message of messages) {
|
||||
if (message?.content?.type === 'contact') {
|
||||
group.push(message);
|
||||
} else {
|
||||
if (group.length > 0) {
|
||||
result.push({
|
||||
type: 'contact_group',
|
||||
messages: group,
|
||||
});
|
||||
group = [];
|
||||
}
|
||||
result.push(message);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
load_and_render(messages) {
|
||||
let messages_by_id = this.process_messages(messages);
|
||||
let final_messages = this.group_following(
|
||||
this.finalize_messages(messages_by_id)
|
||||
);
|
||||
return html`
|
||||
<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>`
|
||||
)}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
render() {
|
||||
return this.load_and_render(this.messages || []);
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define('tf-news', TfNewsElement);
|
318
apps/ssb/tf-profile.js
Normal file
318
apps/ssb/tf-profile.js
Normal file
@ -0,0 +1,318 @@
|
||||
import {LitElement, html, unsafeHTML} from './lit-all.min.js';
|
||||
import * as tfrpc from '/static/tfrpc.js';
|
||||
import * as tfutils from './tf-utils.js';
|
||||
import {styles} from './tf-styles.js';
|
||||
|
||||
class TfProfileElement extends LitElement {
|
||||
static get properties() {
|
||||
return {
|
||||
editing: {type: Object},
|
||||
whoami: {type: String},
|
||||
id: {type: String},
|
||||
users: {type: Object},
|
||||
size: {type: Number},
|
||||
server_follows_me: {type: Boolean},
|
||||
following: {type: Boolean},
|
||||
blocking: {type: Boolean},
|
||||
};
|
||||
}
|
||||
|
||||
static styles = styles;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
let self = this;
|
||||
this.editing = null;
|
||||
this.whoami = null;
|
||||
this.id = null;
|
||||
this.users = {};
|
||||
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) {
|
||||
tfrpc.rpc
|
||||
.appendMessage(
|
||||
this.whoami,
|
||||
Object.assign(
|
||||
{
|
||||
type: 'contact',
|
||||
contact: this.id,
|
||||
},
|
||||
change
|
||||
)
|
||||
)
|
||||
.catch(function (error) {
|
||||
alert(error?.message);
|
||||
});
|
||||
}
|
||||
|
||||
follow() {
|
||||
this.modify({following: true});
|
||||
}
|
||||
|
||||
unfollow() {
|
||||
this.modify({following: false});
|
||||
}
|
||||
|
||||
block() {
|
||||
this.modify({blocking: true});
|
||||
}
|
||||
|
||||
unblock() {
|
||||
this.modify({blocking: false});
|
||||
}
|
||||
|
||||
edit() {
|
||||
let original = this.users[this.id];
|
||||
this.editing = {
|
||||
name: original.name,
|
||||
description: original.description,
|
||||
image: original.image,
|
||||
publicWebHosting: original.publicWebHosting,
|
||||
};
|
||||
console.log(this.editing);
|
||||
}
|
||||
|
||||
save_edits() {
|
||||
let self = this;
|
||||
let message = {
|
||||
type: 'about',
|
||||
about: this.whoami,
|
||||
};
|
||||
for (let key of Object.keys(this.editing)) {
|
||||
if (this.editing[key] !== this.users[this.id][key]) {
|
||||
message[key] = this.editing[key];
|
||||
}
|
||||
}
|
||||
tfrpc.rpc
|
||||
.appendMessage(this.whoami, message)
|
||||
.then(function () {
|
||||
self.editing = null;
|
||||
})
|
||||
.catch(function (error) {
|
||||
alert(error?.message);
|
||||
});
|
||||
}
|
||||
|
||||
discard_edits() {
|
||||
this.editing = null;
|
||||
}
|
||||
|
||||
attach_image() {
|
||||
let self = this;
|
||||
let input = document.createElement('input');
|
||||
input.type = 'file';
|
||||
input.onchange = function (event) {
|
||||
let file = event.target.files[0];
|
||||
file
|
||||
.arrayBuffer()
|
||||
.then(function (buffer) {
|
||||
let bin = Array.from(new Uint8Array(buffer));
|
||||
return tfrpc.rpc.store_blob(bin);
|
||||
})
|
||||
.then(function (id) {
|
||||
self.editing = Object.assign({}, self.editing, {image: id});
|
||||
console.log(self.editing);
|
||||
})
|
||||
.catch(function (e) {
|
||||
alert(e.message);
|
||||
});
|
||||
};
|
||||
input.click();
|
||||
}
|
||||
|
||||
async server_follow_me(follow) {
|
||||
try {
|
||||
await tfrpc.rpc.setServerFollowingMe(this.whoami, follow);
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
}
|
||||
try {
|
||||
await this.initial_load();
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
}
|
||||
}
|
||||
|
||||
copy_id() {
|
||||
navigator.clipboard.writeText(this.id);
|
||||
}
|
||||
|
||||
render() {
|
||||
if (
|
||||
this.id == this.whoami &&
|
||||
this.editing &&
|
||||
this.server_follows_me === undefined
|
||||
) {
|
||||
this.initial_load();
|
||||
}
|
||||
this.load();
|
||||
let self = this;
|
||||
let profile = this.users[this.id] || {};
|
||||
tfrpc.rpc
|
||||
.query(
|
||||
`SELECT SUM(LENGTH(content)) AS size FROM messages WHERE author = ?`,
|
||||
[this.id]
|
||||
)
|
||||
.then(function (result) {
|
||||
self.size = result[0].size;
|
||||
});
|
||||
let edit;
|
||||
let follow;
|
||||
let block;
|
||||
if (this.id === this.whoami) {
|
||||
if (this.editing) {
|
||||
let server_follow;
|
||||
if (this.server_follows_me === true) {
|
||||
server_follow = html`<button
|
||||
class="w3-button w3-theme-d1"
|
||||
@click=${() => this.server_follow_me(false)}
|
||||
>
|
||||
Server, Stop Following Me
|
||||
</button>`;
|
||||
} else if (this.server_follows_me === false) {
|
||||
server_follow = html`<button
|
||||
class="w3-button w3-theme-d1"
|
||||
@click=${() => this.server_follow_me(true)}
|
||||
>
|
||||
Server, Follow Me
|
||||
</button>`;
|
||||
}
|
||||
edit = html`
|
||||
<button class="w3-button w3-theme-d1" @click=${this.save_edits}>
|
||||
Save Profile
|
||||
</button>
|
||||
<button class="w3-button w3-theme-d1" @click=${this.discard_edits}>
|
||||
Discard
|
||||
</button>
|
||||
${server_follow}
|
||||
`;
|
||||
} else {
|
||||
edit = html`<button class="w3-button w3-theme-d1" @click=${this.edit}>
|
||||
Edit Profile
|
||||
</button>`;
|
||||
}
|
||||
}
|
||||
if (this.id !== this.whoami && this.following !== undefined) {
|
||||
follow = this.following
|
||||
? html`<button class="w3-button w3-theme-d1" @click=${this.unfollow}>
|
||||
Unfollow
|
||||
</button>`
|
||||
: html`<button class="w3-button w3-theme-d1" @click=${this.follow}>
|
||||
Follow
|
||||
</button>`;
|
||||
}
|
||||
if (this.id !== this.whoami && this.blocking !== undefined) {
|
||||
block = this.blocking
|
||||
? html`<button class="w3-button w3-theme-d1" @click=${this.unblock}>
|
||||
Unblock
|
||||
</button>`
|
||||
: html`<button class="w3-button w3-theme-d1" @click=${this.block}>
|
||||
Block
|
||||
</button>`;
|
||||
}
|
||||
let edit_profile = this.editing
|
||||
? html`
|
||||
<div style="flex: 1 0 50%; display: flex; flex-direction: column; gap: 8px">
|
||||
<div class="w3-container">
|
||||
<div>
|
||||
<label for="name">Name:</label>
|
||||
<input class="w3-input w3-theme-d1" type="text" id="name" value=${this.editing.name} @input=${(event) => (this.editing = Object.assign({}, this.editing, {name: event.srcElement.value}))}></input>
|
||||
</div>
|
||||
<div><label for="description">Description:</label></div>
|
||||
<textarea class="w3-input w3-theme-d1" style="resize: vertical" rows="8" id="description" @input=${(event) => (this.editing = Object.assign({}, this.editing, {description: event.srcElement.value}))}>${this.editing.description}</textarea>
|
||||
<div>
|
||||
<label for="public_web_hosting">Public Web Hosting:</label>
|
||||
<input class="w3-check w3-theme-d1" type="checkbox" id="public_web_hosting" ?checked=${this.editing.publicWebHosting} @input=${(event) => (self.editing = Object.assign({}, self.editing, {publicWebHosting: event.srcElement.checked}))}></input>
|
||||
</div>
|
||||
<div>
|
||||
<button class="w3-button w3-theme-d1" @click=${this.attach_image}>Attach Image</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>`
|
||||
: null;
|
||||
let image =
|
||||
typeof profile.image == 'string' ? profile.image : profile.image?.link;
|
||||
image = this.editing?.image ?? image;
|
||||
let description = this.editing?.description ?? profile.description;
|
||||
return html`<div class="w3-container" style="box-sizing: border-box; border: 2px solid black; background-color: rgba(255, 255, 255, 0.2)">
|
||||
<p><tf-user id=${this.id} .users=${this.users}></tf-user> (${tfutils.human_readable_size(this.size)})
|
||||
<input type="text" class="w3-input w3-border w3-theme-d1" readonly value=${this.id}></input>
|
||||
<button class="w3-button w3-theme-d1 w3-ripple" @click=${this.copy_id}>Copy</button>
|
||||
<div style="display: flex; flex-direction: row; gap: 1em">
|
||||
${edit_profile}
|
||||
<div style="flex: 1 0 50%">
|
||||
<div><img src=${'/' + image + '/view'} style="width: 256px; height: auto"></img></div>
|
||||
<div>${unsafeHTML(tfutils.markdown(description))}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
Following ${profile.following} identities.
|
||||
Followed by ${profile.followed} identities.
|
||||
Blocking ${profile.blocking} identities.
|
||||
Blocked by ${profile.blocked} identities.
|
||||
</div>
|
||||
<p>
|
||||
${edit}
|
||||
${follow}
|
||||
${block}
|
||||
</p>
|
||||
</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define('tf-profile', TfProfileElement);
|
72
apps/ssb/tf-reactions-modal.js
Normal file
72
apps/ssb/tf-reactions-modal.js
Normal file
@ -0,0 +1,72 @@
|
||||
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"
|
||||
@click=${this.clear}
|
||||
>
|
||||
<div
|
||||
class="w3-modal-content w3-card-4 w3-theme-d1"
|
||||
onclick="event.stopPropagation()"
|
||||
>
|
||||
<div class="w3-container w3-padding">
|
||||
<header class="w3-container">
|
||||
<h2>Reactions</h2>
|
||||
<span class="w3-button w3-display-topright" @click=${this.clear}
|
||||
>×</span
|
||||
>
|
||||
</header>
|
||||
<ul class="w3-theme-dark w3-container w3-ul">
|
||||
${this.votes.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);
|
314
apps/ssb/tf-styles.js
Normal file
314
apps/ssb/tf-styles.js
Normal file
@ -0,0 +1,314 @@
|
||||
import {css} from './lit-all.min.js';
|
||||
|
||||
const tf = css`
|
||||
img {
|
||||
max-width: min(640px, 100%);
|
||||
max-height: min(480px, auto);
|
||||
}
|
||||
|
||||
.tab {
|
||||
border: 0;
|
||||
padding: 8px;
|
||||
margin: 0px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.tab:disabled {
|
||||
color: #088;
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
.content_warning {
|
||||
border: 1px solid #fff;
|
||||
border-radius: 1em;
|
||||
padding: 8px;
|
||||
margin: 4px;
|
||||
}
|
||||
|
||||
div.img_caption {
|
||||
color: #888;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
div.img_caption::after {
|
||||
content: ' ±';
|
||||
}
|
||||
|
||||
pre code {
|
||||
display: block;
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
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];
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user